import React, { useMemo, useState } from 'react'; import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import Modal from '../components/ui/Modal'; import PriceInput from '../components/ui/PriceInput'; import SearchableSelect from '../components/ui/SearchableSelect'; import PersianDateInput from '../components/ui/PersianDateInput'; import { formatDate, isoToUnix, unixToIso } from '../lib/utils'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useBranches } from '../hooks/useBranches'; import { usePriceLists, type PriceList, type PriceListItem } from '../hooks/usePriceLists'; import { useAllServiceItems } from '../hooks/useServiceCatalog'; interface Draft { uuid?: string; name: string; address_uuid: string | null; valid_from: number; valid_to: number; items: PriceListItem[]; } const DAY = 86400; function emptyDraft(): Draft { const now = Math.floor(Date.now() / 1000); return { name: '', address_uuid: null, valid_from: now, valid_to: now + 90 * DAY, items: [] }; } /** * لیست‌های قیمت بازه‌دار. * * وضعیت هر لیست سه حالت دارد و هر سه معنای عملیاتی متفاوتی دارند: پیش‌نویس هیچ اثری * روی قیمت امروز ندارد، فعال حاکم است، و منقضی فقط تاریخچه است. */ export default function PriceListsPage() { const { lists, loading, create, update, setItems, activate, remove } = usePriceLists(); const { branches } = useBranches(); const { items: services } = useAllServiceItems(); const { can } = usePermissions(); const canManage = can('appointment_settings', 'update'); const [urlState, setUrlState] = useUrlState({ search: '' }); const [draft, setDraft] = useState(null); const now = Math.floor(Date.now() / 1000); const rows = useMemo(() => { const q = urlState.search.trim(); return lists.filter((l) => q === '' || l.name.includes(q)); }, [lists, urlState.search]); const statusOf = (list: PriceList) => { if (!list.active) return { label: 'پیش‌نویس', className: 'badge' }; if (list.valid_to < now) return { label: 'منقضی', className: 'badge red' }; return { label: 'فعال', className: 'badge green' }; }; const columns: Column[] = [ { key: 'name', header: 'لیست', render: (l) => (
{l.name} {l.address_uuid === null ? 'همهٔ شعبه‌ها' : l.address_name ?? 'یک شعبه'}
), }, { key: 'range', header: 'بازهٔ اعتبار', render: (l) => ( {formatDate(l.valid_from)} تا {formatDate(l.valid_to)} ), }, { key: 'items', header: 'تعداد قیمت', render: (l) => {l.items.length}, }, { key: 'status', header: 'وضعیت', render: (l) => { const status = statusOf(l); return ( {status.label} ); }, }, ]; const save = async () => { if (!draft) return; const body = { name: draft.name, address_uuid: draft.address_uuid, valid_from: draft.valid_from, valid_to: draft.valid_to, }; const saved = draft.uuid ? await update.mutateAsync({ uuid: draft.uuid, body }) : await create.mutateAsync(body); await setItems.mutateAsync({ uuid: saved.data.uuid, items: draft.items }); setDraft(null); }; return (
setDraft(emptyDraft())}> لیست تازه ) : undefined } />
setUrlState({ search: v })} searchPlaceholder="جستجو در لیست‌ها..." emptyMessage="هنوز لیست قیمتی ساخته نشده است" actions={(l) => canManage ? (
{/* «کپی از لیست قبلی»: بیشتر لیست‌ها نسخهٔ کمی‌تغییریافتهٔ قبلی‌اند. */} {!l.active && ( )} {!l.active && ( )}
) : null } />
setDraft(null)} footer={ <> } > {draft && (
setDraft({ ...draft, name: e.target.value })} placeholder="مثلاً: تعرفهٔ نیمهٔ دوم ۱۴۰۵" />
setDraft({ ...draft, address_uuid: v ? String(v) : null })} options={[ { value: '', label: 'همهٔ شعبه‌ها' }, ...branches.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' })), ]} /> لیستِ یک شعبه بر لیست عمومی مقدم است و با آن تداخل حساب نمی‌شود.
setDraft({ ...draft, valid_from: isoToUnix(iso) ?? draft.valid_from })} />
setDraft({ ...draft, valid_to: isoToUnix(iso) ?? draft.valid_to })} />
{draft.valid_to <= draft.valid_from && ( پایان بازه باید بعد از شروع آن باشد. )}

قیمت‌ها

{draft.items.map((item, index) => (
setDraft({ ...draft, items: draft.items.map((it, i) => i === index ? { ...it, service_uuid: String(v ?? '') } : it, ), }) } options={services.map((s) => ({ value: s.uuid, label: s.name }))} placeholder="خدمت" />
setDraft({ ...draft, items: draft.items.map((it, i) => (i === index ? { ...it, price_rials: v } : it)), }) } suffix="ریال" />
))}
خدمتی که در این لیست نیاید، قیمتش از تعرفهٔ سال یا خودِ خدمت خوانده می‌شود — پس هیچ‌وقت بی‌قیمت نمی‌ماند.
)}
); }