Files
clinicpro/assets/admin/components/appointments/ResourceTimeline.test.tsx
T
hamedandClaude Opus 5 444ebc897a 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>
2026-08-02 14:14:10 +03:30

100 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});