Resources, branches, price lists, holidays and the new categories page sat in the settings menu but rendered bare, so clicking one made the settings sidebar disappear — the subscription page was the only one that kept it. Eleven pages now wrap in SettingsLayout with the key of the menu entry they belong to, and the four resource pages (list, types, skills, pools) share one menu entry plus a sub-nav between them, rather than four entries that would make the menu a third longer without making anything clearer. .seg accepts `a` as well as `button`, and treats `active` as an alias of `on`. Both were needed: cross-page tabs must be real links, and the pages already using `active` (service detail, clinic appointment settings) had no visible highlight at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
209 lines
8.3 KiB
TypeScript
209 lines
8.3 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 SettingsLayout from '../components/layout/SettingsLayout';
|
|
import { useUrlState } from '../hooks/useUrlState';
|
|
import { usePermissions } from '../hooks/usePermissions';
|
|
import { useBranches } from '../hooks/useBranches';
|
|
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 { branches } = useBranches();
|
|
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 (
|
|
<SettingsLayout active="resources">
|
|
<div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>
|
|
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
|
|
</div>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SettingsLayout active="resources">
|
|
<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}
|
|
branches={branches}
|
|
types={types}
|
|
saving={update.isPending}
|
|
onClose={() => setEditOpen(false)}
|
|
onSave={(payload) =>
|
|
update.mutate({ uuid: resource.uuid, d: payload }, { onSuccess: () => setEditOpen(false) })
|
|
}
|
|
/>
|
|
</div>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|