239 lines
6.7 KiB
TypeScript
239 lines
6.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|