feat: move "منابع" to main sidebar and update related components
This commit is contained in:
@@ -36,15 +36,22 @@ describe('SettingsLayout', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* سایدبار و فهرست موبایل یک منبع دارند؛ آیتمی که به منو اضافه میشود باید در هر دو
|
* سایدبار و فهرست موبایل یک منبع دارند؛ آیتمی که به منو اضافه میشود باید در هر دو
|
||||||
* دیده شود. قبلاً دو کپی بود و «منابع»/«دستهبندیها» فقط در موبایل ظاهر شدند.
|
* دیده شود. قبلاً دو کپی بود و «دستهبندیها» فقط در موبایل ظاهر شد.
|
||||||
*/
|
*/
|
||||||
it('shows the resource-first entries in the desktop sidebar too', () => {
|
it('shows the resource-first entries in the desktop sidebar too', () => {
|
||||||
useAuthStore.setState({ primaryRole: 'clinic' });
|
useAuthStore.setState({ primaryRole: 'clinic' });
|
||||||
renderWithProviders(<SettingsLayout active="resources"><div /></SettingsLayout>);
|
renderWithProviders(<SettingsLayout active="service-categories"><div /></SettingsLayout>);
|
||||||
|
|
||||||
expect(screen.getByText('منابع').closest('a')).toHaveAttribute('href', '/admin/resources');
|
|
||||||
expect(screen.getByText('دستهبندیها').closest('a')).toHaveAttribute('href', '/admin/service-categories');
|
expect(screen.getByText('دستهبندیها').closest('a')).toHaveAttribute('href', '/admin/service-categories');
|
||||||
expect(screen.getByText('منابع').closest('a')).toHaveAttribute('aria-current', 'page');
|
expect(screen.getByText('دستهبندیها').closest('a')).toHaveAttribute('aria-current', 'page');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** «منابع» کارِ روزمره است و به سایدبار اصلی رفت؛ نباید در منوی تنظیمات تکرار شود. */
|
||||||
|
it('does not list منابع in the settings menu any more', () => {
|
||||||
|
useAuthStore.setState({ primaryRole: 'clinic' });
|
||||||
|
renderWithProviders(<SettingsLayout active="service-categories"><div /></SettingsLayout>);
|
||||||
|
|
||||||
|
expect(screen.queryByText('منابع')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('role-gates the desktop sidebar: a doctor does not see the clinic-doctors tab', () => {
|
it('role-gates the desktop sidebar: a doctor does not see the clinic-doctors tab', () => {
|
||||||
|
|||||||
@@ -141,6 +141,22 @@ describe("Sidebar — گِیت منوی منشی بر اساس مجوز", () =>
|
|||||||
expect(screen.queryByText("تنظیمات نوبتدهی")).not.toBeInTheDocument();
|
expect(screen.queryByText("تنظیمات نوبتدهی")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("پزشکان کلینیک")).not.toBeInTheDocument();
|
expect(screen.queryByText("پزشکان کلینیک")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «منابع» از منوی تنظیمات به سایدبار اصلی منتقل شد. گِیتش عوض نشده — همان
|
||||||
|
* `appointment_settings.view` که در منوی تنظیمات داشت.
|
||||||
|
*/
|
||||||
|
it("«منابع» در سایدبار اصلی میآید و به مجوز appointment_settings گِیت است", () => {
|
||||||
|
setSecretary({ appointment_settings: { view: true } });
|
||||||
|
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
|
||||||
|
expect(screen.getByText("منابع").closest("a")).toHaveAttribute("href", "/admin/resources");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("منشیِ بدون مجوز، «منابع» را نمیبیند", () => {
|
||||||
|
setSecretary({ appointments: { view: true } });
|
||||||
|
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
|
||||||
|
expect(screen.queryByText("منابع")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Sidebar — گِیت منوی پزشکِ عضوِ کلینیک بر اساس مجوز", () => {
|
describe("Sidebar — گِیت منوی پزشکِ عضوِ کلینیک بر اساس مجوز", () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ClipboardDocumentCheckIcon,
|
ClipboardDocumentCheckIcon,
|
||||||
Cog6ToothIcon,
|
Cog6ToothIcon,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
|
CubeIcon,
|
||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
DevicePhoneMobileIcon,
|
DevicePhoneMobileIcon,
|
||||||
DocumentTextIcon,
|
DocumentTextIcon,
|
||||||
@@ -102,6 +103,9 @@ function buildSections(
|
|||||||
if (can("services", "view")) {
|
if (can("services", "view")) {
|
||||||
items.push({ to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها" });
|
items.push({ to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها" });
|
||||||
}
|
}
|
||||||
|
if (can("appointment_settings", "view")) {
|
||||||
|
items.push({ to: "/admin/resources", icon: CubeIcon, label: "منابع" });
|
||||||
|
}
|
||||||
if (can("inventory", "view")) {
|
if (can("inventory", "view")) {
|
||||||
items.push({ to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری" });
|
items.push({ to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری" });
|
||||||
}
|
}
|
||||||
@@ -306,6 +310,11 @@ function buildSections(
|
|||||||
icon: WrenchScrewdriverIcon,
|
icon: WrenchScrewdriverIcon,
|
||||||
label: "سرویس ها",
|
label: "سرویس ها",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: "/admin/resources",
|
||||||
|
icon: CubeIcon,
|
||||||
|
label: "منابع",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -381,6 +390,11 @@ function buildSections(
|
|||||||
icon: WrenchScrewdriverIcon,
|
icon: WrenchScrewdriverIcon,
|
||||||
label: "سرویس ها",
|
label: "سرویس ها",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: "/admin/resources",
|
||||||
|
icon: CubeIcon,
|
||||||
|
label: "منابع",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -448,6 +462,13 @@ function buildSections(
|
|||||||
label: "سرویس ها",
|
label: "سرویس ها",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (can("appointment_settings", "view")) {
|
||||||
|
items.push({
|
||||||
|
to: "/admin/resources",
|
||||||
|
icon: CubeIcon,
|
||||||
|
label: "منابع",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (can("inventory", "view")) {
|
if (can("inventory", "view")) {
|
||||||
items.push({
|
items.push({
|
||||||
to: "/admin/inventory",
|
to: "/admin/inventory",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
CreditCardIcon, UserIcon, CalendarDaysIcon, BuildingOffice2Icon,
|
CreditCardIcon, UserIcon, CalendarDaysIcon, BuildingOffice2Icon,
|
||||||
BanknotesIcon, UsersIcon, ShieldCheckIcon,
|
BanknotesIcon, UsersIcon, ShieldCheckIcon,
|
||||||
TagIcon, ChatBubbleLeftRightIcon, UserCircleIcon, UserPlusIcon, ReceiptPercentIcon,
|
TagIcon, ChatBubbleLeftRightIcon, UserCircleIcon, UserPlusIcon, ReceiptPercentIcon,
|
||||||
CubeIcon, RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +34,8 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [
|
|||||||
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] },
|
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] },
|
||||||
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] },
|
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] },
|
||||||
{ key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'], perm: ['clinic_doctors', 'view'] },
|
{ key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'], perm: ['clinic_doctors', 'view'] },
|
||||||
{ key: 'resources', label: 'منابع', icon: CubeIcon, to: '/admin/resources', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
|
// «منابع» به سایدبار اصلی («مدیریت») منتقل شد — کارِ روزمره است، نه تنظیمات.
|
||||||
|
// گِیتش آنجا همین است: `appointment_settings.view` در هر چهار نقشی که اینجا میدیدندش.
|
||||||
{ key: 'service-categories', label: 'دستهبندیها', icon: RectangleStackIcon, to: '/admin/service-categories', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
|
{ key: 'service-categories', label: 'دستهبندیها', icon: RectangleStackIcon, to: '/admin/service-categories', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
|
||||||
{ key: 'holidays', label: 'تعطیلات رسمی', icon: CalendarDaysIcon, to: '/admin/holidays', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
|
{ key: 'holidays', label: 'تعطیلات رسمی', icon: CalendarDaysIcon, to: '/admin/holidays', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
|
||||||
{ key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial', perm: ['payments', 'view'] },
|
{ key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial', perm: ['payments', 'view'] },
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsP
|
|||||||
import ResourceServicesPanel from '../components/resources/ResourceServicesPanel';
|
import ResourceServicesPanel from '../components/resources/ResourceServicesPanel';
|
||||||
import ResourceSkillsPanel from '../components/resources/ResourceSkillsPanel';
|
import ResourceSkillsPanel from '../components/resources/ResourceSkillsPanel';
|
||||||
import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesPanel';
|
import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesPanel';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
||||||
import { useUrlState } from '../hooks/useUrlState';
|
import { useUrlState } from '../hooks/useUrlState';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useAddresses } from '../hooks/useAddresses';
|
import { useAddresses } from '../hooks/useAddresses';
|
||||||
@@ -61,16 +60,13 @@ export default function ResourceDetailPage() {
|
|||||||
// یک لحظه میپرد و دوباره برمیگردد.
|
// یک لحظه میپرد و دوباره برمیگردد.
|
||||||
if (loading || !resource) {
|
if (loading || !resource) {
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
|
||||||
<div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>
|
<div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>
|
||||||
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
|
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={resource.name}
|
title={resource.name}
|
||||||
@@ -153,7 +149,6 @@ export default function ResourceDetailPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { usePermissions } from '../hooks/usePermissions';
|
|||||||
import { useAddresses } from '../hooks/useAddresses';
|
import { useAddresses } from '../hooks/useAddresses';
|
||||||
import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources';
|
import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources';
|
||||||
import type { ResourcePool } from '../types';
|
import type { ResourcePool } from '../types';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
||||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +66,6 @@ export default function ResourcePoolsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="استخر منابع"
|
title="استخر منابع"
|
||||||
@@ -145,7 +143,6 @@ export default function ResourcePoolsPage() {
|
|||||||
onCancel={() => setToDelete(null)}
|
onCancel={() => setToDelete(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
|
|||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useResourceTypes } from '../hooks/useResources';
|
import { useResourceTypes } from '../hooks/useResources';
|
||||||
import type { ResourceType } from '../types';
|
import type { ResourceType } from '../types';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
||||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||||
|
|
||||||
/** نوع منبع — کلینیک خودش تعریفش میکند؛ سه نوع سیستمی را backfill میسازد. */
|
/** نوع منبع — کلینیک خودش تعریفش میکند؛ سه نوع سیستمی را backfill میسازد. */
|
||||||
@@ -44,7 +43,6 @@ export default function ResourceTypesPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="نوع منابع"
|
title="نوع منابع"
|
||||||
@@ -114,7 +112,6 @@ export default function ResourceTypesPage() {
|
|||||||
onCancel={() => setToDelete(null)}
|
onCancel={() => setToDelete(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import ResourceSkillsModal from '../components/resources/ResourceSkillsModal';
|
|||||||
import ResourceServicesModal from '../components/resources/ResourceServicesModal';
|
import ResourceServicesModal from '../components/resources/ResourceServicesModal';
|
||||||
import { useAllServiceItems } from '../hooks/useServiceCatalog';
|
import { useAllServiceItems } from '../hooks/useServiceCatalog';
|
||||||
import type { ClinicResource } from '../types';
|
import type { ClinicResource } from '../types';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
||||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||||
|
|
||||||
/** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
|
/** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
|
||||||
@@ -114,164 +113,161 @@ export default function ResourcesPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
<div className="fade-in">
|
||||||
<div className="fade-in">
|
<PageHeader
|
||||||
<PageHeader
|
title="منابع"
|
||||||
title="منابع"
|
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
action={
|
||||||
backTo="/admin/settings-menu"
|
canUpdate ? (
|
||||||
action={
|
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
|
||||||
canUpdate ? (
|
<PlusIcon style={{ width: 16 }} /> افزودن منبع
|
||||||
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
|
</button>
|
||||||
<PlusIcon style={{ width: 16 }} /> افزودن منبع
|
) : undefined
|
||||||
</button>
|
}
|
||||||
) : undefined
|
/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourcesSubNav />
|
<ResourcesSubNav />
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
searchValue={urlState.search}
|
searchValue={urlState.search}
|
||||||
onSearchChange={(v) => setUrlState({ search: v })}
|
onSearchChange={(v) => setUrlState({ search: v })}
|
||||||
searchPlaceholder="جستجو در منابع..."
|
searchPlaceholder="جستجو در منابع..."
|
||||||
emptyMessage="هیچ منبعی با این فیلترها یافت نشد"
|
emptyMessage="هیچ منبعی با این فیلترها یافت نشد"
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginRight: 'auto' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginRight: 'auto' }}>
|
||||||
<div style={{ minWidth: 180 }}>
|
<div style={{ minWidth: 180 }}>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
|
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
|
||||||
value={urlState.address || null}
|
value={urlState.address || null}
|
||||||
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
|
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
|
||||||
placeholder="همهٔ شعبهها"
|
placeholder="همهٔ شعبهها"
|
||||||
isClearable
|
isClearable
|
||||||
height={36}
|
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>
|
||||||
}
|
<div style={{ minWidth: 170 }}>
|
||||||
actions={
|
<SearchableSelect
|
||||||
canUpdate
|
options={types.map((t) => ({ value: t.uuid, label: t.name }))}
|
||||||
? (r) => (
|
value={urlState.type || null}
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
onChange={(v) => setUrlState({ type: v ? String(v) : '' })}
|
||||||
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, resource: r })}>
|
placeholder="همهٔ نوعها"
|
||||||
ویرایش
|
isClearable
|
||||||
</button>
|
height={36}
|
||||||
<button type="button" className="btn secondary sm" onClick={() => setSkillsFor(r)}>
|
/>
|
||||||
مهارتها
|
</div>
|
||||||
</button>
|
<div style={{ minWidth: 170 }}>
|
||||||
<button type="button" className="btn secondary sm" onClick={() => setServicesFor(r)}>
|
<SearchableSelect
|
||||||
سرویسها
|
options={skills.map((s) => ({ value: s.uuid, label: s.name }))}
|
||||||
</button>
|
value={urlState.skill || null}
|
||||||
<Link className="btn secondary sm" to={`/admin/resources/${r.uuid}`}>
|
onChange={(v) => setUrlState({ skill: v ? String(v) : '' })}
|
||||||
تنظیمات
|
placeholder="همهٔ مهارتها"
|
||||||
</Link>
|
isClearable
|
||||||
{/* مسدودسازی موردی از تقویم جداست: آن الگوی کاری را عوض میکند،
|
height={36}
|
||||||
این یک بازهٔ مشخص را میبندد. */}
|
/>
|
||||||
<button type="button" className="btn secondary sm" onClick={() => setBlocksFor(r)}>
|
</div>
|
||||||
مسدودسازی
|
<div style={{ minWidth: 140 }}>
|
||||||
</button>
|
<SearchableSelect
|
||||||
<button type="button" className="btn secondary sm" onClick={() => setToDelete(r)}>
|
options={[
|
||||||
حذف
|
{ value: '1', label: 'فعال' },
|
||||||
</button>
|
{ value: '0', label: 'غیرفعال' },
|
||||||
</div>
|
]}
|
||||||
)
|
value={urlState.status || null}
|
||||||
: undefined
|
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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<ResourceBlocksModal
|
<ResourceBlocksModal
|
||||||
resourceUuid={blocksFor?.uuid ?? null}
|
resourceUuid={blocksFor?.uuid ?? null}
|
||||||
resourceName={blocksFor?.name ?? ''}
|
resourceName={blocksFor?.name ?? ''}
|
||||||
onClose={() => setBlocksFor(null)}
|
onClose={() => setBlocksFor(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResourceFormModal
|
<ResourceFormModal
|
||||||
open={editing.open}
|
open={editing.open}
|
||||||
resource={editing.resource}
|
resource={editing.resource}
|
||||||
addresses={addresses}
|
addresses={addresses}
|
||||||
types={types}
|
types={types}
|
||||||
saving={create.isPending || update.isPending}
|
saving={create.isPending || update.isPending}
|
||||||
onClose={() => setEditing({ open: false, resource: null })}
|
onClose={() => setEditing({ open: false, resource: null })}
|
||||||
onSave={(payload) => {
|
onSave={(payload) => {
|
||||||
const opts = { onSuccess: () => setEditing({ open: false, resource: null }) };
|
const opts = { onSuccess: () => setEditing({ open: false, resource: null }) };
|
||||||
if (editing.resource) update.mutate({ uuid: editing.resource.uuid, d: payload }, opts);
|
if (editing.resource) update.mutate({ uuid: editing.resource.uuid, d: payload }, opts);
|
||||||
else create.mutate(payload, opts);
|
else create.mutate(payload, opts);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResourceSkillsModal
|
<ResourceSkillsModal
|
||||||
resource={skillsFor}
|
resource={skillsFor}
|
||||||
skills={skills}
|
skills={skills}
|
||||||
saving={setSkills.isPending}
|
saving={setSkills.isPending}
|
||||||
onClose={() => setSkillsFor(null)}
|
onClose={() => setSkillsFor(null)}
|
||||||
onSave={(lines) =>
|
onSave={(lines) =>
|
||||||
skillsFor &&
|
skillsFor &&
|
||||||
setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) })
|
setSkills.mutate({ uuid: skillsFor.uuid, skills: lines }, { onSuccess: () => setSkillsFor(null) })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResourceServicesModal
|
<ResourceServicesModal
|
||||||
resource={servicesFor}
|
resource={servicesFor}
|
||||||
offerings={offerings}
|
offerings={offerings}
|
||||||
services={serviceOptions}
|
services={serviceOptions}
|
||||||
saving={saveServices.isPending}
|
saving={saveServices.isPending}
|
||||||
onClose={() => setServicesFor(null)}
|
onClose={() => setServicesFor(null)}
|
||||||
onSave={(lines) =>
|
onSave={(lines) =>
|
||||||
servicesFor &&
|
servicesFor &&
|
||||||
saveServices.mutate(
|
saveServices.mutate(
|
||||||
{ uuid: servicesFor.uuid, services: lines },
|
{ uuid: servicesFor.uuid, services: lines },
|
||||||
{ onSuccess: () => setServicesFor(null) },
|
{ onSuccess: () => setServicesFor(null) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={!!toDelete}
|
open={!!toDelete}
|
||||||
title="حذف منبع"
|
title="حذف منبع"
|
||||||
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
|
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
|
||||||
confirmLabel="حذف"
|
confirmLabel="حذف"
|
||||||
loading={remove.isPending}
|
loading={remove.isPending}
|
||||||
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
|
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
|
||||||
onCancel={() => setToDelete(null)}
|
onCancel={() => setToDelete(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
|
|||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useSkills } from '../hooks/useResources';
|
import { useSkills } from '../hooks/useResources';
|
||||||
import type { Skill } from '../types';
|
import type { Skill } from '../types';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
||||||
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +40,6 @@ export default function SkillsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout active="resources">
|
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مهارتها"
|
title="مهارتها"
|
||||||
@@ -111,7 +109,6 @@ export default function SkillsPage() {
|
|||||||
onCancel={() => setToDelete(null)}
|
onCancel={() => setToDelete(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user