Three pages, all on the existing design system: BranchesPage lists the current environment's booking locations with their working-hours and active-room counts, and two subpages edit the week and the rooms. The list page deliberately does not create or rename a branch — clinic and doctor detail pages already do that, and duplicating it would give one physical place two edit surfaces. Route permission reuses `appointment_settings` rather than inventing a new one. Two real bugs fell out of exercising this end to end: `days` was serialising as a JSON *array*, not an object keyed "0".."6" — keys 0..6 are sequential so json_encode collapses them to a list. The client reads days["0"] either way, so nothing looked broken, but the response shape was unstable: one missing day would flip the same field to an object. The controller now casts to stdClass and WorkingHoursTest::testDaysIsAJsonObjectNotAnArray pins it. Found by curling the endpoint for the docs, not by any test. `<input type="time">` caps at 23:59, so it can neither display nor produce the legal end value 1440. An all-day range would have vanished from the form and been corrupted by the first save. Ranges now carry an explicit end-of-day flag, with a round-trip test proving 1440 survives. docs/api/branch.md documents all eight endpoints with responses captured from real curl runs against ddev, including the 422 and 404 bodies. doctor.md records that active/timezone now appear on all nine existing address endpoints (additive), and tenancy.md gains the two lessons this task taught: an aggregate child whose root is itself declared global inherits no environment and needs a real pair, and TenantFilter is not a substitute for an explicit ownership check because hard isolation only applies to a *chosen* context. Verified: phpunit 1067 tests / 2974 assertions green; slot-mode frozen contract green; phpstan 14 errors before and after, none in touched files; tsc clean; vitest 87 files / 612 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
4.0 KiB
TypeScript
100 lines
4.0 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, 'بهروزرسانی شعبه ناموفق بود'),
|
|
});
|
|
|
|
return { branches: query.data?.data ?? [], 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 };
|
|
}
|