Files
meditation-admin/app/dashboard/chat-topics/page.tsx
T
2026-06-03 17:08:59 +03:30

239 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { apiFetch, ApiError } from "@/lib/api";
import { useList } from "@/lib/useResource";
import { useToast } from "@/components/toast";
import { DataTable, type Column } from "@/components/DataTable";
import {
Badge,
Button,
ConfirmDialog,
Field,
Input,
Textarea,
Switch,
Modal,
PageHeader,
} from "@/components/ui";
import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
import { toFa } from "@/lib/utils";
interface ChatTopic {
id: number;
title?: string;
description?: string | null;
order?: number | null;
is_active?: boolean;
}
export default function ChatTopicsPage() {
const { data, loading, error, reload } = useList<ChatTopic>("/chat-topics");
const toast = useToast();
const [editing, setEditing] = useState<ChatTopic | null>(null);
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [order, setOrder] = useState("");
const [isActive, setIsActive] = useState(true);
const [deleting, setDeleting] = useState<ChatTopic | null>(null);
const [removing, setRemoving] = useState(false);
function openCreate() {
setEditing(null);
setTitle("");
setDescription("");
setOrder("");
setIsActive(true);
setOpen(true);
}
function openEdit(row: ChatTopic) {
setEditing(row);
setTitle(row.title ?? "");
setDescription(row.description ?? "");
setOrder(row.order != null ? String(row.order) : "");
setIsActive(row.is_active ?? true);
setOpen(true);
}
async function save(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
try {
// chat-topics is a JSON apiResource (no files).
const payload = {
title,
description: description || null,
order: order === "" ? null : Number(order),
is_active: isActive,
};
if (editing) {
await apiFetch(`/chat-topics/${editing.id}`, {
method: "PUT",
body: payload,
});
toast.success("موضوع ویرایش شد.");
} else {
await apiFetch("/chat-topics", { method: "POST", body: 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(`/chat-topics/${deleting.id}`, { method: "DELETE" });
toast.success("موضوع حذف شد.");
setDeleting(null);
reload();
} catch (err) {
toast.error(err instanceof ApiError ? err.message : "خطا در حذف");
} finally {
setRemoving(false);
}
}
const columns: Column<ChatTopic>[] = [
{ key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" },
{ key: "title", header: "عنوان", render: (r) => r.title ?? "—" },
{
key: "description",
header: "توضیحات",
render: (r) => r.description || "—",
},
{
key: "order",
header: "ترتیب",
render: (r) => toFa(r.order ?? "—"),
className: "w-20",
},
{
key: "is_active",
header: "وضعیت",
render: (r) =>
r.is_active ? (
<Badge tone="success">فعال</Badge>
) : (
<Badge tone="neutral">غیرفعال</Badge>
),
},
];
return (
<div>
<PageHeader
title="موضوعات پیشنهادی گفتگو"
subtitle="موضوعاتی که در بخش گفتگو با مشاور به کاربر پیشنهاد می‌شود"
action={
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
موضوع جدید
</Button>
}
/>
<DataTable
columns={columns}
rows={data}
loading={loading}
error={error}
onRetry={reload}
emptyMessage="هنوز موضوعی ثبت نشده است."
emptyAction={
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
موضوع جدید
</Button>
}
actions={(row) => (
<>
<Button variant="ghost" onClick={() => openEdit(row)} aria-label="ویرایش">
<EditIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={() => setDeleting(row)}
aria-label="حذف"
className="text-danger"
>
<TrashIcon className="h-4 w-4" />
</Button>
</>
)}
/>
<Modal
open={open}
onClose={() => setOpen(false)}
title={editing ? "ویرایش موضوع" : "موضوع جدید"}
footer={
<>
<Button variant="secondary" onClick={() => setOpen(false)}>
انصراف
</Button>
<Button form="chat-topic-form" type="submit" loading={saving}>
ذخیره
</Button>
</>
}
>
<form id="chat-topic-form" onSubmit={save} className="flex flex-col gap-4">
<Field label="عنوان" required>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="مثلاً هیچ راه واحدی برای مدیتیشن وجود ندارد"
required
autoFocus
/>
</Field>
<Field label="توضیحات">
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="توضیح اختیاری درباره موضوع"
/>
</Field>
<Field label="ترتیب">
<Input
type="number"
dir="ltr"
value={order}
onChange={(e) => setOrder(e.target.value)}
placeholder="مثلاً ۱"
/>
</Field>
<Field label="وضعیت">
<Switch
checked={isActive}
onChange={setIsActive}
label={isActive ? "فعال" : "غیرفعال"}
/>
</Field>
</form>
</Modal>
<ConfirmDialog
open={!!deleting}
message={`آیا از حذف «${deleting?.title ?? ""}» مطمئن هستید؟`}
loading={removing}
onConfirm={confirmDelete}
onClose={() => setDeleting(null)}
/>
</div>
);
}