feat: move "منابع" to main sidebar and update related components
This commit is contained in:
@@ -9,7 +9,6 @@ import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsP
|
||||
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 { useAddresses } from '../hooks/useAddresses';
|
||||
@@ -61,16 +60,13 @@ export default function ResourceDetailPage() {
|
||||
// یک لحظه میپرد و دوباره برمیگردد.
|
||||
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}
|
||||
@@ -153,7 +149,6 @@ export default function ResourceDetailPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useAddresses } from '../hooks/useAddresses';
|
||||
import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources';
|
||||
import type { ResourcePool } from '../types';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||
|
||||
/**
|
||||
@@ -67,7 +66,6 @@ export default function ResourcePoolsPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLayout active="resources">
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="استخر منابع"
|
||||
@@ -145,7 +143,6 @@ export default function ResourcePoolsPage() {
|
||||
onCancel={() => setToDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useResourceTypes } from '../hooks/useResources';
|
||||
import type { ResourceType } from '../types';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||
|
||||
/** نوع منبع — کلینیک خودش تعریفش میکند؛ سه نوع سیستمی را backfill میسازد. */
|
||||
@@ -44,7 +43,6 @@ export default function ResourceTypesPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLayout active="resources">
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="نوع منابع"
|
||||
@@ -114,7 +112,6 @@ export default function ResourceTypesPage() {
|
||||
onCancel={() => setToDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import ResourceSkillsModal from '../components/resources/ResourceSkillsModal';
|
||||
import ResourceServicesModal from '../components/resources/ResourceServicesModal';
|
||||
import { useAllServiceItems } from '../hooks/useServiceCatalog';
|
||||
import type { ClinicResource } from '../types';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||
|
||||
/** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
|
||||
@@ -114,164 +113,161 @@ export default function ResourcesPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLayout active="resources">
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="منابع"
|
||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||
backTo="/admin/settings-menu"
|
||||
action={
|
||||
canUpdate ? (
|
||||
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
|
||||
<PlusIcon style={{ width: 16 }} /> افزودن منبع
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="منابع"
|
||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||
action={
|
||||
canUpdate ? (
|
||||
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
|
||||
<PlusIcon style={{ width: 16 }} /> افزودن منبع
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<ResourcesSubNav />
|
||||
<ResourcesSubNav />
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
searchValue={urlState.search}
|
||||
onSearchChange={(v) => setUrlState({ search: v })}
|
||||
searchPlaceholder="جستجو در منابع..."
|
||||
emptyMessage="هیچ منبعی با این فیلترها یافت نشد"
|
||||
headerExtra={
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginRight: 'auto' }}>
|
||||
<div style={{ minWidth: 180 }}>
|
||||
<SearchableSelect
|
||||
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
|
||||
value={urlState.address || null}
|
||||
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
|
||||
placeholder="همهٔ شعبهها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ minWidth: 170 }}>
|
||||
<SearchableSelect
|
||||
options={types.map((t) => ({ value: t.uuid, label: t.name }))}
|
||||
value={urlState.type || null}
|
||||
onChange={(v) => setUrlState({ type: v ? String(v) : '' })}
|
||||
placeholder="همهٔ نوعها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ minWidth: 170 }}>
|
||||
<SearchableSelect
|
||||
options={skills.map((s) => ({ value: s.uuid, label: s.name }))}
|
||||
value={urlState.skill || null}
|
||||
onChange={(v) => setUrlState({ skill: v ? String(v) : '' })}
|
||||
placeholder="همهٔ مهارتها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ minWidth: 140 }}>
|
||||
<SearchableSelect
|
||||
options={[
|
||||
{ value: '1', label: 'فعال' },
|
||||
{ value: '0', label: 'غیرفعال' },
|
||||
]}
|
||||
value={urlState.status || null}
|
||||
onChange={(v) => setUrlState({ status: v ? String(v) : '' })}
|
||||
placeholder="همهٔ وضعیتها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
searchValue={urlState.search}
|
||||
onSearchChange={(v) => setUrlState({ search: v })}
|
||||
searchPlaceholder="جستجو در منابع..."
|
||||
emptyMessage="هیچ منبعی با این فیلترها یافت نشد"
|
||||
headerExtra={
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginRight: 'auto' }}>
|
||||
<div style={{ minWidth: 180 }}>
|
||||
<SearchableSelect
|
||||
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
|
||||
value={urlState.address || null}
|
||||
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
|
||||
placeholder="همهٔ شعبهها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</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
|
||||
}
|
||||
/>
|
||||
<div style={{ minWidth: 170 }}>
|
||||
<SearchableSelect
|
||||
options={types.map((t) => ({ value: t.uuid, label: t.name }))}
|
||||
value={urlState.type || null}
|
||||
onChange={(v) => setUrlState({ type: v ? String(v) : '' })}
|
||||
placeholder="همهٔ نوعها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ minWidth: 170 }}>
|
||||
<SearchableSelect
|
||||
options={skills.map((s) => ({ value: s.uuid, label: s.name }))}
|
||||
value={urlState.skill || null}
|
||||
onChange={(v) => setUrlState({ skill: v ? String(v) : '' })}
|
||||
placeholder="همهٔ مهارتها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ minWidth: 140 }}>
|
||||
<SearchableSelect
|
||||
options={[
|
||||
{ value: '1', label: 'فعال' },
|
||||
{ value: '0', label: 'غیرفعال' },
|
||||
]}
|
||||
value={urlState.status || null}
|
||||
onChange={(v) => setUrlState({ status: v ? String(v) : '' })}
|
||||
placeholder="همهٔ وضعیتها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
<ResourceBlocksModal
|
||||
resourceUuid={blocksFor?.uuid ?? null}
|
||||
resourceName={blocksFor?.name ?? ''}
|
||||
onClose={() => setBlocksFor(null)}
|
||||
/>
|
||||
|
||||
<ResourceFormModal
|
||||
open={editing.open}
|
||||
resource={editing.resource}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
<ResourceFormModal
|
||||
open={editing.open}
|
||||
resource={editing.resource}
|
||||
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) })
|
||||
}
|
||||
/>
|
||||
<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) },
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
<ConfirmDialog
|
||||
open={!!toDelete}
|
||||
title="حذف منبع"
|
||||
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
|
||||
confirmLabel="حذف"
|
||||
loading={remove.isPending}
|
||||
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
|
||||
onCancel={() => setToDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useSkills } from '../hooks/useResources';
|
||||
import type { Skill } from '../types';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||
|
||||
/**
|
||||
@@ -41,7 +40,6 @@ export default function SkillsPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLayout active="resources">
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="مهارتها"
|
||||
@@ -111,7 +109,6 @@ export default function SkillsPage() {
|
||||
onCancel={() => setToDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user