import React, { useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { PlusIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import ResourceBlocksModal from '../components/ResourceBlocksModal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import SearchableSelect from '../components/ui/SearchableSelect'; import { ActiveBadge } from '../components/ui/StatusBadge'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useBranches } from '../hooks/useBranches'; import { useResources, useResourceTypes, useSkills } from '../hooks/useResources'; import ResourceFormModal from '../components/resources/ResourceFormModal'; import ResourceSkillsModal from '../components/resources/ResourceSkillsModal'; import type { ClinicResource } from '../types'; /** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */ const SUBJECT_LABEL: Record = { doctor: 'پزشک', staff: 'پرسنل', room: 'اتاق', }; /** * منابع — هر چیزی که ممکن است اشغال باشد. * * فیلترها (شعبه/نوع/مهارت/وضعیت) در URL می‌نشینند تا «بازگشت» و رفرش همان نما را * بدهند، و همان‌ها مستقیم به سرور می‌روند: فیلتر کردن سمت کلاینت با فهرستی که خودش * فیلترشده آمده، دوباره‌کاری و منبع اختلاف است. */ export default function ResourcesPage() { const [urlState, setUrlState] = useUrlState({ search: '', address: '', type: '', skill: '', status: '', }); const { branches } = useBranches(); const { types } = useResourceTypes(); const { skills } = useSkills(); const { can } = usePermissions(); const canUpdate = can('appointment_settings', 'update'); const { resources, loading, create, update, remove, setSkills } = useResources({ address_uuid: urlState.address || undefined, type_uuid: urlState.type || undefined, skill_uuid: urlState.skill || undefined, active: urlState.status || undefined, }); const [editing, setEditing] = useState<{ open: boolean; resource: ClinicResource | null }>({ open: false, resource: null }); const [skillsFor, setSkillsFor] = useState(null); const [blocksFor, setBlocksFor] = useState(null); const [toDelete, setToDelete] = useState(null); const rows = useMemo(() => { const q = urlState.search.trim(); return q === '' ? resources : resources.filter((r) => `${r.name} ${r.type_name}`.includes(q)); }, [resources, urlState.search]); const columns: Column[] = [ { key: 'name', header: 'منبع', render: (r) => (
{r.name} {r.type_name} {r.subject_kind ? ` · ${SUBJECT_LABEL[r.subject_kind]}` : ' · تجهیزات'}
), }, { key: 'address_name', header: 'شعبه', render: (r) => {r.address_name || '—'} }, { key: 'capacity', header: 'ظرفیت هم‌زمان', render: (r) => {r.capacity} نفر, }, { key: 'buffers', header: 'آماده‌سازی / تمیزکاری', render: (r) => ( {r.setup_minutes} / {r.cleanup_minutes} دقیقه ), }, { key: 'skills', header: 'مهارت‌ها', render: (r) => r.skills.length === 0 ? ( ) : (
{r.skills.map((s) => ( {s.skill_name} · {s.level} ))}
), }, { key: 'active', header: 'وضعیت', render: (r) => }, ]; return (
setEditing({ open: true, resource: null })}> افزودن منبع ) : undefined } /> setUrlState({ search: v })} searchPlaceholder="جستجو در منابع..." emptyMessage="هیچ منبعی با این فیلترها یافت نشد" headerExtra={
({ value: b.uuid, label: b.name || 'بدون نام' }))} value={urlState.address || null} onChange={(v) => setUrlState({ address: v ? String(v) : '' })} placeholder="همهٔ شعبه‌ها" isClearable height={36} />
({ value: t.uuid, label: t.name }))} value={urlState.type || null} onChange={(v) => setUrlState({ type: v ? String(v) : '' })} placeholder="همهٔ نوع‌ها" isClearable height={36} />
({ value: s.uuid, label: s.name }))} value={urlState.skill || null} onChange={(v) => setUrlState({ skill: v ? String(v) : '' })} placeholder="همهٔ مهارت‌ها" isClearable height={36} />
setUrlState({ status: v ? String(v) : '' })} placeholder="همهٔ وضعیت‌ها" isClearable height={36} />
} actions={ canUpdate ? (r) => (
تقویم {/* مسدودسازی موردی از تقویم جداست: آن الگوی کاری را عوض می‌کند، این یک بازهٔ مشخص را می‌بندد. */}
) : undefined } /> setBlocksFor(null)} /> setEditing({ open: false, resource: null })} onSave={(payload) => { const opts = { onSuccess: () => setEditing({ open: false, resource: null }) }; if (editing.resource) update.mutate({ uuid: editing.resource.uuid, d: payload }, opts); else create.mutate(payload, opts); }} /> setSkillsFor(null)} onSave={(lines) => skillsFor && setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) }) } /> toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} onCancel={() => setToDelete(null)} />
); }