Writing the query-count test that task 14 owed showed the growth was real: one resource cost 10 queries, six cost 33 — about five per resource, because the available-minutes figure walked each resource's calendar on its own. Holidays, tenant overrides and branch hours are identical for every resource in a report, so they now load once outside the loop; shifts and exceptions load for all resources in one query each. The batched path is a new method rather than a change to rawAvailability, which the booking engine also calls. The test pins the shape of the growth, not an exact count. Also landed: - app:segment:seed-templates with beauty, dental and physio presets. Building four segments and their requirements by hand is the first thing a new clinic must do and the most tedious; this gives them something to edit instead of an empty page. It refuses to touch a service that already has segments unless --force, and it will not invent resource types the tenant never defined. - book-all is all-or-nothing, proven rather than asserted: with a calendar open one day a week and a 1-2 day protocol gap, session one finds a slot and session two cannot, and every session must come back planned. - credit_refundable: false takes the credit back with a negative adjustment and deletes nothing — the ledger stays append-only. - the segments editor has frontend tests, including that it sends back what the user sees and renders read-only without the permission. useBranches now returns [] for a non-array payload instead of throwing "branches.map is not a function" and taking the page down with it. BookingLocationsScanTest built a Clinic around a Doctor loaded from a different manager, which Doctrine treats as a new entity; it flushed fine most runs and failed on cascade in others. It now loads the doctor from the same manager. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api, ApiError, type ApiResponse } from '../lib/api';
|
|
import type { Branch, BranchWorkingHours, Room, RoomPayload, WorkingHoursPayload } from '../types';
|
|
|
|
/**
|
|
* «شعبه» یک جدول تازه نیست — همان آدرس محل نوبتدهی است (`doctor_addresses`).
|
|
* ساخت/ویرایش نام و آدرس همانجایی انجام میشود که همیشه (جزئیات کلینیک/پزشک)؛
|
|
* این هوک فقط چیزهای شعبهای را میدهد: فعال/غیرفعال، منطقهٔ زمانی، ساعت کاری، اتاق.
|
|
*/
|
|
const BRANCHES_KEY = ['branches'];
|
|
|
|
function fail(e: unknown, fallback: string) {
|
|
toast.error(e instanceof ApiError ? e.message : fallback);
|
|
}
|
|
|
|
export function useBranches() {
|
|
const qc = useQueryClient();
|
|
|
|
const query = useQuery({
|
|
queryKey: BRANCHES_KEY,
|
|
queryFn: () => api.get<ApiResponse<Branch[]>>('/api/v1/branches'),
|
|
});
|
|
|
|
const update = useMutation({
|
|
mutationFn: ({ uuid, d }: { uuid: string; d: { active?: boolean; timezone?: string } }) =>
|
|
api.patch<ApiResponse<Branch>>(`/api/v1/branch/${uuid}`, d),
|
|
onSuccess: () => {
|
|
toast.success('شعبه بهروزرسانی شد');
|
|
qc.invalidateQueries({ queryKey: BRANCHES_KEY });
|
|
},
|
|
onError: (e) => fail(e, 'بهروزرسانی شعبه ناموفق بود'),
|
|
});
|
|
|
|
// پاسخِ غیرآرایه (خطای سرور، شکل دیگر) نباید صفحه را با «map is not a function»
|
|
// بترکاند؛ فهرست خالی رفتار درست است.
|
|
const branches = Array.isArray(query.data?.data) ? query.data.data : [];
|
|
|
|
return { branches, loading: query.isLoading, update };
|
|
}
|
|
|
|
export function useBranchWorkingHours(branchUuid: string | undefined) {
|
|
const qc = useQueryClient();
|
|
const key = ['branch-working-hours', branchUuid];
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () => api.get<ApiResponse<BranchWorkingHours>>(`/api/v1/branch/${branchUuid}/working-hours`),
|
|
enabled: !!branchUuid,
|
|
});
|
|
|
|
/** PUT قرارداد جایگزینی کامل دارد: آرایهٔ خالی یعنی شعبه بسته، نه «تغییری نده». */
|
|
const save = useMutation({
|
|
mutationFn: (days: WorkingHoursPayload) =>
|
|
api.put<ApiResponse<BranchWorkingHours>>(`/api/v1/branch/${branchUuid}/working-hours`, { days }),
|
|
onSuccess: () => {
|
|
toast.success('ساعت کاری ذخیره شد');
|
|
qc.invalidateQueries({ queryKey: key });
|
|
qc.invalidateQueries({ queryKey: BRANCHES_KEY });
|
|
},
|
|
onError: (e) => fail(e, 'ذخیرهٔ ساعت کاری ناموفق بود'),
|
|
});
|
|
|
|
return { workingHours: query.data?.data, loading: query.isLoading, save };
|
|
}
|
|
|
|
export function useBranchRooms(branchUuid: string | undefined) {
|
|
const qc = useQueryClient();
|
|
const key = ['branch-rooms', branchUuid];
|
|
|
|
const invalidate = () => {
|
|
qc.invalidateQueries({ queryKey: key });
|
|
qc.invalidateQueries({ queryKey: BRANCHES_KEY });
|
|
};
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () => api.get<ApiResponse<Room[]>>(`/api/v1/branch/${branchUuid}/rooms`),
|
|
enabled: !!branchUuid,
|
|
});
|
|
|
|
const create = useMutation({
|
|
mutationFn: (d: RoomPayload) =>
|
|
api.post<ApiResponse<Room>>('/api/v1/room', { ...d, address_uuid: branchUuid }),
|
|
onSuccess: () => { toast.success('اتاق افزوده شد'); invalidate(); },
|
|
onError: (e) => fail(e, 'افزودن اتاق ناموفق بود'),
|
|
});
|
|
|
|
const update = useMutation({
|
|
mutationFn: ({ uuid, d }: { uuid: string; d: RoomPayload }) =>
|
|
api.patch<ApiResponse<Room>>(`/api/v1/room/${uuid}`, d),
|
|
onSuccess: () => { toast.success('اتاق بهروزرسانی شد'); invalidate(); },
|
|
onError: (e) => fail(e, 'بهروزرسانی اتاق ناموفق بود'),
|
|
});
|
|
|
|
const remove = useMutation({
|
|
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/room/${uuid}`),
|
|
onSuccess: () => { toast.success('اتاق حذف شد'); invalidate(); },
|
|
onError: (e) => fail(e, 'حذف اتاق ناموفق بود'),
|
|
});
|
|
|
|
return { rooms: query.data?.data ?? [], loading: query.isLoading, create, update, remove };
|
|
}
|