From 000cf7076121bc82ccabcb4cca31d38517e9e19b Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sat, 1 Aug 2026 13:45:25 +0330 Subject: [PATCH] feat(resources): warn before switching a resource off, and pick dates in Jalali The deactivation warning was blocked on task 07: there was no way to count "the appointments on this resource" until occupancy rows linked the two. They do now, so GET /resource/{uuid} returns upcoming_appointments. It stays off the list endpoint, where it would be one count query per row. It is a warning, not a block, and the wording says so: switching a resource off does not cancel anything, it only removes the resource from future searches. The panel shows it the moment the "active" box is unticked. The calendar's exception range still used , which is Gregorian. Operators say dates in Jalali, and the mental conversion is exactly where an exception gets recorded a day off. PersianDateInput takes the same YYYY-MM-DD string, so this is a drop-in swap. Co-Authored-By: Claude Opus 5 (1M context) --- .../resources/ResourceFormModal.test.tsx | 67 +++++++++++++++++++ .../resources/ResourceFormModal.tsx | 23 +++++++ assets/admin/hooks/useResources.ts | 23 +++++++ assets/admin/pages/ResourceCalendarPage.tsx | 11 ++- docs/api/resource.md | 9 +++ .../task-02-resource-model/checklist.md | 2 +- .../task-03-resource-calendar/checklist.md | 2 +- .../ResourceOccupancyRepository.php | 22 ++++++ .../Controller/ResourceController.php | 10 ++- 9 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 assets/admin/components/resources/ResourceFormModal.test.tsx diff --git a/assets/admin/components/resources/ResourceFormModal.test.tsx b/assets/admin/components/resources/ResourceFormModal.test.tsx new file mode 100644 index 00000000..37758bf2 --- /dev/null +++ b/assets/admin/components/resources/ResourceFormModal.test.tsx @@ -0,0 +1,67 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '../../test/utils'; + +vi.mock('../../lib/api', () => ({ + api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, + ApiError: class extends Error {}, +})); + +vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +import { api } from '../../lib/api'; +import ResourceFormModal from './ResourceFormModal'; + +const get = api.get as ReturnType; + +const resource = { + uuid: 'r-1', + name: 'لیزر ۱', + address_uuid: 'b-1', + type_uuid: 't-1', + capacity: 1, + is_active: true, + attributes: {}, +}; + +const props = { + open: true, + resource: resource as never, + branches: [{ uuid: 'b-1', name: 'شعبه' }] as never, + types: [{ uuid: 't-1', name: 'دستگاه', code: 'device' }] as never, + saving: false, + onClose: () => {}, + onSave: () => {}, +}; + +describe('ResourceFormModal — هشدار غیرفعال‌سازی', () => { + beforeEach(() => vi.clearAllMocks()); + + /** ⭐ غیرفعال‌کردن نوبت‌ها را لغو نمی‌کند؛ اپراتور باید بداند چند بیمار درگیرند. */ + it('warns with the upcoming count only once the resource is switched off', async () => { + get.mockResolvedValue({ data: { ...resource, upcoming_appointments: 4 } }); + const user = userEvent.setup(); + + renderWithProviders(); + + await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/resource/r-1')); + expect(screen.queryByText(/نوبت آیندهٔ فعال دارد/)).toBeNull(); + + await user.click(screen.getByLabelText('منبع فعال است')); + + expect(await screen.findByText(/۴ نوبت آیندهٔ فعال دارد/)).toBeInTheDocument(); + }); + + it('stays quiet when nothing is booked on the resource', async () => { + get.mockResolvedValue({ data: { ...resource, upcoming_appointments: 0 } }); + const user = userEvent.setup(); + + renderWithProviders(); + + await waitFor(() => expect(get).toHaveBeenCalled()); + await user.click(screen.getByLabelText('منبع فعال است')); + + expect(screen.queryByText(/نوبت آیندهٔ فعال دارد/)).toBeNull(); + }); +}); diff --git a/assets/admin/components/resources/ResourceFormModal.tsx b/assets/admin/components/resources/ResourceFormModal.tsx index d19f0bf1..09c93026 100644 --- a/assets/admin/components/resources/ResourceFormModal.tsx +++ b/assets/admin/components/resources/ResourceFormModal.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'; import Modal from '../ui/Modal'; import SearchableSelect from '../ui/SearchableSelect'; import type { Branch, ClinicResource, ResourcePayload, ResourceType } from '../../types'; +import { useResourceDetail } from '../../hooks/useResources'; +import { formatNumber } from '../../lib/utils'; /** کلیدهای شناخته‌شدهٔ ویژگی — قرارداد است نه اجبار؛ سرور هر کلید snake_case را می‌پذیرد. */ const KNOWN_ATTRIBUTES = ['gender', 'device_model', 'floor', 'brand']; @@ -21,6 +23,8 @@ interface Props { export default function ResourceFormModal({ open, resource, branches, types, saving, onClose, onSave, }: Props) { + // شمار نوبت‌های آینده فقط برای منبعِ موجود معنا دارد و فقط وقتی مودال باز است. + const { upcomingAppointments: upcoming } = useResourceDetail(open ? resource?.uuid : undefined); const [name, setName] = useState(''); const [addressUuid, setAddressUuid] = useState(null); const [typeUuid, setTypeUuid] = useState(null); @@ -184,6 +188,25 @@ export default function ResourceFormModal({ منبع فعال است + {/* غیرفعال‌کردن نوبت‌های ثبت‌شده را لغو نمی‌کند؛ فقط از جستجوی وقتِ بعدی حذف + می‌شود. پس این هشدار است نه مانع — ولی اپراتور باید بداند چند بیمار روی + منبعی نوبت دارند که دارد خاموش می‌شود. */} + {!active && upcoming > 0 && ( + + این منبع {formatNumber(upcoming)} نوبت آیندهٔ فعال دارد. غیرفعال‌کردن آن‌ها را + لغو نمی‌کند؛ فقط این منبع دیگر در جستجوی وقت نمی‌آید. + + )} +