feat(appointments): a resource-first view on the timeline
The appointments page only ever showed one doctor's row, but in the resource-first model a single appointment can hold a room and a device at the same time, and that — not the doctor's schedule — is what runs the capacity out. An hour could look free on the doctor's lane while the only alexandrite laser was already taken. A third view, "منابع", draws one lane per resource for the selected day. Blocks come from resource_occupancy rather than the appointment: that range includes the device's setup and cleanup minutes and is the same range the availability engine treats as busy. A multi-segment appointment therefore shows up on every resource it holds, and each block links to the appointment it belongs to. GET /api/v1/resources/timeline keeps a fixed query count — one for occupancy, one for shifts, one for the patient names — instead of one per resource. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../test/utils';
|
||||
import ResourceTimeline from './ResourceTimeline';
|
||||
import type { ResourceTimelineLane } from '../../hooks/useResourceTimeline';
|
||||
|
||||
/** نیمهشبِ یک روز ثابت، تا موقعیت بلوکها به ساعت اجرای تست وابسته نباشد. */
|
||||
const DAY_START = Math.floor(new Date(2026, 7, 5, 0, 0, 0).getTime() / 1000);
|
||||
const at = (hour: number, minute = 0) => DAY_START + hour * 3600 + minute * 60;
|
||||
|
||||
function lane(overrides: Partial<ResourceTimelineLane> = {}): ResourceTimelineLane {
|
||||
return {
|
||||
uuid: 'r-1',
|
||||
name: 'لیزر دایود',
|
||||
type_name: 'دستگاه لیزر',
|
||||
address_name: 'شعبهٔ مرکزی',
|
||||
capacity: 1,
|
||||
shifts: [{ start_minute: 9 * 60, end_minute: 17 * 60 }],
|
||||
items: [
|
||||
{
|
||||
uuid: 'o-1',
|
||||
starts_at: at(10),
|
||||
ends_at: at(11),
|
||||
status: 'booked',
|
||||
segment_name: 'لیزر',
|
||||
appointment_id: 47,
|
||||
patient_name: 'زهرا احمدی',
|
||||
appointment_uuid: 'appt-1',
|
||||
appointment_status: 'confirmed',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ResourceTimeline', () => {
|
||||
it('یک ردیف بهازای هر منبع با نام بیمار و بخش نشان میدهد', () => {
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[lane()]} dayStart={DAY_START} loading={false} error={null} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('لیزر دایود')).toBeInTheDocument();
|
||||
expect(screen.getByText('دستگاه لیزر')).toBeInTheDocument();
|
||||
expect(screen.getByText('زهرا احمدی · لیزر')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('بلوک به جزئیات همان نوبت لینک میدهد', () => {
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[lane()]} dayStart={DAY_START} loading={false} error={null} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('زهرا احمدی · لیزر').closest('a'))
|
||||
.toHaveAttribute('href', '/admin/appointments/appt-1');
|
||||
});
|
||||
|
||||
/**
|
||||
* یک نوبت میتواند همزمان اتاق و دستگاه را بگیرد؛ همین است که ظرفیت را تمام میکند و
|
||||
* نمای پزشکمحور نشانش نمیدهد.
|
||||
*/
|
||||
it('یک نوبت روی چند منبع، روی هر ردیف دیده میشود', () => {
|
||||
const room = lane({
|
||||
uuid: 'r-2',
|
||||
name: 'اتاق لیزر ۱',
|
||||
type_name: 'اتاق',
|
||||
items: [{ ...lane().items[0], uuid: 'o-2', segment_name: 'بیحسی موضعی' }],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[lane(), room]} dayStart={DAY_START} loading={false} error={null} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('زهرا احمدی · لیزر')).toBeInTheDocument();
|
||||
expect(screen.getByText('زهرا احمدی · بیحسی موضعی')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('روزِ بدون شیفت و بدون نوبت را صریح میگوید', () => {
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[lane({ shifts: [], items: [] })]} dayStart={DAY_START} loading={false} error={null} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('این روز شیفتی ندارد')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('بدون هیچ منبعی به صفحهٔ تعریف منبع راه میدهد', () => {
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[]} dayStart={DAY_START} loading={false} error={null} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('تنظیمات ← منابع').closest('a')).toHaveAttribute('href', '/admin/resources');
|
||||
});
|
||||
|
||||
it('خطای سرور را نشان میدهد، نه تایملاین خالی', () => {
|
||||
renderWithProviders(
|
||||
<ResourceTimeline lanes={[]} dayStart={DAY_START} loading={false} error="خطای داخلی سرور" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('خطای داخلی سرور')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user