Files
clinicpro/assets/admin/pages/ResourceDetailPage.tsx
T

204 lines
8.1 KiB
TypeScript

import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { PencilIcon } from '@heroicons/react/24/outline';
import PageHeader from '../components/ui/PageHeader';
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 { 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: 'دسته‌بندی‌ها' },
] as const;
type TabId = typeof TABS[number]['id'];
const SUBJECT_LABEL: Record<string, string> = {
doctor: 'پزشک',
staff: 'پرسنل',
room: 'اتاق',
};
/**
* یک منبع و همهٔ تنظیمات مستقلش، در یک صفحهٔ تب‌بندی‌شده — همان ساختار صفحهٔ
* «مدیریت نوبت‌دهی» کلینیک.
*
* منبع در مدل Resource-First واحدِ ظرفیت است، پس ساعت کاری و تعطیلات را خودش دارد نه
* فقط پزشکِ پشتش؛ تقویم منبع درون برنامهٔ هفتگی تنگ‌تر می‌شود، آن را گشاد نمی‌کند.
*/
export default function ResourceDetailPage() {
const { resourceUuid } = useParams<{ resourceUuid: string }>();
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, 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);
// حالت‌های بارگذاری و نبودِ منبع هم داخل پوستهٔ تنظیمات می‌مانند، وگرنه منوی کناری
// یک لحظه می‌پرد و دوباره برمی‌گردد.
if (loading || !resource) {
return (
<div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
</div>
);
}
return (
<div className="fade-in">
<PageHeader
title={resource.name}
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}${resource.address_name ? ` · ${resource.address_name}` : ''}`}
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: resource.name }]}
action={
canUpdate ? (
<button type="button" className="btn primary sm" onClick={() => setEditOpen(true)}>
<PencilIcon style={{ width: 15 }} /> ویرایش
</button>
) : undefined
}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 'var(--gap)' }}>
<ActiveBadge active={resource.active} />
</div>
<div className="seg" style={{ marginBottom: 'var(--gap)', overflowX: 'auto', flexWrap: 'nowrap' }}>
{TABS.map((t) => (
<button
key={t.id}
className={tab === t.id ? 'active' : ''}
style={{ whiteSpace: 'nowrap' }}
onClick={() => setUrlState({ tab: t.id })}
>
{t.label}
</button>
))}
</div>
{tab === 'info' && <InfoTab resource={resource} />}
{tab === 'hours' && <ResourceWorkingHoursPanel resourceUuid={resourceUuid} canUpdate={canUpdate} />}
{tab === 'exceptions' && <ResourceExceptionsPanel resourceUuid={resourceUuid} canUpdate={canUpdate} />}
{tab === 'services' && (
<div className="card card-pad">
<ResourceServicesPanel
resource={resource}
offerings={offerings}
services={serviceOptions}
saving={saveServices.isPending}
onSave={(lines) => saveServices.mutate({ uuid: resource.uuid, services: lines })}
/>
</div>
)}
{tab === 'skills' && (
<div className="card card-pad">
<ResourceSkillsPanel
resource={resource}
skills={skills}
saving={setSkills.isPending}
onSave={(lines) => setSkills.mutate({ uuid: resource.uuid, skills: lines })}
/>
</div>
)}
{tab === 'categories' && (
<ResourceCategoriesPanel
resource={resource}
canUpdate={canUpdate}
saving={setCategories.isPending}
onSave={(categoryUuids) => setCategories.mutate({ uuid: resource.uuid, categoryUuids })}
/>
)}
<ResourceFormModal
open={editOpen}
resource={resource}
addresses={addresses}
types={types}
saving={update.isPending}
onClose={() => setEditOpen(false)}
onSave={(payload) =>
update.mutate({ uuid: resource.uuid, d: payload }, { onSuccess: () => setEditOpen(false) })
}
/>
</div>
);
}
function Row({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
gap: 12, padding: '11px 0', borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>{label}</span>
<span style={{ fontSize: 13.5, color: 'var(--text-2)', textAlign: 'left', minWidth: 0 }}>{children}</span>
</div>
);
}
function InfoTab({ resource }: { resource: ClinicResource }) {
const attributes = Object.entries(resource.attributes ?? {});
return (
<div className="card card-pad">
<Row label="شعبه">{resource.address_name || '—'}</Row>
<Row label="نوع منبع">{resource.type_name}</Row>
<Row label="ظرفیت هم‌زمان">{resource.capacity} نفر</Row>
<Row label="آماده‌سازی / تمیزکاری">
{resource.setup_minutes} / {resource.cleanup_minutes} دقیقه
</Row>
<Row label="مهارت‌ها">
{resource.skills.length === 0 ? '—' : (
<span style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>
{resource.skills.map((s) => (
<span key={s.skill_uuid} className="badge blue" style={{ fontSize: 11 }}>
{s.skill_name} · {s.level}
</span>
))}
</span>
)}
</Row>
<Row label="دسته‌بندی‌ها">
{(resource.categories ?? []).length === 0 ? '—' : (
<span style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>
{(resource.categories ?? []).map((c) => (
<span key={c.uuid} className="badge gray" style={{ fontSize: 11 }}>{c.name}</span>
))}
</span>
)}
</Row>
{attributes.map(([key, value]) => (
<Row key={key} label={key}>{String(value)}</Row>
))}
</div>
);
}