import React, { useMemo, useState } from 'react'; import { Link } from 'react-router'; import { PlusIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import SearchableSelect from '../components/ui/SearchableSelect'; import { ActiveBadge } from '../components/ui/StatusBadge'; import { formatNumber } from '../lib/utils'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useResourceQuota, useResources, useResourceTypes, useSkills } from '../hooks/useResources'; import ResourceFormModal from '../components/resources/ResourceFormModal'; import type { ClinicResource } from '../types'; import ResourcesSubNav from '../components/resources/ResourcesSubNav'; /** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */ const SUBJECT_LABEL: Record = { doctor: 'پزشک', staff: 'پرسنل', room: 'اتاق', }; /** * منابع — هر چیزی که ممکن است اشغال باشد. * * فیلترها (شعبه/نوع/مهارت/وضعیت) در URL می‌نشینند تا «بازگشت» و رفرش همان نما را * بدهند، و همان‌ها مستقیم به سرور می‌روند: فیلتر کردن سمت کلاینت با فهرستی که خودش * فیلترشده آمده، دوباره‌کاری و منبع اختلاف است. */ export default function ResourcesPage() { const [urlState, setUrlState] = useUrlState({ search: '', type: '', skill: '', status: '', }); const { types } = useResourceTypes(); const { skills } = useSkills(); const { can } = usePermissions(); const canUpdate = can('resources', 'update'); const { resources, loading, create } = useResources({ type_uuid: urlState.type || undefined, skill_uuid: urlState.skill || undefined, active: urlState.status || undefined, }); // ساخت تنها کاری است که به منبعِ موجود گره نمی‌خورد، پس تنها مودالی است که می‌ماند. const [createOpen, setCreateOpen] = useState(false); const quota = useResourceQuota(); 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: '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 (
{!quota.unlimited && ( {formatNumber(quota.used)} از {formatNumber(quota.limit)} منبع پلن فعلی )} {quota.atLimit ? ( ارتقای پنل ) : ( )}
) : undefined } /> setUrlState({ search: v })} searchPlaceholder="جستجو در منابع..." emptyMessage="هیچ منبعی با این فیلترها یافت نشد" headerExtra={
({ 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={(r) => ( مدیریت )} /> setCreateOpen(false)} onSave={(payload) => create.mutate(payload, { onSuccess: () => setCreateOpen(false) })} /> ); }