feat: refactor resource management by removing modal components and integrating functionality into tabs; add ResourceBlocksPanel for temporary deactivation of resources

This commit is contained in:
hamed
2026-08-03 10:12:01 +03:30
parent b2b36e3eec
commit 00c275d618
10 changed files with 391 additions and 362 deletions
+18 -95
View File
@@ -3,18 +3,13 @@ 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 { useAddresses } from '../hooks/useAddresses';
import { useResourceServices, useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import { useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import ResourceFormModal from '../components/resources/ResourceFormModal';
import ResourceSkillsModal from '../components/resources/ResourceSkillsModal';
import ResourceServicesModal from '../components/resources/ResourceServicesModal';
import { useAllServiceItems } from '../hooks/useServiceCatalog';
import type { ClinicResource } from '../types';
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
@@ -43,21 +38,15 @@ export default function ResourcesPage() {
const { can } = usePermissions();
const canUpdate = can('appointment_settings', 'update');
const { resources, loading, create, update, remove, setSkills } = useResources({
const { resources, loading, create } = 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<ClinicResource | null>(null);
const [servicesFor, setServicesFor] = useState<ClinicResource | null>(null);
const [blocksFor, setBlocksFor] = useState<ClinicResource | null>(null);
const [toDelete, setToDelete] = useState<ClinicResource | null>(null);
const { offerings, save: saveServices } = useResourceServices(servicesFor?.uuid);
const { items: serviceOptions } = useAllServiceItems();
// ساخت تنها کاری است که به منبعِ موجود گره نمی‌خورد، پس تنها مودالی است که می‌ماند.
const [createOpen, setCreateOpen] = useState(false);
const rows = useMemo(() => {
const q = urlState.search.trim();
@@ -119,7 +108,7 @@ export default function ResourcesPage() {
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار هم‌زمان."
action={
canUpdate ? (
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
<button type="button" className="btn primary" onClick={() => setCreateOpen(true)}>
<PlusIcon style={{ width: 16 }} /> افزودن منبع
</button>
) : undefined
@@ -183,90 +172,24 @@ export default function ResourcesPage() {
</div>
</div>
}
actions={
canUpdate
? (r) => (
<div style={{ display: 'flex', gap: 6 }}>
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, resource: r })}>
ویرایش
</button>
<button type="button" className="btn secondary sm" onClick={() => setSkillsFor(r)}>
مهارتها
</button>
<button type="button" className="btn secondary sm" onClick={() => setServicesFor(r)}>
سرویسها
</button>
<Link className="btn secondary sm" to={`/admin/resources/${r.uuid}`}>
تنظیمات
</Link>
{/* مسدودسازی موردی از تقویم جداست: آن الگوی کاری را عوض می‌کند،
این یک بازهٔ مشخص را می‌بندد. */}
<button type="button" className="btn secondary sm" onClick={() => setBlocksFor(r)}>
مسدودسازی
</button>
<button type="button" className="btn secondary sm" onClick={() => setToDelete(r)}>
حذف
</button>
</div>
)
: undefined
}
/>
<ResourceBlocksModal
resourceUuid={blocksFor?.uuid ?? null}
resourceName={blocksFor?.name ?? ''}
onClose={() => setBlocksFor(null)}
/* یک اقدام، نه شش‌تا: ویرایش/مهارت‌ها/سرویس‌ها/مسدودسازی/حذف همگی تب‌های
خودِ منبع‌اند. شش دکمه در هر ردیف، جدول را از لبه بیرون می‌زد و همان کار
را دو جای مختلف (مودالِ ردیف و تبِ منبع) قابل انجام می‌کرد. */
actions={(r) => (
<Link className="btn secondary sm" to={`/admin/resources/${r.uuid}`}>
مدیریت
</Link>
)}
/>
<ResourceFormModal
open={editing.open}
resource={editing.resource}
open={createOpen}
resource={null}
addresses={addresses}
types={types}
saving={create.isPending || update.isPending}
onClose={() => 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);
}}
/>
<ResourceSkillsModal
resource={skillsFor}
skills={skills}
saving={setSkills.isPending}
onClose={() => setSkillsFor(null)}
onSave={(lines) =>
skillsFor &&
setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) })
}
/>
<ResourceServicesModal
resource={servicesFor}
offerings={offerings}
services={serviceOptions}
saving={saveServices.isPending}
onClose={() => setServicesFor(null)}
onSave={(lines) =>
servicesFor &&
saveServices.mutate(
{ uuid: servicesFor.uuid, services: lines },
{ onSuccess: () => setServicesFor(null) },
)
}
/>
<ConfirmDialog
open={!!toDelete}
title="حذف منبع"
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
confirmLabel="حذف"
loading={remove.isPending}
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
onCancel={() => setToDelete(null)}
saving={create.isPending}
onClose={() => setCreateOpen(false)}
onSave={(payload) => create.mutate(payload, { onSuccess: () => setCreateOpen(false) })}
/>
</div>
);