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:
hamed
2026-08-02 13:34:49 +03:30
co-authored by Claude Opus 5
parent f11514950e
commit 4380e64a5d
13 changed files with 1010 additions and 919 deletions
+154 -148
View File
@@ -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>
);
}