import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api, ApiError, type ApiResponse } from '../lib/api'; import type { ClinicResource, ResourcePool, ResourcePayload, ResourceServiceOffering, ResourceType, Skill, } from '../types'; /** * منابع: هر چیزی که ممکن است اشغال باشد. هر منبع مال یک شعبه است، و شعبه همان * آدرس محل نوبت‌دهی است — پس فیلترها با `address_uuid` کار می‌کنند نه `branch_id`. */ const RESOURCES_KEY = 'resources'; const TYPES_KEY = ['resource-types']; const SKILLS_KEY = ['skills']; const POOLS_KEY = ['resource-pools']; function fail(e: unknown, fallback: string) { toast.error(e instanceof ApiError ? e.message : fallback); } export type ResourceFilters = { address_uuid?: string; type_uuid?: string; skill_uuid?: string; active?: string; }; function toQuery(filters: ResourceFilters): string { const params = new URLSearchParams(); Object.entries(filters).forEach(([k, v]) => { if (v) params.set(k, v); }); const qs = params.toString(); return qs === '' ? '' : `?${qs}`; } export function useResources(filters: ResourceFilters = {}) { const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: [RESOURCES_KEY] }); const query = useQuery({ queryKey: [RESOURCES_KEY, filters], queryFn: () => api.get>(`/api/v1/resources${toQuery(filters)}`), }); const create = useMutation({ mutationFn: (d: ResourcePayload) => api.post>('/api/v1/resource', d), onSuccess: () => { toast.success('منبع افزوده شد'); invalidate(); }, onError: (e) => fail(e, 'افزودن منبع ناموفق بود'), }); const update = useMutation({ mutationFn: ({ uuid, d }: { uuid: string; d: ResourcePayload }) => api.patch>(`/api/v1/resource/${uuid}`, d), onSuccess: () => { toast.success('منبع به‌روزرسانی شد'); invalidate(); }, onError: (e) => fail(e, 'به‌روزرسانی منبع ناموفق بود'), }); const remove = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/resource/${uuid}`), onSuccess: () => { toast.success('منبع حذف شد'); invalidate(); }, onError: (e) => fail(e, 'حذف منبع ناموفق بود'), }); /** جایگزینی کامل: مهارتی که در بدنه نیست، برداشته می‌شود. */ const setSkills = useMutation({ mutationFn: ({ uuid, skills }: { uuid: string; skills: { skill_uuid: string; level: number }[] }) => api.put>(`/api/v1/resource/${uuid}/skills`, { skills }), onSuccess: () => { toast.success('مهارت‌ها ذخیره شد'); invalidate(); }, onError: (e) => fail(e, 'ذخیرهٔ مهارت‌ها ناموفق بود'), }); return { resources: query.data?.data ?? [], loading: query.isLoading, create, update, remove, setSkills, }; } /** * سرویس‌هایی که یک منبع ارائه می‌دهد. * * مقدار مؤثر و منبعِ هر عدد از سرور می‌آید، نه از محاسبهٔ فرانت: زنجیرهٔ حل چهار سطح * دارد و بازسازی‌اش اینجا یعنی دو پیاده‌سازی که با هم واگرا می‌شوند. */ export function useResourceServices(resourceUuid?: string) { const qc = useQueryClient(); const key = ['resource-services', resourceUuid]; const query = useQuery({ queryKey: key, queryFn: () => api.get>(`/api/v1/resource/${resourceUuid}/services`), enabled: !!resourceUuid, }); const save = useMutation({ mutationFn: ({ uuid, services }: { uuid: string; services: Array> }) => api.put>(`/api/v1/resource/${uuid}/services`, { services }), onSuccess: () => { toast.success('سرویس‌های منبع ذخیره شد'); qc.invalidateQueries({ queryKey: ['resource-services'] }); qc.invalidateQueries({ queryKey: [RESOURCES_KEY] }); }, onError: (e) => fail(e, 'ذخیرهٔ سرویس‌ها ناموفق بود'), }); return { offerings: query.data?.data ?? [], loading: query.isLoading, save }; } export function useResourceTypes() { const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: TYPES_KEY }); const query = useQuery({ queryKey: TYPES_KEY, queryFn: () => api.get>('/api/v1/resource-types'), }); const create = useMutation({ mutationFn: (d: { code: string; name: string }) => api.post>('/api/v1/resource-types', d), onSuccess: () => { toast.success('نوع منبع افزوده شد'); invalidate(); }, onError: (e) => fail(e, 'افزودن نوع منبع ناموفق بود'), }); const update = useMutation({ mutationFn: ({ uuid, d }: { uuid: string; d: { name?: string; active?: boolean } }) => api.patch>(`/api/v1/resource-type/${uuid}`, d), onSuccess: () => { toast.success('نوع منبع به‌روزرسانی شد'); invalidate(); }, onError: (e) => fail(e, 'به‌روزرسانی ناموفق بود'), }); const remove = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/resource-type/${uuid}`), onSuccess: () => { toast.success('نوع منبع حذف شد'); invalidate(); }, onError: (e) => fail(e, 'حذف نوع منبع ناموفق بود'), }); return { types: query.data?.data ?? [], loading: query.isLoading, create, update, remove }; } export function useSkills() { const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: SKILLS_KEY }); const query = useQuery({ queryKey: SKILLS_KEY, queryFn: () => api.get>('/api/v1/skills'), }); const create = useMutation({ mutationFn: (d: { name: string }) => api.post>('/api/v1/skills', d), onSuccess: () => { toast.success('مهارت افزوده شد'); invalidate(); }, onError: (e) => fail(e, 'افزودن مهارت ناموفق بود'), }); const update = useMutation({ mutationFn: ({ uuid, d }: { uuid: string; d: { name?: string; active?: boolean } }) => api.patch>(`/api/v1/skill/${uuid}`, d), onSuccess: () => { toast.success('مهارت به‌روزرسانی شد'); invalidate(); }, onError: (e) => fail(e, 'به‌روزرسانی ناموفق بود'), }); const remove = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/skill/${uuid}`), onSuccess: () => { toast.success('مهارت حذف شد'); invalidate(); }, // پیام سرور دقیق است («به N منبع داده شده»)، پس همان نشان داده می‌شود. onError: (e) => fail(e, 'حذف مهارت ناموفق بود'), }); return { skills: query.data?.data ?? [], loading: query.isLoading, create, update, remove }; } export function useResourcePools() { const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: POOLS_KEY }); const query = useQuery({ queryKey: POOLS_KEY, queryFn: () => api.get>('/api/v1/resource-pools'), }); const create = useMutation({ mutationFn: (d: { address_uuid: string; type_uuid: string; name: string }) => api.post>('/api/v1/resource-pools', d), onSuccess: () => { toast.success('استخر افزوده شد'); invalidate(); }, onError: (e) => fail(e, 'افزودن استخر ناموفق بود'), }); const update = useMutation({ mutationFn: ({ uuid, d }: { uuid: string; d: { name?: string; active?: boolean } }) => api.patch>(`/api/v1/resource-pool/${uuid}`, d), onSuccess: () => { toast.success('استخر به‌روزرسانی شد'); invalidate(); }, onError: (e) => fail(e, 'به‌روزرسانی ناموفق بود'), }); const remove = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/resource-pool/${uuid}`), onSuccess: () => { toast.success('استخر حذف شد'); invalidate(); }, onError: (e) => fail(e, 'حذف استخر ناموفق بود'), }); /** جایگزینی کامل اعضا؛ سرور هم‌شعبه و هم‌نوع بودن را اجبار می‌کند. */ const setMembers = useMutation({ mutationFn: ({ uuid, members }: { uuid: string; members: { resource_uuid: string; priority: number }[] }) => api.put>(`/api/v1/resource-pool/${uuid}/members`, { members }), onSuccess: () => { toast.success('اعضای استخر ذخیره شد'); invalidate(); }, onError: (e) => fail(e, 'ذخیرهٔ اعضا ناموفق بود'), }); return { pools: query.data?.data ?? [], loading: query.isLoading, create, update, remove, setMembers }; } /** * جزئیات یک منبع — تنها جایی که شمار نوبت‌های آیندهٔ آن می‌آید. * * در فهرست نمی‌آید چون آنجا یک کوئری per ردیف می‌شد؛ اینجا فقط وقتی لازم است که * کاربر دارد همان یک منبع را ویرایش می‌کند. */ export function useResourceDetail(uuid: string | undefined) { const query = useQuery({ queryKey: ['resource-detail', uuid], queryFn: () => api.get>( `/api/v1/resource/${uuid}`, ), enabled: !!uuid, }); return { resource: query.data?.data, upcomingAppointments: query.data?.data?.upcoming_appointments ?? 0, loading: query.isLoading, }; }