import React, { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { PencilIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import { ActiveBadge } from '../components/ui/StatusBadge'; import ResourceFormModal from '../components/resources/ResourceFormModal'; import ResourceWorkingHoursPanel from '../components/resources/ResourceWorkingHoursPanel'; import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsPanel'; import ResourceServicesPanel from '../components/resources/ResourceServicesPanel'; import ResourceSkillsPanel from '../components/resources/ResourceSkillsPanel'; import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesPanel'; import ResourceBlocksPanel from '../components/resources/ResourceBlocksPanel'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useAddresses } from '../hooks/useAddresses'; import { useResourceDetail, useResourceServices, useResources, useResourceTypes, useSkills } from '../hooks/useResources'; import { useAllServiceItems } from '../hooks/useServiceCatalog'; import type { ClinicResource } from '../types'; const TABS = [ { id: 'info', label: 'اطلاعات' }, { id: 'hours', label: 'ساعات کاری' }, { id: 'exceptions', label: 'تعطیلات و استثنا' }, { id: 'services', label: 'سرویس‌ها' }, { id: 'skills', label: 'مهارت‌ها' }, { id: 'categories', label: 'دسته‌بندی‌ها' }, { id: 'blocks', label: 'غیرفعال‌سازی موقت' }, ] as const; type TabId = typeof TABS[number]['id']; const SUBJECT_LABEL: Record = { doctor: 'پزشک', staff: 'پرسنل', room: 'اتاق', }; /** * یک منبع و همهٔ تنظیمات مستقلش، در یک صفحهٔ تب‌بندی‌شده — همان ساختار صفحهٔ * «مدیریت نوبت‌دهی» کلینیک. * * منبع در مدل Resource-First واحدِ ظرفیت است، پس ساعت کاری و تعطیلات را خودش دارد نه * فقط پزشکِ پشتش؛ تقویم منبع درون برنامهٔ هفتگی تنگ‌تر می‌شود، آن را گشاد نمی‌کند. */ export default function ResourceDetailPage() { const { resourceUuid } = useParams<{ resourceUuid: string }>(); const navigate = useNavigate(); const [urlState, setUrlState] = useUrlState({ tab: 'info' }); const tab = (TABS.some((t) => t.id === urlState.tab) ? urlState.tab : 'info') as TabId; const { resource, loading } = useResourceDetail(resourceUuid); const { addresses } = useAddresses(); const { types } = useResourceTypes(); const { skills } = useSkills(); const { update, remove, setSkills, setCategories } = useResources(); const { offerings, save: saveServices } = useResourceServices(resourceUuid); const { items: serviceOptions } = useAllServiceItems(); const { can } = usePermissions(); const canUpdate = can('appointment_settings', 'update'); const [editOpen, setEditOpen] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); // حالت‌های بارگذاری و نبودِ منبع هم داخل پوستهٔ تنظیمات می‌مانند، وگرنه منوی کناری // یک لحظه می‌پرد و دوباره برمی‌گردد. if (loading || !resource) { return (
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
); } return (
setEditOpen(true)}> ویرایش ) : undefined } />
{TABS.map((t) => ( ))}
{tab === 'info' && ( update.mutate({ uuid: resource.uuid, d: { active: !resource.active } })} onDelete={() => setConfirmDelete(true)} /> )} {tab === 'hours' && } {tab === 'exceptions' && } {tab === 'services' && (
saveServices.mutate({ uuid: resource.uuid, services: lines })} />
)} {tab === 'skills' && (
setSkills.mutate({ uuid: resource.uuid, skills: lines })} />
)} {tab === 'categories' && ( setCategories.mutate({ uuid: resource.uuid, categoryUuids })} /> )} {tab === 'blocks' && } {/* حذف موفق یعنی این صفحه دیگر منبعی ندارد؛ ماندن روی آن یک «منبع یافت نشد» است. */} remove.mutate(resource.uuid, { onSuccess: () => { setConfirmDelete(false); navigate('/admin/resources'); }, }) } onCancel={() => setConfirmDelete(false)} /> setEditOpen(false)} onSave={(payload) => update.mutate({ uuid: resource.uuid, d: payload }, { onSuccess: () => setEditOpen(false) }) } />
); } function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label} {children}
); } /** * اطلاعات منبع + دو اقدامِ سطح-منبع. * * فعال/غیرفعال و حذف پیش‌تر دکمه‌های ردیفِ جدول بودند. جدول حالا فقط فهرست است و * هر اقدام کنارِ همان چیزی نشسته که تغییرش می‌دهد؛ حذف هم پایین و جدا از بقیه است * تا با «ویرایش» و «غیرفعال» یک‌جا هم‌وزن دیده نشود. */ function InfoTab({ resource, canUpdate, toggling, onToggleActive, onDelete }: { resource: ClinicResource; canUpdate: boolean; toggling: boolean; onToggleActive: () => void; onDelete: () => void; }) { const attributes = Object.entries(resource.attributes ?? {}); return (
{resource.address_name || '—'} {resource.type_name} {resource.capacity} نفر {resource.setup_minutes} / {resource.cleanup_minutes} دقیقه {resource.skills.length === 0 ? '—' : ( {resource.skills.map((s) => ( {s.skill_name} · {s.level} ))} )} {(resource.categories ?? []).length === 0 ? '—' : ( {(resource.categories ?? []).map((c) => ( {c.name} ))} )} {attributes.map(([key, value]) => ( {String(value)} ))}
{canUpdate && (

وضعیت منبع

منبع غیرفعال در نوبت‌دهی پیشنهاد نمی‌شود. برای بستن فقط یک بازهٔ مشخص، از تب «غیرفعال‌سازی موقت» استفاده کنید.

{resource.active ? 'این منبع فعال است' : 'این منبع غیرفعال است'}
{resource.active ? 'در جستجوی وقت و رزرو نوبت در دسترس است.' : 'در جستجوی وقت و رزرو نوبت ظاهر نمی‌شود.'}
{/* پس‌زمینه عمداً خنثی می‌ماند: `.btn.danger` خودش `--danger-bg` است و روی ردیفِ هم‌رنگ نامرئی می‌شد. مرزِ قرمز به‌تنهایی هشدار را می‌رساند. */}
حذف منبع
برگشت‌پذیر نیست. اگر فقط موقتاً لازمش ندارید، غیرفعالش کنید.
)}
); }