The table had two date columns for one date: "تاریخ" printed the raw 1405-05-13 string in Latin digits, and the column labelled "میلادی" ran the same day through formatDate — which returns Jalali. One date, twice, under a label that lied. It is now a single formatted Jalali column. The closure form used a native <input type="date">: Gregorian, an English mm/dd/yyyy placeholder in an RTL Persian panel, and a white box in dark mode because a native control does not follow the theme. It is the shared Persian picker now. That picker turned out to be a div with an onClick — no role, no tab stop, no accessible name, and its clear button was a span. Since every page that picks a date goes through it, it gained role/tabIndex/Enter-Space, an ariaLabel prop, and a real button for clear. The page passes labels for the year select and both form fields, and the global topbar search got an aria-label, which takes the runtime accessibility probe on this page to clean. useHolidays now returns an error, so a failed request reads as an error instead of an empty year — previously indistinguishable. The page had no test file; it has eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api, ApiError, type ApiResponse } from '../lib/api';
|
|
import type {
|
|
HolidayOverride, NationalHoliday, ResourceAvailability,
|
|
ResourceCalendarDays, ResourceException, WorkingHoursPayload,
|
|
} from '../types';
|
|
|
|
function fail(e: unknown, fallback: string) {
|
|
toast.error(e instanceof ApiError ? e.message : fallback);
|
|
}
|
|
|
|
export function useResourceCalendar(resourceUuid: string | undefined) {
|
|
const qc = useQueryClient();
|
|
const key = ['resource-calendar', resourceUuid];
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () => api.get<ApiResponse<ResourceCalendarDays>>(`/api/v1/resource/${resourceUuid}/calendar`),
|
|
enabled: !!resourceUuid,
|
|
});
|
|
|
|
/** PUT جایگزینی کامل است: روزی که نفرستید خالی میشود. */
|
|
const save = useMutation({
|
|
mutationFn: (days: WorkingHoursPayload) =>
|
|
api.put<ApiResponse<ResourceCalendarDays>>(`/api/v1/resource/${resourceUuid}/calendar`, { days }),
|
|
onSuccess: () => {
|
|
toast.success('شیفتها ذخیره شد');
|
|
qc.invalidateQueries({ queryKey: key });
|
|
qc.invalidateQueries({ queryKey: ['resource-availability', resourceUuid] });
|
|
},
|
|
onError: (e) => fail(e, 'ذخیرهٔ شیفتها ناموفق بود'),
|
|
});
|
|
|
|
return { calendar: query.data?.data, loading: query.isLoading, save };
|
|
}
|
|
|
|
export function useResourceExceptions(resourceUuid: string | undefined) {
|
|
const qc = useQueryClient();
|
|
const key = ['resource-exceptions', resourceUuid];
|
|
|
|
const invalidate = () => {
|
|
qc.invalidateQueries({ queryKey: key });
|
|
qc.invalidateQueries({ queryKey: ['resource-availability', resourceUuid] });
|
|
};
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () => api.get<ApiResponse<ResourceException[]>>(`/api/v1/resource/${resourceUuid}/exceptions`),
|
|
enabled: !!resourceUuid,
|
|
});
|
|
|
|
const create = useMutation({
|
|
mutationFn: (d: { type: string; starts_at: number; ends_at: number; reason?: string | null }) =>
|
|
api.post<ApiResponse<ResourceException>>(`/api/v1/resource/${resourceUuid}/exception`, d),
|
|
onSuccess: () => { toast.success('استثنا ثبت شد'); invalidate(); },
|
|
onError: (e) => fail(e, 'ثبت استثنا ناموفق بود'),
|
|
});
|
|
|
|
const remove = useMutation({
|
|
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/resource-exception/${uuid}`),
|
|
onSuccess: () => { toast.success('استثنا حذف شد'); invalidate(); },
|
|
onError: (e) => fail(e, 'حذف استثنا ناموفق بود'),
|
|
});
|
|
|
|
return { exceptions: query.data?.data ?? [], loading: query.isLoading, create, remove };
|
|
}
|
|
|
|
export function useResourceAvailability(resourceUuid: string | undefined, from: number, to: number) {
|
|
const query = useQuery({
|
|
queryKey: ['resource-availability', resourceUuid, from, to],
|
|
queryFn: () =>
|
|
api.get<ApiResponse<ResourceAvailability>>(`/api/v1/resource/${resourceUuid}/availability?from=${from}&to=${to}`),
|
|
enabled: !!resourceUuid,
|
|
});
|
|
|
|
return { availability: query.data?.data, loading: query.isLoading };
|
|
}
|
|
|
|
export function useHolidays(year: number) {
|
|
const qc = useQueryClient();
|
|
const key = ['national-holidays', year];
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () =>
|
|
api.get<ApiResponse<{ year: number; holidays: NationalHoliday[]; overrides: HolidayOverride[] }>>(
|
|
`/api/v1/national-holidays?year=${year}`,
|
|
),
|
|
});
|
|
|
|
const setOverride = useMutation({
|
|
mutationFn: (d: { date: number; is_working: boolean; note?: string | null }) =>
|
|
api.post<ApiResponse<HolidayOverride>>('/api/v1/holiday-overrides', d),
|
|
onSuccess: () => { toast.success('استثنای تعطیلی ذخیره شد'); qc.invalidateQueries({ queryKey: key }); },
|
|
onError: (e) => fail(e, 'ذخیرهٔ استثنا ناموفق بود'),
|
|
});
|
|
|
|
const removeOverride = useMutation({
|
|
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/holiday-override/${uuid}`),
|
|
onSuccess: () => { toast.success('استثنا حذف شد'); qc.invalidateQueries({ queryKey: key }); },
|
|
onError: (e) => fail(e, 'حذف استثنا ناموفق بود'),
|
|
});
|
|
|
|
return {
|
|
holidays: query.data?.data?.holidays ?? [],
|
|
overrides: query.data?.data?.overrides ?? [],
|
|
loading: query.isLoading,
|
|
// بدون این، درخواستِ شکستخورده مثل «سالِ خالی» دیده میشود و کاربر دنبال
|
|
// تعطیلاتی میگردد که ثبت شدهاند ولی نیامدهاند.
|
|
error: query.isError ? ((query.error as Error)?.message || 'خطای نامشخص') : null,
|
|
setOverride,
|
|
removeOverride,
|
|
};
|
|
}
|