"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, Select, Textarea, Modal, PageHeader, } from "@/components/ui"; import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; import { MediaPreview } from "@/components/MediaPreview"; import { pickUrl } from "@/lib/media"; import { toFa } from "@/lib/utils"; type CategoryType = "media" | "playlist" | "breathing_template"; interface Category { id: number; name: string; type: CategoryType; order?: number; description?: string | null; icon?: string | null; subcategories_count?: number; } // General category types, shared across media, playlists and breathing templates. const TYPE_OPTIONS: { value: CategoryType; label: string }[] = [ { value: "media", label: "رسانه" }, { value: "playlist", label: "پلی‌لیست" }, { value: "breathing_template", label: "قالب تنفس" }, ]; const TYPE_LABELS: Record = Object.fromEntries( TYPE_OPTIONS.map((o) => [o.value, o.label]), ) as Record; // The icon is a direct image-file field on the category (not an image_id ref). const ICON_KEYS = ["icon", "icon_url"]; export default function CategoriesPage() { const [typeFilter, setTypeFilter] = useState("media"); const { data, loading, error, reload } = useList("/categories", { type: typeFilter, }); const toast = useToast(); const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [type, setType] = useState("media"); const [order, setOrder] = useState("0"); const [description, setDescription] = useState(""); const [icon, setIcon] = useState(null); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(null); const [removing, setRemoving] = useState(false); function openCreate() { setEditing(null); setName(""); setType(typeFilter); setOrder("0"); setDescription(""); setIcon(null); setOpen(true); } function openEdit(row: Category) { setEditing(row); setName(row.name ?? ""); setType(row.type ?? "media"); setOrder(String(row.order ?? 0)); setDescription(row.description ?? ""); setIcon(null); setOpen(true); } async function save(e: React.FormEvent) { e.preventDefault(); setSaving(true); try { // Playlist categories live in the music_categories table, so their writes // go to /music-categories (image is the `image` field, not `icon`). const targetType = editing ? editing.type : type; if (targetType === "playlist") { const body = toFormData({ name, description, order, image: icon }); if (editing) { await apiFetch(`/music-categories/${editing.id}`, { method: "POST", body }); toast.success("دسته‌بندی ویرایش شد."); } else { await apiFetch("/music-categories", { method: "POST", body }); toast.success("دسته‌بندی افزوده شد."); } } else if (editing) { // Icon is a file, so update goes through POST + Laravel method spoofing. const body = toFormData({ _method: "PUT", name, type, order, description, icon, }); await apiFetch(`/categories/${editing.id}`, { method: "POST", body }); toast.success("دسته‌بندی ویرایش شد."); } else { await apiFetch("/categories", { method: "POST", body: toFormData({ name, type, order, description, icon }), }); 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 { const path = deleting.type === "playlist" ? `/music-categories/${deleting.id}` : `/categories/${deleting.id}`; await apiFetch(path, { method: "DELETE" }); toast.success("دسته‌بندی حذف شد."); setDeleting(null); reload(); } catch (err) { toast.error(err instanceof ApiError ? err.message : "خطا در حذف"); } finally { setRemoving(false); } } const columns: Column[] = [ { key: "icon", header: "آیکن", className: "w-20", render: (r) => ( ), }, { key: "order", header: "ترتیب", render: (r) => toFa(r.order ?? 0), className: "w-20", }, { key: "name", header: "نام دسته‌بندی" }, { key: "type", header: "نوع", render: (r) => TYPE_LABELS[r.type] ?? r.type, className: "w-28", }, { key: "description", header: "توضیحات", render: (r) => r.description || "—", }, { key: "subcategories_count", header: "زیر‌دسته‌ها", render: (r) => toFa(r.subcategories_count ?? 0), className: "w-28", }, ]; return (
} /> } onClick={openCreate}> دسته‌بندی جدید } actions={(row) => ( <> )} /> setOpen(false)} title={editing ? "ویرایش دسته‌بندی" : "دسته‌بندی جدید"} footer={ <> } >
setName(e.target.value)} placeholder="مثلاً کودکان" required autoFocus /> setOrder(e.target.value)} dir="ltr" />