feat(resource): remove ResourceCategoriesPanel and related functionality from ResourceDetailPage
This commit is contained in:
@@ -1,94 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import SearchableSelect from '../ui/SearchableSelect';
|
|
||||||
import { useCatalogCategories } from '../../hooks/useCatalogCategories';
|
|
||||||
import type { CatalogCategory, ClinicResource } from '../../types';
|
|
||||||
|
|
||||||
type Flat = { uuid: string; name: string; depth: number };
|
|
||||||
|
|
||||||
function flatten(nodes: CatalogCategory[], depth = 0): Flat[] {
|
|
||||||
return nodes.flatMap((n) => [
|
|
||||||
{ uuid: n.uuid, name: n.name, depth },
|
|
||||||
...flatten(n.children ?? [], depth + 1),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* دستهبندی یک منبع — فقط **انتخاب** از کاتالوگ سراسری.
|
|
||||||
*
|
|
||||||
* ساختن دسته اینجا عمداً ممکن نیست؛ تنها جای ساخت «تنظیمات ← دستهبندیها» است، وگرنه
|
|
||||||
* هر کاربر نسخهٔ خودش از «تمام بدن» را میسازد.
|
|
||||||
*/
|
|
||||||
export default function ResourceCategoriesPanel({ resource, canUpdate, saving, onSave }: {
|
|
||||||
resource: ClinicResource | null;
|
|
||||||
canUpdate: boolean;
|
|
||||||
saving: boolean;
|
|
||||||
onSave: (categoryUuids: string[]) => void;
|
|
||||||
}) {
|
|
||||||
const { tree, loading } = useCatalogCategories();
|
|
||||||
const [chosen, setChosen] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setChosen((resource?.categories ?? []).map((c) => c.uuid));
|
|
||||||
}, [resource]);
|
|
||||||
|
|
||||||
const all = useMemo(() => flatten(tree), [tree]);
|
|
||||||
const picked = new Set(chosen);
|
|
||||||
const available = all.filter((c) => !picked.has(c.uuid));
|
|
||||||
const nameOf = (uuid: string) => all.find((c) => c.uuid === uuid)?.name ?? uuid;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card card-pad" style={{ display: 'grid', gap: 14 }}>
|
|
||||||
<p style={{ margin: 0, fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.9 }}>
|
|
||||||
دستهبندی سراسری است و اینجا فقط انتخاب میشود. برای ساخت یا ویرایش به{' '}
|
|
||||||
<Link to="/admin/service-categories" style={{ color: 'var(--primary)' }}>تنظیمات ← دستهبندیها</Link>{' '}
|
|
||||||
بروید.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>در حال بارگذاری...</span>
|
|
||||||
) : chosen.length === 0 ? (
|
|
||||||
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0 }}>این منبع هیچ دستهبندیای ندارد.</p>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
||||||
{chosen.map((uuid) => (
|
|
||||||
<span key={uuid} className="badge blue" style={{ fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
{nameOf(uuid)}
|
|
||||||
{canUpdate && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setChosen((c) => c.filter((x) => x !== uuid))}
|
|
||||||
aria-label={`حذف ${nameOf(uuid)}`}
|
|
||||||
style={{ background: 'none', border: 0, cursor: 'pointer', color: 'inherit', padding: 0, lineHeight: 1 }}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canUpdate && available.length > 0 && (
|
|
||||||
<div style={{ display: 'grid', gap: 6, maxWidth: 380 }}>
|
|
||||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>افزودن دستهبندی</label>
|
|
||||||
<SearchableSelect
|
|
||||||
options={available.map((c) => ({ value: c.uuid, label: '— '.repeat(c.depth) + c.name }))}
|
|
||||||
value={null}
|
|
||||||
onChange={(v) => v && setChosen((c) => [...c, String(v)])}
|
|
||||||
placeholder="یک دستهبندی انتخاب کنید"
|
|
||||||
height={38}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canUpdate && (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<button type="button" className="btn primary" disabled={saving} onClick={() => onSave(chosen)}>
|
|
||||||
{saving ? 'در حال ذخیره...' : 'ذخیره'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -85,18 +85,10 @@ export function useResources(filters: ResourceFilters = {}) {
|
|||||||
onError: (e) => fail(e, 'ذخیرهٔ مهارتها ناموفق بود'),
|
onError: (e) => fail(e, 'ذخیرهٔ مهارتها ناموفق بود'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** دستهٔ منبع از کاتالوگ سراسری میآید؛ ساختنش فقط در «تنظیمات ← دستهبندیها» ممکن است. */
|
|
||||||
const setCategories = useMutation({
|
|
||||||
mutationFn: ({ uuid, categoryUuids }: { uuid: string; categoryUuids: string[] }) =>
|
|
||||||
api.put<ApiResponse<ClinicResource>>(`/api/v1/resource/${uuid}/categories`, { category_uuids: categoryUuids }),
|
|
||||||
onSuccess: () => { toast.success('دستهبندیهای منبع ذخیره شد'); invalidate(); },
|
|
||||||
onError: (e) => fail(e, 'ذخیرهٔ دستهبندیها ناموفق بود'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: query.data?.data ?? [],
|
resources: query.data?.data ?? [],
|
||||||
loading: query.isLoading,
|
loading: query.isLoading,
|
||||||
create, update, remove, setSkills, setCategories,
|
create, update, remove, setSkills,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,17 +188,18 @@ describe('ResourceDetailPage', () => {
|
|||||||
expect(screen.queryByRole('button', { name: /کپی/ })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /کپی/ })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('تب دستهبندی فقط انتخاب میدهد، نه ساخت', async () => {
|
/**
|
||||||
|
* دستهبندی از این صفحه برداشته شد: چیزی که کاربر مینوشت هیچجا خوانده نمیشد و
|
||||||
|
* «کدام منبع این سرویس را میدهد» را تب سرویسها صریحتر جواب میدهد.
|
||||||
|
*/
|
||||||
|
it('تب دستهبندی وجود ندارد و لینک قدیمی به تب اطلاعات میافتد', async () => {
|
||||||
mockApi();
|
mockApi();
|
||||||
const user = userEvent.setup();
|
renderPage('/admin/resources/r1?tab=categories');
|
||||||
renderPage();
|
|
||||||
|
|
||||||
|
// نبودِ تب وقتی معنا دارد که صفحه واقعاً رندر شده باشد.
|
||||||
await waitFor(() => expect(screen.getByText('ظرفیت همزمان')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('ظرفیت همزمان')).toBeInTheDocument());
|
||||||
await user.click(screen.getByRole('button', { name: 'دستهبندیها' }));
|
expect(screen.queryByRole('button', { name: 'دستهبندیها' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('دست')).not.toBeInTheDocument();
|
||||||
await waitFor(() => expect(screen.getByText(/فقط انتخاب میشود/)).toBeInTheDocument());
|
|
||||||
expect(screen.queryByRole('button', { name: /افزودن دستهبندی جدید/ })).not.toBeInTheDocument();
|
|
||||||
expect(screen.getByText('تنظیمات ← دستهبندیها')).toHaveAttribute('href', '/admin/service-categories');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** تب استثناها فقط دو ورودی دارد: تعطیلات رسمی، و مرخصی و سرویس. */
|
/** تب استثناها فقط دو ورودی دارد: تعطیلات رسمی، و مرخصی و سرویس. */
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import ResourceWorkingHoursPanel from '../components/resources/ResourceWorkingHo
|
|||||||
import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsPanel';
|
import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsPanel';
|
||||||
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 ResourceBlocksPanel from '../components/resources/ResourceBlocksPanel';
|
import ResourceBlocksPanel from '../components/resources/ResourceBlocksPanel';
|
||||||
import { useUrlState } from '../hooks/useUrlState';
|
import { useUrlState } from '../hooks/useUrlState';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
@@ -23,7 +22,6 @@ const TABS = [
|
|||||||
{ id: 'exceptions', label: 'تعطیلات و استثنا' },
|
{ id: 'exceptions', label: 'تعطیلات و استثنا' },
|
||||||
{ id: 'services', label: 'سرویسها' },
|
{ id: 'services', label: 'سرویسها' },
|
||||||
{ id: 'skills', label: 'مهارتها' },
|
{ id: 'skills', label: 'مهارتها' },
|
||||||
{ id: 'categories', label: 'دستهبندیها' },
|
|
||||||
{ id: 'blocks', label: 'غیرفعالسازی موقت' },
|
{ id: 'blocks', label: 'غیرفعالسازی موقت' },
|
||||||
] as const;
|
] as const;
|
||||||
type TabId = typeof TABS[number]['id'];
|
type TabId = typeof TABS[number]['id'];
|
||||||
@@ -49,7 +47,7 @@ export default function ResourceDetailPage() {
|
|||||||
const { resource, loading } = useResourceDetail(resourceUuid);
|
const { resource, loading } = useResourceDetail(resourceUuid);
|
||||||
const { types } = useResourceTypes();
|
const { types } = useResourceTypes();
|
||||||
const { skills } = useSkills();
|
const { skills } = useSkills();
|
||||||
const { update, setSkills, setCategories } = useResources();
|
const { update, setSkills } = useResources();
|
||||||
const { offerings, save: saveServices } = useResourceServices(resourceUuid);
|
const { offerings, save: saveServices } = useResourceServices(resourceUuid);
|
||||||
const { items: serviceOptions } = useAllServiceItems();
|
const { items: serviceOptions } = useAllServiceItems();
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
@@ -143,15 +141,6 @@ export default function ResourceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'categories' && (
|
|
||||||
<ResourceCategoriesPanel
|
|
||||||
resource={resource}
|
|
||||||
canUpdate={canUpdate}
|
|
||||||
saving={setCategories.isPending}
|
|
||||||
onSave={(categoryUuids) => setCategories.mutate({ uuid: resource.uuid, categoryUuids })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'blocks' && <ResourceBlocksPanel resourceUuid={resourceUuid} canUpdate={canUpdate} />}
|
{tab === 'blocks' && <ResourceBlocksPanel resourceUuid={resourceUuid} canUpdate={canUpdate} />}
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -229,15 +218,6 @@ function InfoTab({ resource, canUpdate, toggling, onToggleActive }: {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="دستهبندیها">
|
|
||||||
{(resource.categories ?? []).length === 0 ? '—' : (
|
|
||||||
<span style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>
|
|
||||||
{(resource.categories ?? []).map((c) => (
|
|
||||||
<span key={c.uuid} className="badge gray" style={{ fontSize: 11 }}>{c.name}</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
{attributes.map(([key, value]) => (
|
{attributes.map(([key, value]) => (
|
||||||
<Row key={key} label={key}>{String(value)}</Row>
|
<Row key={key} label={key}>{String(value)}</Row>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -559,6 +559,12 @@
|
|||||||
|
|
||||||
مجوز: `appointment_settings.update`.
|
مجوز: `appointment_settings.update`.
|
||||||
|
|
||||||
|
> **از ۲۰۲۶-۰۸ در پنل ادمین سطحی ندارد.** تب «دستهبندیها»ی صفحهٔ منبع برداشته شد؛ اندپوینت و
|
||||||
|
> جدول و فیلد `categories` در پاسخ سرِ جایشاناند، ولی هیچ صفحهای آنها را نمینویسد و نمیخواند.
|
||||||
|
> تنها اثر رفتاریِ این داده، ترتیبِ `findEligible` است که مصرفکنندهاش (`AppointmentPlanBuilder`)
|
||||||
|
> فقط `count` و `max` میگیرد — یعنی امروز روی هیچ خروجیای اثر ندارد. «کدام منبع این سرویس را
|
||||||
|
> میدهد» را `ResourceServiceOffering` صریح و بهصورت فیلتر جواب میدهد.
|
||||||
|
|
||||||
دستهٔ منبع از **کاتالوگ سراسری** انتخاب میشود (`CatalogCategory`) — همان دستههایی که سرویس
|
دستهٔ منبع از **کاتالوگ سراسری** انتخاب میشود (`CatalogCategory`) — همان دستههایی که سرویس
|
||||||
هم از آنها استفاده میکند. ساخت دسته اینجا ممکن نیست؛ فقط در «تنظیمات ← دستهبندیها»
|
هم از آنها استفاده میکند. ساخت دسته اینجا ممکن نیست؛ فقط در «تنظیمات ← دستهبندیها»
|
||||||
([`clinic-services.md`](clinic-services.md)).
|
([`clinic-services.md`](clinic-services.md)).
|
||||||
|
|||||||
Reference in New Issue
Block a user