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
+70 -64
View File
@@ -9,6 +9,8 @@ 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';
/**
* مهارت‌ها — «کدام اپراتور مجاز است با کدام دستگاه کار کند» یک اطلاعات است، نه یک
@@ -39,73 +41,77 @@ export default function SkillsPage() {
];
return (
<div className="fade-in">
<PageHeader
title="مهارت‌ها"
description="مهارت روی منابع می‌نشیند و در انتخاب منبع مناسب استفاده می‌شود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمی‌شود."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارت‌ها' }]}
action={
canUpdate ? (
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, skill: null })}>
<PlusIcon style={{ width: 16 }} /> افزودن مهارت
</button>
) : undefined
}
/>
<SettingsLayout active="resources">
<div className="fade-in">
<PageHeader
title="مهارت‌ها"
description="مهارت روی منابع می‌نشیند و در انتخاب منبع مناسب استفاده می‌شود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمی‌شود."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارت‌ها' }]}
action={
canUpdate ? (
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, skill: null })}>
<PlusIcon style={{ width: 16 }} /> افزودن مهارت
</button>
) : undefined
}
/>
<DataTable
columns={columns}
data={rows}
loading={loading}
searchValue={urlState.search}
onSearchChange={(v) => setUrlState({ search: v })}
searchPlaceholder="جستجو در مهارت‌ها..."
emptyMessage="هنوز مهارتی تعریف نشده است"
actions={
canUpdate
? (s) => (
<div style={{ display: 'flex', gap: 6 }}>
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, skill: s })}>
ویرایش
</button>
<button
type="button"
className="btn secondary sm"
disabled={(s.resources_count ?? 0) > 0}
title={(s.resources_count ?? 0) > 0 ? 'اول از منابع برداشته شود' : undefined}
onClick={() => setToDelete(s)}
>
حذف
</button>
</div>
)
: undefined
}
/>
<ResourcesSubNav />
<SkillModal
open={editing.open}
skill={editing.skill}
saving={create.isPending || update.isPending}
onClose={() => setEditing({ open: false, skill: null })}
onSave={({ name, active }) => {
const opts = { onSuccess: () => setEditing({ open: false, skill: null }) };
if (editing.skill) update.mutate({ uuid: editing.skill.uuid, d: { name, active } }, opts);
else create.mutate({ name }, opts);
}}
/>
<DataTable
columns={columns}
data={rows}
loading={loading}
searchValue={urlState.search}
onSearchChange={(v) => setUrlState({ search: v })}
searchPlaceholder="جستجو در مهارت‌ها..."
emptyMessage="هنوز مهارتی تعریف نشده است"
actions={
canUpdate
? (s) => (
<div style={{ display: 'flex', gap: 6 }}>
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, skill: s })}>
ویرایش
</button>
<button
type="button"
className="btn secondary sm"
disabled={(s.resources_count ?? 0) > 0}
title={(s.resources_count ?? 0) > 0 ? 'اول از منابع برداشته شود' : undefined}
onClick={() => setToDelete(s)}
>
حذف
</button>
</div>
)
: undefined
}
/>
<ConfirmDialog
open={!!toDelete}
title="حذف مهارت"
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
confirmLabel="حذف"
loading={remove.isPending}
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
onCancel={() => setToDelete(null)}
/>
</div>
<SkillModal
open={editing.open}
skill={editing.skill}
saving={create.isPending || update.isPending}
onClose={() => setEditing({ open: false, skill: null })}
onSave={({ name, active }) => {
const opts = { onSuccess: () => setEditing({ open: false, skill: null }) };
if (editing.skill) update.mutate({ uuid: editing.skill.uuid, d: { name, active } }, opts);
else create.mutate({ name }, opts);
}}
/>
<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>
);
}