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)} نوبت آیندهٔ فعال دارد. غیرفعال‌کردن آن‌ها را + لغو نمی‌کند؛ فقط این منبع دیگر در جستجوی وقت نمی‌آید. + + )} +