import { useEffect, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { PlusIcon } from '@heroicons/react/24/outline'; import { api, ApiError, type ApiResponse } from '../lib/api'; import PageHeader from '../components/ui/PageHeader'; import Modal from '../components/ui/Modal'; import Field from '../components/ui/Field'; import Input from '../components/ui/Input'; import Switch from '../components/ui/Switch'; import DataTable, { type Column } from '../components/ui/DataTable'; import type { PracticeDomain } from '../types'; const CODE_PATTERN = /^[a-z0-9_]{1,40}$/; /** * حوزه‌های فعالیت — فقط ادمین پلتفرم. * * ساختنش عمداً دست مدیر کلینیک نیست: هر حوزه به یک پیاده‌سازی `TreatmentWorkflow` * گره خورده، و حوزه‌ای که کسی برایش کد ننوشته بی‌صدا رفتار پیش‌فرض می‌گیرد. ستون * «فرآیند اختصاصی» همین را نشان می‌دهد تا حدس زده نشود. */ export default function AdminPracticeDomainsPage() { const qc = useQueryClient(); const [editing, setEditing] = useState<{ open: boolean; domain: PracticeDomain | null }>({ open: false, domain: null }); const { data, isLoading } = useQuery({ queryKey: ['practice-domains'], queryFn: () => api.get>('/api/v1/practice-domains'), }); const invalidate = () => qc.invalidateQueries({ queryKey: ['practice-domains'] }); const fail = (e: unknown, fallback: string) => toast.error(e instanceof ApiError ? e.message : fallback); const create = useMutation({ mutationFn: (d: { code: string; name: string; sort_order: number }) => api.post>('/api/v1/practice-domains', d), onSuccess: () => { toast.success('حوزهٔ فعالیت افزوده شد'); invalidate(); }, onError: (e) => fail(e, 'افزودن حوزه ناموفق بود'), }); const update = useMutation({ mutationFn: ({ uuid, d }: { uuid: string; d: { name: string; sort_order: number; active: boolean } }) => api.patch>(`/api/v1/practice-domain/${uuid}`, d), onSuccess: () => { toast.success('حوزهٔ فعالیت به‌روزرسانی شد'); invalidate(); }, onError: (e) => fail(e, 'به‌روزرسانی ناموفق بود'), }); const columns: Column[] = [ { key: 'name', header: 'نام', render: (d) => {d.name} }, { key: 'code', header: 'کد', render: (d) => {d.code} }, { key: 'has_workflow', header: 'فرآیند اختصاصی', render: (d) => ( {d.has_workflow ? 'دارد' : 'پیش‌فرض'} ), }, { key: 'active', header: 'وضعیت', render: (d) => ( {d.active ? 'فعال' : 'غیرفعال'} ), }, { key: 'actions', header: '', render: (d) => ( ), }, ]; return ( <> setEditing({ open: true, domain: null })}> افزودن حوزه } /> setEditing({ open: false, domain: null })} onSave={(payload) => { const opts = { onSuccess: () => setEditing({ open: false, domain: null }) }; if (editing.domain) { update.mutate({ uuid: editing.domain.uuid, d: { name: payload.name, sort_order: payload.sortOrder, active: payload.active }, }, opts); } else { create.mutate({ code: payload.code, name: payload.name, sort_order: payload.sortOrder }, opts); } }} /> ); } function DomainModal({ open, domain, saving, onClose, onSave }: { open: boolean; domain: PracticeDomain | null; saving: boolean; onClose: () => void; onSave: (p: { code: string; name: string; sortOrder: number; active: boolean }) => void; }) { const [code, setCode] = useState(''); const [name, setName] = useState(''); const [sortOrder, setSortOrder] = useState(0); const [active, setActive] = useState(true); useEffect(() => { if (!open) return; setCode(domain?.code ?? ''); setName(domain?.name ?? ''); setSortOrder(domain?.sort_order ?? 0); setActive(domain?.active ?? true); }, [open, domain]); const isEdit = domain !== null; const codeValid = CODE_PATTERN.test(code); const invalid = name.trim() === '' || (!isEdit && !codeValid); return ( } >
{ e.preventDefault(); if (!saving && !invalid) onSave({ code: code.trim(), name: name.trim(), sortOrder, active }); }} > setCode(e.target.value)} style={isEdit ? { opacity: 0.6, cursor: 'not-allowed' } : undefined} /> {isEdit ? 'کد پس از ساخت تغییر نمی‌کند؛ فرآیند درمان روی همین کد سوار می‌شود.' : 'فرآیند درمان به این کد گره می‌خورد، پس بعداً قابل تغییر نیست.'} setName(e.target.value)} /> setSortOrder(Number(e.target.value))} /> {isEdit && ( )}
); }