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 ───────────────────────────────────────────── */