feat(appointments): book onto a resource from its tab, drop the read-only resource timeline

"Add appointment" while a resource tab is active now opens a booking modal for
that resource: its own services, then a time, then the responsible doctor.

It reuses the booking engine that already existed (appointment-availability →
appointment-hold → appointment-confirm) rather than adding a second path. That
engine answers service-first and returns a resource assignment per slot, so the
modal keeps only the slots where the engine actually offered this resource and
pins that role to it on hold. Showing the other slots would let an operator pick
a time that can only come back as a 409.

The responsible doctor is required because every appointment has a doctor and
confirm will not run without one; the resource records which device the work
happens on.

The read-only "منابع" timeline under the schedule is removed along with its
component and hook, which had no other consumers. GET /api/v1/resources/timeline
is untouched on the backend and now has no client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 11:43:33 +03:30
co-authored by Claude Opus 5
parent 993478fcc0
commit fd27ceef7d
6 changed files with 353 additions and 456 deletions
-59
View File
@@ -1,59 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api, type ApiResponse } from '../lib/api';
export type ResourceTimelineItem = {
uuid: string;
starts_at: number;
ends_at: number;
status: 'booked' | 'hold';
segment_name: string | null;
appointment_id: number | null;
patient_name: string | null;
appointment_uuid: string | null;
appointment_status: string | null;
};
export type ResourceTimelineLane = {
uuid: string;
name: string;
type_name: string;
address_name: string | null;
capacity: number;
shifts: { start_minute: number; end_minute: number }[];
/** دقیقهٔ شیفت، دقیقهٔ پرشده (ظرفیت‌آگاه) و باقی‌ماندهٔ آزاد. */
shift_minutes: number;
busy_minutes: number;
free_minutes: number;
items: ResourceTimelineItem[];
};
export type ResourceTimelineDay = {
date: number;
day_of_week: number;
resources: ResourceTimelineLane[];
};
/**
* «کدام منبع در این روز کِی گرفته است» — ورودی نمای منبع‌محورِ صفحهٔ نوبت‌ها.
*
* بازه‌ها از جدول اشغال می‌آیند نه از خودِ نوبت: آماده‌سازی و تمیزکاری دستگاه هم
* درونشان است و همان بازه‌ای است که موتور جستجو اشغال می‌بیند.
*/
export function useResourceTimeline(date: string, options: { addressUuid?: string; onlyBookable?: boolean } = {}) {
const params = new URLSearchParams({ date });
if (options.addressUuid) params.set('address_uuid', options.addressUuid);
// منبعی که به هیچ سرویسی وصل نیست ردیفِ همیشه‌خالی است؛ در تایم‌لاین نوبت‌ها نمی‌آید.
if (options.onlyBookable) params.set('only_bookable', '1');
const query = useQuery({
queryKey: ['resource-timeline', date, options.addressUuid ?? null, options.onlyBookable ?? false],
queryFn: () => api.get<ApiResponse<ResourceTimelineDay>>(`/api/v1/resources/timeline?${params}`),
enabled: !!date,
});
return {
day: query.data?.data,
loading: query.isLoading,
error: query.isError ? ((query.error as Error)?.message || 'خطای نامشخص') : null,
};
}