feat(catalog): manage global categories from settings
Categories are the taxonomy both services and resources select from, but
until now they could only be reached through the service catalog, so every
environment ended up with its own spelling of "whole body".
- Settings > Categories page: global CRUD plus the "includes" edge
- POST/GET/DELETE /api/v1/service-category/{uuid}/includes — a DAG, kept
separate from `parent` because "hand" sits under both "whole body" and
"upper limb"; a cycle is refused with 422
- PUT /api/v1/resource/{uuid}/categories — full replacement, and a category
from another environment is rejected explicitly since the uuid arrives in
the request body where TenantFilter does not reach
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { api, ApiError, type ApiResponse } from '../lib/api';
|
||||
import type { CatalogCategory } from '../types';
|
||||
|
||||
/**
|
||||
* دستهٔ **سراسری** کلینیک — یک بار در تنظیمات تعریف میشود و سرویس و منبع از همان
|
||||
* استفاده میکنند.
|
||||
*
|
||||
* ⚠️ با `useServiceCategories` اشتباه نشود: آن یکی enum نوع خدمت (سرپایی/بستری) است و
|
||||
* همنام بودنشان تصادفی است.
|
||||
*/
|
||||
const KEY = ['catalog-categories'];
|
||||
|
||||
function fail(e: unknown, fallback: string) {
|
||||
toast.error(e instanceof ApiError ? e.message : fallback);
|
||||
}
|
||||
|
||||
export function useCatalogCategories() {
|
||||
const qc = useQueryClient();
|
||||
const invalidate = () => qc.invalidateQueries({ queryKey: KEY });
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: KEY,
|
||||
queryFn: () => api.get<ApiResponse<CatalogCategory[]>>('/api/v1/service-categories/tree'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: (d: { name: string; parent_uuid?: string | null }) =>
|
||||
api.post<ApiResponse<CatalogCategory>>('/api/v1/service-category', d),
|
||||
onSuccess: () => { toast.success('دستهبندی افزوده شد'); invalidate(); },
|
||||
onError: (e) => fail(e, 'افزودن دستهبندی ناموفق بود'),
|
||||
});
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: ({ uuid, d }: { uuid: string; d: { name?: string; sort_order?: number; active?: boolean } }) =>
|
||||
api.patch<ApiResponse<CatalogCategory>>(`/api/v1/service-category/${uuid}`, d),
|
||||
onSuccess: () => { toast.success('دستهبندی بهروزرسانی شد'); invalidate(); },
|
||||
onError: (e) => fail(e, 'بهروزرسانی ناموفق بود'),
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/service-category/${uuid}`),
|
||||
// پیام سرور دقیق است («ابتدا زیردستهها را حذف کنید»)، پس همان نشان داده میشود.
|
||||
onSuccess: () => { toast.success('دستهبندی حذف شد'); invalidate(); },
|
||||
onError: (e) => fail(e, 'حذف دستهبندی ناموفق بود'),
|
||||
});
|
||||
|
||||
return { tree: query.data?.data ?? [], loading: query.isLoading, create, update, remove };
|
||||
}
|
||||
|
||||
/**
|
||||
* یالهای «این دسته شامل آن دسته است».
|
||||
*
|
||||
* جدا از `parent` است: آن سلسلهمراتب نمایشیِ منوست، این میگوید انتخاب همزمان دو
|
||||
* سرویس از این دو دسته تعارض دارد («تمام بدن» و «دست»).
|
||||
*/
|
||||
export function useCategoryIncludes(categoryUuid?: string) {
|
||||
const qc = useQueryClient();
|
||||
const key = ['category-includes', categoryUuid];
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => api.get<ApiResponse<CatalogCategory[]>>(`/api/v1/service-category/${categoryUuid}/includes`),
|
||||
enabled: !!categoryUuid,
|
||||
});
|
||||
|
||||
const invalidate = () => qc.invalidateQueries({ queryKey: ['category-includes'] });
|
||||
|
||||
const add = useMutation({
|
||||
mutationFn: ({ uuid, childUuid }: { uuid: string; childUuid: string }) =>
|
||||
api.post<ApiResponse<CatalogCategory>>(`/api/v1/service-category/${uuid}/includes`, { child_category_uuid: childUuid }),
|
||||
onSuccess: () => { toast.success('زیرمجموعه افزوده شد'); invalidate(); },
|
||||
// پیام سرور میگوید کدام دو دسته حلقه میسازند.
|
||||
onError: (e) => fail(e, 'افزودن زیرمجموعه ناموفق بود'),
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: ({ uuid, childUuid }: { uuid: string; childUuid: string }) =>
|
||||
api.delete<ApiResponse<null>>(`/api/v1/service-category/${uuid}/includes/${childUuid}`),
|
||||
onSuccess: () => { toast.success('زیرمجموعه حذف شد'); invalidate(); },
|
||||
onError: (e) => fail(e, 'حذف زیرمجموعه ناموفق بود'),
|
||||
});
|
||||
|
||||
return { includes: query.data?.data ?? [], loading: query.isLoading, add, remove };
|
||||
}
|
||||
Reference in New Issue
Block a user