diff --git a/app/dashboard/breathing/page.tsx b/app/dashboard/breathing/page.tsx new file mode 100644 index 0000000..37f124c --- /dev/null +++ b/app/dashboard/breathing/page.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { useState } from "react"; +import { apiFetch, ApiError, toFormData } from "@/lib/api"; +import { useList } from "@/lib/useResource"; +import { useToast } from "@/components/toast"; +import { DataTable, type Column } from "@/components/DataTable"; +import { + Button, + ConfirmDialog, + Field, + Input, + Textarea, + Modal, + PageHeader, +} from "@/components/ui"; +import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { toFa, formatDuration } from "@/lib/utils"; + +interface BreathingTemplate { + id: number; + name?: string; + inhale?: number; + exhale?: number; + breath_hold?: number; + duration?: number; + description?: string; + image_id?: number; +} + +interface BreathingForm { + name: string; + inhale: string; + exhale: string; + breath_hold: string; + duration: string; + description: string; + image_id: string; +} + +const emptyForm: BreathingForm = { + name: "", + inhale: "", + exhale: "", + breath_hold: "", + duration: "", + description: "", + image_id: "", +}; + +function numOrUndefined(v: string): number | undefined { + if (v === "") return undefined; + const n = Number(v); + return Number.isNaN(n) ? undefined : n; +} + +export default function BreathingPage() { + const { data, loading, error, reload } = + useList("/breathing-templates"); + const toast = useToast(); + + const [editing, setEditing] = useState(null); + const [open, setOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const [saving, setSaving] = useState(false); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + function set(key: K, value: string) { + setForm((f) => ({ ...f, [key]: value })); + } + + function openCreate() { + setEditing(null); + setForm(emptyForm); + setOpen(true); + } + function openEdit(row: BreathingTemplate) { + setEditing(row); + setForm({ + name: row.name ?? "", + inhale: row.inhale != null ? String(row.inhale) : "", + exhale: row.exhale != null ? String(row.exhale) : "", + breath_hold: row.breath_hold != null ? String(row.breath_hold) : "", + duration: row.duration != null ? String(row.duration) : "", + description: row.description ?? "", + image_id: row.image_id != null ? String(row.image_id) : "", + }); + setOpen(true); + } + + async function save(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + const payload = { + name: form.name, + inhale: numOrUndefined(form.inhale), + exhale: numOrUndefined(form.exhale), + breath_hold: numOrUndefined(form.breath_hold), + duration: numOrUndefined(form.duration), + description: form.description, + image_id: numOrUndefined(form.image_id), + }; + if (editing) { + // Update is JSON on /breathing-templates/:id + await apiFetch(`/breathing-templates/${editing.id}`, { + method: "PUT", + body: payload, + }); + toast.success("قالب تنفس ویرایش شد."); + } else { + // Create is multipart on /breathing-templates + await apiFetch("/breathing-templates", { + method: "POST", + body: toFormData(payload), + }); + toast.success("قالب تنفس افزوده شد."); + } + setOpen(false); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); + } finally { + setSaving(false); + } + } + + async function confirmDelete() { + if (!deleting) return; + setRemoving(true); + try { + await apiFetch(`/breathing-templates/${deleting.id}`, { + method: "DELETE", + }); + toast.success("قالب تنفس حذف شد."); + setDeleting(null); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در حذف"); + } finally { + setRemoving(false); + } + } + + const columns: Column[] = [ + { key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" }, + { key: "name", header: "نام", render: (r) => r.name ?? "—" }, + { + key: "inhale", + header: "دم", + render: (r) => (r.inhale != null ? toFa(r.inhale) : "—"), + }, + { + key: "exhale", + header: "بازدم", + render: (r) => (r.exhale != null ? toFa(r.exhale) : "—"), + }, + { + key: "breath_hold", + header: "حبس نفس", + render: (r) => (r.breath_hold != null ? toFa(r.breath_hold) : "—"), + }, + { + key: "duration", + header: "مدت زمان", + render: (r) => (r.duration != null ? formatDuration(r.duration) : "—"), + }, + ]; + + return ( +
+ } onClick={openCreate}> + قالب جدید + + } + /> + + } onClick={openCreate}> + قالب جدید + + } + actions={(row) => ( + <> + + + + )} + /> + + setOpen(false)} + title={editing ? "ویرایش قالب تنفس" : "قالب تنفس جدید"} + footer={ + <> + + + + } + > +
+ + set("name", e.target.value)} + placeholder="مثلاً تنفس آرام‌بخش" + required + autoFocus + /> + +
+ + set("inhale", e.target.value)} + dir="ltr" + /> + + + set("exhale", e.target.value)} + dir="ltr" + /> + + + set("breath_hold", e.target.value)} + dir="ltr" + /> + + + set("duration", e.target.value)} + dir="ltr" + /> + +
+ + set("image_id", e.target.value)} + dir="ltr" + /> + + +