feat: implement useUrlState hook for managing URL-based state in admin pages

- Refactor multiple admin pages (BlogsPage, ClinicsPage, DoctorsPage, etc.) to utilize the new useUrlState hook for managing pagination, search, and filter states via URL.
- Ensure that the state persists in the URL, allowing users to return to the same state when navigating back from detail pages.
- Update relevant components to handle state changes appropriately and maintain clean URLs by removing default values.
- Add SlotPicker component for selecting appointment slots based on availability.
- Create tests for useUrlState to validate its functionality and ensure correct behavior when interacting with the URL.
- Update API documentation to reflect changes in appointment creation and slot selection processes.
This commit is contained in:
hamed
2026-07-29 21:04:40 +03:30
parent e0e8fbd1e4
commit 684cf1f783
20 changed files with 668 additions and 79 deletions
@@ -13,6 +13,13 @@ import type { Appointment } from '../types';
const get = api.get as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;
const post = api.post as ReturnType<typeof vi.fn>;
const navigate = vi.fn();
vi.mock('react-router-dom', async () => ({
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
useNavigate: () => navigate,
}));
const appt: Appointment = {
uuid: 'ap1', patient_name: 'مریم خلیلی', patient_mobile: '09136549874',
@@ -28,11 +35,13 @@ const appt: Appointment = {
beforeEach(() => {
get.mockReset(); patch.mockReset();
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patient?search=')) return Promise.resolve({ success: true, data: [{ uuid: 'rec1' }] });
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [{ uuid: 'rec1' }] });
if (url === '/api/v1/patient/rec1/wallet') return Promise.resolve({ success: true, data: { balance_rials: 500000, recent_transactions: [] } });
return Promise.resolve({ success: true, data: [] });
});
patch.mockResolvedValue({ success: true, data: {} });
post.mockReset();
navigate.mockReset();
});
function openMenu() {
@@ -48,6 +57,33 @@ describe('AppointmentActionsMenu (عملیات نوبت)', () => {
}
});
it('«ثبت سرویس» پروندهٔ موجود بیمار را باز می‌کند', async () => {
openMenu();
fireEvent.click(screen.getByText('ثبت سرویس'));
await waitFor(() => expect(navigate).toHaveBeenCalledWith('/admin/patients/rec1/session/new'));
// مسیر جستجو باید همان endpoint واقعی باشد، وگرنه ۴۰۴ می‌گیرد و «خطا در یافتن پرونده» می‌دهد.
expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/patients?search='));
expect(post).not.toHaveBeenCalled();
});
it('«ثبت سرویس» برای بیمارِ بدون پرونده، اول پرونده می‌سازد', async () => {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [] });
return Promise.resolve({ success: true, data: [] });
});
post.mockResolvedValue({ success: true, data: { uuid: 'rec-new' } });
openMenu();
fireEvent.click(screen.getByText('ثبت سرویس'));
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/patient', expect.objectContaining({
mobile: '09136549874',
name: 'مریم خلیلی',
})));
expect(navigate).toHaveBeenCalledWith('/admin/patients/rec-new/session/new');
});
it('info modal shows appointment details and patient wallet balance', async () => {
openMenu();
fireEvent.click(screen.getByText('مشاهده'));
@@ -107,7 +143,7 @@ describe('AppointmentActionsMenu (عملیات نوبت)', () => {
it('replace modal picks an existing patient from the record search', async () => {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patient?search=')) return Promise.resolve({ success: true, data: [
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [
{ uuid: 'rec9', user_name: 'پریسا همتی', user_mobile: '09120009999' },
] });
return Promise.resolve({ success: true, data: [] });