From 4380e64a5d18fbea05546e8b846c7c651b40d810 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 2 Aug 2026 13:34:49 +0330 Subject: [PATCH] fix(settings): every settings page renders the settings shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/resources/ResourcesSubNav.tsx | 33 ++ assets/admin/pages/BranchRoomsPage.tsx | 111 ++--- assets/admin/pages/BranchWorkingHoursPage.tsx | 219 ++++----- assets/admin/pages/BranchesPage.tsx | 117 ++--- assets/admin/pages/CatalogCategoriesPage.tsx | 147 +++--- assets/admin/pages/HolidaysSettingsPage.tsx | 125 ++--- assets/admin/pages/PriceListsPage.tsx | 433 +++++++++--------- assets/admin/pages/ResourceDetailPage.tsx | 16 +- assets/admin/pages/ResourcePoolsPage.tsx | 148 +++--- assets/admin/pages/ResourceTypesPage.tsx | 134 +++--- assets/admin/pages/ResourcesPage.tsx | 302 ++++++------ assets/admin/pages/SkillsPage.tsx | 134 +++--- assets/admin/styles.css | 10 +- 13 files changed, 1010 insertions(+), 919 deletions(-) create mode 100644 assets/admin/components/resources/ResourcesSubNav.tsx diff --git a/assets/admin/components/resources/ResourcesSubNav.tsx b/assets/admin/components/resources/ResourcesSubNav.tsx new file mode 100644 index 00000000..f521af09 --- /dev/null +++ b/assets/admin/components/resources/ResourcesSubNav.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; + +const LINKS = [ + { to: '/admin/resources', label: 'منابع' }, + { to: '/admin/resources/types', label: 'نوع منابع' }, + { to: '/admin/resources/skills', label: 'مهارت‌ها' }, + { to: '/admin/resources/pools', label: 'استخر منابع' }, +]; + +/** + * چهار صفحهٔ خانوادهٔ منابع، یک آیتم در منوی تنظیمات دارند؛ این نوار جابه‌جایی بینشان + * است. اگر هر چهار تا آیتم جدا در منو می‌گرفتند، منوی تنظیمات یک‌سوم بلندتر می‌شد بی‌آنکه + * چیزی روشن‌تر شود. + */ +export default function ResourcesSubNav() { + const { pathname } = useLocation(); + + return ( +
+ {LINKS.map((l) => ( + + {l.label} + + ))} +
+ ); +} diff --git a/assets/admin/pages/BranchRoomsPage.tsx b/assets/admin/pages/BranchRoomsPage.tsx index 17fc6a41..996ca463 100644 --- a/assets/admin/pages/BranchRoomsPage.tsx +++ b/assets/admin/pages/BranchRoomsPage.tsx @@ -10,6 +10,7 @@ import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useBranchRooms, useBranches } from '../hooks/useBranches'; import type { Room, RoomPayload } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; /** اتاق‌های یک شعبه. ظرفیت = چند بیمار هم‌زمان، نه چند اتاق. */ export default function BranchRoomsPage() { @@ -49,63 +50,65 @@ export default function BranchRoomsPage() { }; return ( -
- setEditing({ open: true, room: null })}> - افزودن اتاق - - ) : undefined - } - /> + +
+ setEditing({ open: true, room: null })}> + افزودن اتاق + + ) : undefined + } + /> - setUrlState({ search: v })} - searchPlaceholder="جستجو در اتاق‌ها..." - emptyMessage="هنوز اتاقی برای این شعبه ثبت نشده است" - actions={ - canUpdate - ? (r) => ( -
- - -
- ) - : undefined - } - /> + setUrlState({ search: v })} + searchPlaceholder="جستجو در اتاق‌ها..." + emptyMessage="هنوز اتاقی برای این شعبه ثبت نشده است" + actions={ + canUpdate + ? (r) => ( +
+ + +
+ ) + : undefined + } + /> - setEditing({ open: false, room: null })} - onSave={save} - /> + setEditing({ open: false, room: null })} + onSave={save} + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+ ); } diff --git a/assets/admin/pages/BranchWorkingHoursPage.tsx b/assets/admin/pages/BranchWorkingHoursPage.tsx index fbd443e5..cb20f840 100644 --- a/assets/admin/pages/BranchWorkingHoursPage.tsx +++ b/assets/admin/pages/BranchWorkingHoursPage.tsx @@ -6,6 +6,7 @@ import SearchableSelect from '../components/ui/SearchableSelect'; import { usePermissions } from '../hooks/usePermissions'; import { useBranchWorkingHours, useBranches } from '../hooks/useBranches'; import type { WorkingHourRange } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; /** ۰ = شنبه — همان قرارداد محاسبهٔ اسلات در بک‌اند. */ const DAY_LABELS = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; @@ -116,127 +117,129 @@ export default function BranchWorkingHoursPage() { const totalRanges = Object.values(draft).reduce((sum, ranges) => sum + ranges.length, 0); return ( -
- - {save.isPending ? 'در حال ذخیره...' : 'ذخیرهٔ هفته'} - - ) : undefined - } - /> + +
+ + {save.isPending ? 'در حال ذخیره...' : 'ذخیرهٔ هفته'} + + ) : undefined + } + /> - {error && ( -
- {error} -
- )} - - {branch && canUpdate && ( -
- منطقهٔ زمانی شعبه -
- + {error && ( +
+ {error}
- - {totalRanges === 0 ? 'هیچ بازه‌ای تعریف نشده' : `${totalRanges} بازه در هفته`} - -
- )} + )} - {loading ? ( -
در حال بارگذاری...
- ) : ( -
- {DAY_LABELS.map((label, day) => { - const ranges = draft[day] ?? []; - return ( -
-
-
- {label} - {ranges.length === 0 && ( - بسته + {branch && canUpdate && ( +
+ منطقهٔ زمانی شعبه +
+ +
+ + {totalRanges === 0 ? 'هیچ بازه‌ای تعریف نشده' : `${totalRanges} بازه در هفته`} + +
+ )} + + {loading ? ( +
در حال بارگذاری...
+ ) : ( +
+ {DAY_LABELS.map((label, day) => { + const ranges = draft[day] ?? []; + return ( +
+
+
+ {label} + {ranges.length === 0 && ( + بسته + )} +
+ {canUpdate && ( +
+ {ranges.length > 0 && ( + + )} + +
)}
- {canUpdate && ( -
- {ranges.length > 0 && ( - - )} - -
- )} -
-
- {ranges.map((range, index) => ( -
- - editRange(day, index, { start: e.target.value })} - style={{ width: 120 }} - /> - - {range.endOfDay ? ( - ۲۴:۰۰ - ) : ( +
+ {ranges.map((range, index) => ( +
+ editRange(day, index, { end: e.target.value })} + onChange={(e) => editRange(day, index, { start: e.target.value })} style={{ width: 120 }} /> - )} - - {canUpdate && ( - - )} -
- ))} + + {range.endOfDay ? ( + ۲۴:۰۰ + ) : ( + editRange(day, index, { end: e.target.value })} + style={{ width: 120 }} + /> + )} + + {canUpdate && ( + + )} +
+ ))} +
-
- ); - })} -
- )} -
+ ); + })} +
+ )} +
+ ); } diff --git a/assets/admin/pages/BranchesPage.tsx b/assets/admin/pages/BranchesPage.tsx index 5f263e3b..3c5449d5 100644 --- a/assets/admin/pages/BranchesPage.tsx +++ b/assets/admin/pages/BranchesPage.tsx @@ -8,6 +8,7 @@ import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useBranches } from '../hooks/useBranches'; import type { Branch } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; /** * شعبه‌ها — همان محل‌های نوبت‌دهی محیط جاری. @@ -91,65 +92,67 @@ export default function BranchesPage() { ]; return ( -
- + +
+ - setUrlState({ search: v })} - searchPlaceholder="جستجو در شعبه‌ها..." - emptyMessage="هیچ شعبه‌ای برای این محیط ثبت نشده است" - headerExtra={ -
- setUrlState({ status: v })} - /> -
- } - actions={(b) => ( -
- - ساعت کاری - - - اتاق‌ها - - {canUpdate && ( - - )} -
- )} - /> -
+ onClick={() => { + if ( + b.active && + activeCount === 1 && + !window.confirm('این تنها شعبهٔ فعال است. با غیرفعال کردن آن، هیچ شعبهٔ فعالی باقی نمی‌ماند. ادامه می‌دهید؟') + ) { + return; + } + update.mutate({ uuid: b.uuid, d: { active: !b.active } }); + }} + > + {b.active ? 'غیرفعال کردن' : 'فعال کردن'} + + )} +
+ )} + /> +
+ ); } diff --git a/assets/admin/pages/CatalogCategoriesPage.tsx b/assets/admin/pages/CatalogCategoriesPage.tsx index f50268b4..de970b8b 100644 --- a/assets/admin/pages/CatalogCategoriesPage.tsx +++ b/assets/admin/pages/CatalogCategoriesPage.tsx @@ -10,6 +10,7 @@ import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useCatalogCategories, useCategoryIncludes } from '../hooks/useCatalogCategories'; import type { CatalogCategory } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; /** یک ردیف از درخت، صاف‌شده — با عمق، تا تورفتگی نشان دهد کجای درخت است. */ type Row = CatalogCategory & { depth: number }; @@ -58,81 +59,83 @@ export default function CatalogCategoriesPage() { ]; return ( -
- setEditing({ open: true, category: null })}> - افزودن دسته‌بندی - - ) : undefined - } - /> - - - columns={columns} - data={rows} - loading={loading} - searchValue={urlState.search} - onSearchChange={(v) => setUrlState({ search: v })} - searchPlaceholder="جستجو در دسته‌بندی‌ها..." - emptyMessage="هنوز دسته‌بندی‌ای تعریف نشده است" - actions={ - canUpdate - ? (c) => ( -
- - - -
- ) - : undefined - } - /> - - setEditing({ open: false, category: null })} - onSave={(payload) => { - const done = { onSuccess: () => setEditing({ open: false, category: null }) }; - - if (editing.category) { - update.mutate({ uuid: editing.category.uuid, d: { name: payload.name, sort_order: payload.sort_order, active: payload.active } }, done); - } else { - create.mutate({ name: payload.name, parent_uuid: payload.parent_uuid }, done); + +
+ setEditing({ open: true, category: null })}> + افزودن دسته‌بندی + + ) : undefined } - }} - /> + /> - setIncludesFor(null)} - /> + + columns={columns} + data={rows} + loading={loading} + searchValue={urlState.search} + onSearchChange={(v) => setUrlState({ search: v })} + searchPlaceholder="جستجو در دسته‌بندی‌ها..." + emptyMessage="هنوز دسته‌بندی‌ای تعریف نشده است" + actions={ + canUpdate + ? (c) => ( +
+ + + +
+ ) + : undefined + } + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ setEditing({ open: false, category: null })} + onSave={(payload) => { + const done = { onSuccess: () => setEditing({ open: false, category: null }) }; + + if (editing.category) { + update.mutate({ uuid: editing.category.uuid, d: { name: payload.name, sort_order: payload.sort_order, active: payload.active } }, done); + } else { + create.mutate({ name: payload.name, parent_uuid: payload.parent_uuid }, done); + } + }} + /> + + setIncludesFor(null)} + /> + + toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+ ); } diff --git a/assets/admin/pages/HolidaysSettingsPage.tsx b/assets/admin/pages/HolidaysSettingsPage.tsx index 25e372ea..25287b12 100644 --- a/assets/admin/pages/HolidaysSettingsPage.tsx +++ b/assets/admin/pages/HolidaysSettingsPage.tsx @@ -7,6 +7,7 @@ import { usePermissions } from '../hooks/usePermissions'; import { useHolidays } from '../hooks/useResourceCalendar'; import { formatDate } from '../lib/utils'; import type { NationalHoliday } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; /** سالِ شمسی جاری از همان فرمت‌کنندهٔ شمسیِ مرورگر گرفته می‌شود، نه با محاسبهٔ دستی. */ function currentJalaliYear(): number { @@ -54,69 +55,71 @@ export default function HolidaysSettingsPage() { const years = Array.from({ length: 5 }, (_, i) => thisYear - 1 + i); return ( -
- + +
+ - - ({ value: String(y), label: String(y) }))} - value={String(year)} - onChange={(v) => setUrlState({ year: v ? String(v) : String(thisYear) })} - placeholder="سال" - height={36} - /> -
- } - actions={ - canUpdate - ? (h) => { - const override = overrideByDate.get(h.date); - return ( -
- {override?.is_working ? ( - - ) : ( - - )} -
- ); - } - : undefined - } - /> + + ({ value: String(y), label: String(y) }))} + value={String(year)} + onChange={(v) => setUrlState({ year: v ? String(v) : String(thisYear) })} + placeholder="سال" + height={36} + /> +
+ } + actions={ + canUpdate + ? (h) => { + const override = overrideByDate.get(h.date); + return ( +
+ {override?.is_working ? ( + + ) : ( + + )} +
+ ); + } + : undefined + } + /> - !o.is_working)} - canUpdate={canUpdate} - saving={setOverride.isPending} - onAdd={(date, note) => setOverride.mutate({ date, is_working: false, note })} - onRemove={(uuid) => removeOverride.mutate(uuid)} - /> -
+ !o.is_working)} + canUpdate={canUpdate} + saving={setOverride.isPending} + onAdd={(date, note) => setOverride.mutate({ date, is_working: false, note })} + onRemove={(uuid) => removeOverride.mutate(uuid)} + /> +
+ ); } diff --git a/assets/admin/pages/PriceListsPage.tsx b/assets/admin/pages/PriceListsPage.tsx index 432948b6..adfaaefd 100644 --- a/assets/admin/pages/PriceListsPage.tsx +++ b/assets/admin/pages/PriceListsPage.tsx @@ -12,6 +12,7 @@ import { usePermissions } from '../hooks/usePermissions'; import { useBranches } from '../hooks/useBranches'; import { usePriceLists, type PriceList, type PriceListItem } from '../hooks/usePriceLists'; import { useAllServiceItems } from '../hooks/useServiceCatalog'; +import SettingsLayout from '../components/layout/SettingsLayout'; interface Draft { uuid?: string; @@ -120,236 +121,238 @@ export default function PriceListsPage() { }; return ( -
- setDraft(emptyDraft())}> - لیست تازه - - ) : undefined - } - /> - -
- setUrlState({ search: v })} - searchPlaceholder="جستجو در لیست‌ها..." - emptyMessage="هنوز لیست قیمتی ساخته نشده است" - actions={(l) => + +
+ - - - {/* «کپی از لیست قبلی»: بیشتر لیست‌ها نسخهٔ کمی‌تغییریافتهٔ قبلی‌اند. */} - - - {!l.active && ( - - )} - - {!l.active && ( - - )} -
- ) : null + + ) : undefined } /> -
- - setDraft(null)} - footer={ - <> - - - - } - > - {draft && ( -
-
- - setDraft({ ...draft, name: e.target.value })} - placeholder="مثلاً: تعرفهٔ نیمهٔ دوم ۱۴۰۵" - /> -
- -
- - setDraft({ ...draft, address_uuid: v ? String(v) : null })} - options={[ - { value: '', label: 'همهٔ شعبه‌ها' }, - ...branches.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' })), - ]} - /> - - لیستِ یک شعبه بر لیست عمومی مقدم است و با آن تداخل حساب نمی‌شود. - -
- -
-
- - setDraft({ ...draft, valid_from: isoToUnix(iso) ?? draft.valid_from })} - /> -
-
- - setDraft({ ...draft, valid_to: isoToUnix(iso) ?? draft.valid_to })} - /> -
-
- - {draft.valid_to <= draft.valid_from && ( - - پایان بازه باید بعد از شروع آن باشد. - - )} - -
-

قیمت‌ها

- - {draft.items.map((item, index) => ( -
-
- - setDraft({ - ...draft, - items: draft.items.map((it, i) => - i === index ? { ...it, service_uuid: String(v ?? '') } : it, - ), - }) - } - options={services.map((s) => ({ value: s.uuid, label: s.name }))} - placeholder="خدمت" - /> -
- -
- - setDraft({ - ...draft, - items: draft.items.map((it, i) => (i === index ? { ...it, price_rials: v } : it)), - }) - } - suffix="ریال" - /> -
+
+ setUrlState({ search: v })} + searchPlaceholder="جستجو در لیست‌ها..." + emptyMessage="هنوز لیست قیمتی ساخته نشده است" + actions={(l) => + canManage ? ( +
-
- ))} + {/* «کپی از لیست قبلی»: بیشتر لیست‌ها نسخهٔ کمی‌تغییریافتهٔ قبلی‌اند. */} + + + {!l.active && ( + + )} + + {!l.active && ( + + )} +
+ ) : null + } + /> +
+ + setDraft(null)} + footer={ + <> -
+ + + } + > + {draft && ( +
+
+ + setDraft({ ...draft, name: e.target.value })} + placeholder="مثلاً: تعرفهٔ نیمهٔ دوم ۱۴۰۵" + /> +
- - خدمتی که در این لیست نیاید، قیمتش از تعرفهٔ سال یا خودِ خدمت خوانده می‌شود — - پس هیچ‌وقت بی‌قیمت نمی‌ماند. - -
- )} - -
+
+ + setDraft({ ...draft, address_uuid: v ? String(v) : null })} + options={[ + { value: '', label: 'همهٔ شعبه‌ها' }, + ...branches.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' })), + ]} + /> + + لیستِ یک شعبه بر لیست عمومی مقدم است و با آن تداخل حساب نمی‌شود. + +
+ +
+
+ + setDraft({ ...draft, valid_from: isoToUnix(iso) ?? draft.valid_from })} + /> +
+
+ + setDraft({ ...draft, valid_to: isoToUnix(iso) ?? draft.valid_to })} + /> +
+
+ + {draft.valid_to <= draft.valid_from && ( + + پایان بازه باید بعد از شروع آن باشد. + + )} + +
+

قیمت‌ها

+ + {draft.items.map((item, index) => ( +
+
+ + setDraft({ + ...draft, + items: draft.items.map((it, i) => + i === index ? { ...it, service_uuid: String(v ?? '') } : it, + ), + }) + } + options={services.map((s) => ({ value: s.uuid, label: s.name }))} + placeholder="خدمت" + /> +
+ +
+ + setDraft({ + ...draft, + items: draft.items.map((it, i) => (i === index ? { ...it, price_rials: v } : it)), + }) + } + suffix="ریال" + /> +
+ + +
+ ))} + + +
+ + + خدمتی که در این لیست نیاید، قیمتش از تعرفهٔ سال یا خودِ خدمت خوانده می‌شود — + پس هیچ‌وقت بی‌قیمت نمی‌ماند. + +
+ )} + + + ); } diff --git a/assets/admin/pages/ResourceDetailPage.tsx b/assets/admin/pages/ResourceDetailPage.tsx index 949176b3..a46c2275 100644 --- a/assets/admin/pages/ResourceDetailPage.tsx +++ b/assets/admin/pages/ResourceDetailPage.tsx @@ -9,6 +9,7 @@ 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 { useBranches } from '../hooks/useBranches'; @@ -56,10 +57,20 @@ export default function ResourceDetailPage() { const [editOpen, setEditOpen] = useState(false); - if (loading) return
در حال بارگذاری...
; - if (!resource) return
منبع یافت نشد.
; + // حالت‌های بارگذاری و نبودِ منبع هم داخل پوستهٔ تنظیمات می‌مانند، وگرنه منوی کناری + // یک لحظه می‌پرد و دوباره برمی‌گردد. + if (loading || !resource) { + return ( + +
+ {loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'} +
+
+ ); + } return ( +
+
); } diff --git a/assets/admin/pages/ResourcePoolsPage.tsx b/assets/admin/pages/ResourcePoolsPage.tsx index e04f9efc..16b98a45 100644 --- a/assets/admin/pages/ResourcePoolsPage.tsx +++ b/assets/admin/pages/ResourcePoolsPage.tsx @@ -11,6 +11,8 @@ import { usePermissions } from '../hooks/usePermissions'; import { useBranches } from '../hooks/useBranches'; import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources'; import type { ResourcePool } from '../types'; +import SettingsLayout from '../components/layout/SettingsLayout'; +import ResourcesSubNav from '../components/resources/ResourcesSubNav'; /** * استخر منابع — گروهی از منابع که جایگزین کامل یکدیگرند. @@ -65,81 +67,85 @@ export default function ResourcePoolsPage() { ]; return ( -
- setCreating(true)}> - افزودن استخر - - ) : undefined - } - /> + +
+ setCreating(true)}> + افزودن استخر + + ) : undefined + } + /> - setUrlState({ search: v })} - searchPlaceholder="جستجو در استخرها..." - emptyMessage="هنوز استخری تعریف نشده است" - actions={ - canUpdate - ? (p) => ( -
- - - -
- ) - : undefined - } - /> + - setCreating(false)} - onSave={(payload) => create.mutate(payload, { onSuccess: () => setCreating(false) })} - /> + setUrlState({ search: v })} + searchPlaceholder="جستجو در استخرها..." + emptyMessage="هنوز استخری تعریف نشده است" + actions={ + canUpdate + ? (p) => ( +
+ + + +
+ ) + : undefined + } + /> - setMembersFor(null)} - onSave={(members) => - membersFor && - setMembers.mutate({ uuid: membersFor.uuid, members }, { onSuccess: () => setMembersFor(null) }) - } - /> + setCreating(false)} + onSave={(payload) => create.mutate(payload, { onSuccess: () => setCreating(false) })} + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ setMembersFor(null)} + onSave={(members) => + membersFor && + setMembers.mutate({ uuid: membersFor.uuid, members }, { onSuccess: () => setMembersFor(null) }) + } + /> + + toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+ ); } diff --git a/assets/admin/pages/ResourceTypesPage.tsx b/assets/admin/pages/ResourceTypesPage.tsx index 49f3f9b8..bc281868 100644 --- a/assets/admin/pages/ResourceTypesPage.tsx +++ b/assets/admin/pages/ResourceTypesPage.tsx @@ -9,6 +9,8 @@ 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 می‌سازد. */ export default function ResourceTypesPage() { @@ -42,73 +44,77 @@ export default function ResourceTypesPage() { ]; return ( -
- setEditing({ open: true, type: null })}> - افزودن نوع - - ) : undefined - } - /> + +
+ setEditing({ open: true, type: null })}> + افزودن نوع + + ) : undefined + } + /> - setUrlState({ search: v })} - searchPlaceholder="جستجو در نوع منابع..." - emptyMessage="هنوز نوع منبعی تعریف نشده است" - actions={ - canUpdate - ? (t) => ( -
- - -
- ) - : undefined - } - /> + - setEditing({ open: false, type: null })} - onSave={(payload) => { - const opts = { onSuccess: () => setEditing({ open: false, type: null }) }; - if (editing.type) update.mutate({ uuid: editing.type.uuid, d: { name: payload.name, active: payload.active } }, opts); - else create.mutate({ code: payload.code, name: payload.name }, opts); - }} - /> + setUrlState({ search: v })} + searchPlaceholder="جستجو در نوع منابع..." + emptyMessage="هنوز نوع منبعی تعریف نشده است" + actions={ + canUpdate + ? (t) => ( +
+ + +
+ ) + : undefined + } + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ setEditing({ open: false, type: null })} + onSave={(payload) => { + const opts = { onSuccess: () => setEditing({ open: false, type: null }) }; + if (editing.type) update.mutate({ uuid: editing.type.uuid, d: { name: payload.name, active: payload.active } }, opts); + else create.mutate({ code: payload.code, name: payload.name }, opts); + }} + /> + + toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+ ); } diff --git a/assets/admin/pages/ResourcesPage.tsx b/assets/admin/pages/ResourcesPage.tsx index d323d421..6b9266a3 100644 --- a/assets/admin/pages/ResourcesPage.tsx +++ b/assets/admin/pages/ResourcesPage.tsx @@ -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 = { @@ -112,160 +114,164 @@ export default function ResourcesPage() { ]; return ( -
- setEditing({ open: true, resource: null })}> - افزودن منبع - - ) : undefined - } - /> + +
+ setEditing({ open: true, resource: null })}> + افزودن منبع + + ) : undefined + } + /> - setUrlState({ search: v })} - searchPlaceholder="جستجو در منابع..." - emptyMessage="هیچ منبعی با این فیلترها یافت نشد" - headerExtra={ -
-
- ({ value: b.uuid, label: b.name || 'بدون نام' }))} - value={urlState.address || null} - onChange={(v) => setUrlState({ address: v ? String(v) : '' })} - placeholder="همهٔ شعبه‌ها" - isClearable - height={36} - /> + + + setUrlState({ search: v })} + searchPlaceholder="جستجو در منابع..." + emptyMessage="هیچ منبعی با این فیلترها یافت نشد" + headerExtra={ +
+
+ ({ value: b.uuid, label: b.name || 'بدون نام' }))} + value={urlState.address || null} + onChange={(v) => setUrlState({ address: v ? String(v) : '' })} + placeholder="همهٔ شعبه‌ها" + isClearable + height={36} + /> +
+
+ ({ value: t.uuid, label: t.name }))} + value={urlState.type || null} + onChange={(v) => setUrlState({ type: v ? String(v) : '' })} + placeholder="همهٔ نوع‌ها" + isClearable + height={36} + /> +
+
+ ({ value: s.uuid, label: s.name }))} + value={urlState.skill || null} + onChange={(v) => setUrlState({ skill: v ? String(v) : '' })} + placeholder="همهٔ مهارت‌ها" + isClearable + height={36} + /> +
+
+ setUrlState({ status: v ? String(v) : '' })} + placeholder="همهٔ وضعیت‌ها" + isClearable + height={36} + /> +
-
- ({ value: t.uuid, label: t.name }))} - value={urlState.type || null} - onChange={(v) => setUrlState({ type: v ? String(v) : '' })} - placeholder="همهٔ نوع‌ها" - isClearable - height={36} - /> -
-
- ({ value: s.uuid, label: s.name }))} - value={urlState.skill || null} - onChange={(v) => setUrlState({ skill: v ? String(v) : '' })} - placeholder="همهٔ مهارت‌ها" - isClearable - height={36} - /> -
-
- setUrlState({ status: v ? String(v) : '' })} - placeholder="همهٔ وضعیت‌ها" - isClearable - height={36} - /> -
-
- } - actions={ - canUpdate - ? (r) => ( -
- - - - - تنظیمات - - {/* مسدودسازی موردی از تقویم جداست: آن الگوی کاری را عوض می‌کند، - این یک بازهٔ مشخص را می‌بندد. */} - - -
- ) - : undefined - } - /> + } + actions={ + canUpdate + ? (r) => ( +
+ + + + + تنظیمات + + {/* مسدودسازی موردی از تقویم جداست: آن الگوی کاری را عوض می‌کند، + این یک بازهٔ مشخص را می‌بندد. */} + + +
+ ) + : undefined + } + /> - setBlocksFor(null)} - /> + setBlocksFor(null)} + /> - 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); - }} - /> + 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); + }} + /> - setSkillsFor(null)} - onSave={(lines) => - skillsFor && - setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) }) - } - /> + setSkillsFor(null)} + onSave={(lines) => + skillsFor && + setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) }) + } + /> - setServicesFor(null)} - onSave={(lines) => - servicesFor && - saveServices.mutate( - { uuid: servicesFor.uuid, services: lines }, - { onSuccess: () => setServicesFor(null) }, - ) - } - /> + setServicesFor(null)} + onSave={(lines) => + servicesFor && + saveServices.mutate( + { uuid: servicesFor.uuid, services: lines }, + { onSuccess: () => setServicesFor(null) }, + ) + } + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+
); } diff --git a/assets/admin/pages/SkillsPage.tsx b/assets/admin/pages/SkillsPage.tsx index 47494017..afa7b7a8 100644 --- a/assets/admin/pages/SkillsPage.tsx +++ b/assets/admin/pages/SkillsPage.tsx @@ -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 ( -
- setEditing({ open: true, skill: null })}> - افزودن مهارت - - ) : undefined - } - /> + +
+ setEditing({ open: true, skill: null })}> + افزودن مهارت + + ) : undefined + } + /> - setUrlState({ search: v })} - searchPlaceholder="جستجو در مهارت‌ها..." - emptyMessage="هنوز مهارتی تعریف نشده است" - actions={ - canUpdate - ? (s) => ( -
- - -
- ) - : undefined - } - /> + - 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); - }} - /> + setUrlState({ search: v })} + searchPlaceholder="جستجو در مهارت‌ها..." + emptyMessage="هنوز مهارتی تعریف نشده است" + actions={ + canUpdate + ? (s) => ( +
+ + +
+ ) + : undefined + } + /> - toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} - onCancel={() => setToDelete(null)} - /> -
+ 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); + }} + /> + + toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} + onCancel={() => setToDelete(null)} + /> +
+ ); } diff --git a/assets/admin/styles.css b/assets/admin/styles.css index fc14afe7..7bc7cf84 100644 --- a/assets/admin/styles.css +++ b/assets/admin/styles.css @@ -651,11 +651,15 @@ table.t tbody tr:last-child td { border-bottom: none; } .field:focus-within { border-color: var(--primary); box-shadow: 0 0 0 4px var(--ring); } .field input, .field select, .field textarea { border: none; outline: none; background: none; flex: 1; min-width: 60px; color: var(--text); font-family: inherit; font-size: 14px; } .seg { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 3px; gap: 2px; } -.seg button { padding: 7px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--text-2); transition: .14s; cursor: pointer; font-family: inherit; } -.seg button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } +/* `a` هم پذیرفته می‌شود: نوار تب‌هایی که بین صفحه‌ها جابه‌جا می‌کنند باید لینک واقعی باشند + (باز کردن در تب جدید، کلیک وسط)، نه دکمه‌ای که navigate صدا می‌زند. */ +.seg button, .seg a { padding: 7px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--text-2); transition: .14s; cursor: pointer; font-family: inherit; text-decoration: none; display: inline-flex; align-items: center; } +/* `active` هم‌معنی `on` است — هر دو در کد استفاده شده‌اند و نبودِ این alias یعنی تب فعال + در چند صفحه هیچ نشانه‌ای نداشت. */ +.seg button.on, .seg button.active, .seg a.on, .seg a.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } /* هم‌راستا با .mini-btn: ارتفاع لمسیِ حداقلی روی موبایل (WCAG 2.5.5) */ @media (max-width: 640px), (pointer: coarse) { - .seg button { min-height: 44px; padding: 7px 16px; } + .seg button, .seg a { min-height: 44px; padding: 7px 16px; } } /* ── Switch toggle ───────────────────────────────────────────── */