fix(settings): every settings page renders the settings shell
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>
This commit is contained in:
@@ -16,6 +16,8 @@ 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` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
|
||||
const SUBJECT_LABEL: Record<string, string> = {
|
||||
@@ -112,160 +114,164 @@ export default function ResourcesPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<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
|
||||
}
|
||||
/>
|
||||
<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
|
||||
}
|
||||
/>
|
||||
|
||||
<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={branches.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
|
||||
value={urlState.address || null}
|
||||
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
|
||||
placeholder="همهٔ شعبهها"
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
<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={branches.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>
|
||||
</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>
|
||||
</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
|
||||
}
|
||||
/>
|
||||
}
|
||||
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}
|
||||
branches={branches}
|
||||
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}
|
||||
branches={branches}
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user