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
+36 -6
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import {
@@ -6,6 +6,7 @@ import {
UserIcon, EllipsisHorizontalIcon,
} from '@heroicons/react/24/outline';
import { api } from '../lib/api';
import { useUrlState, pageOf } from '../hooks/useUrlState';
import type { ApiResponse } from '../lib/api';
import type { PatientRecord } from '../types';
import { formatNumber, toDate } from '../lib/utils';
@@ -105,14 +106,43 @@ export default function PatientsListPage() {
const { can } = usePermissions();
const canCreate = can('patients', 'create');
const canUpdate = can('patients', 'update');
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [view, setView] = useState<'table' | 'card'>('card');
// وضعیت لیست (جستجو، فیلترها، نما، صفحه) در URL می‌ماند تا «بازگشت» از پرونده،
// دقیقاً همین نما را برگرداند — نه لیست خام صفحهٔ اول.
const [urlState, setUrlState] = useUrlState({
page: '1', search: '', view: 'card',
gender: '', insurance_id: '', service_status: '', has_debt: '', tags: '',
admitted_from: '', admitted_to: '',
});
const page = pageOf(urlState.page);
const search = urlState.search;
const view: 'table' | 'card' = urlState.view === 'table' ? 'table' : 'card';
const setPage = (p: number) => setUrlState({ page: String(p) });
const setSearch = (v: string) => setUrlState({ search: v, page: '1' });
const [filterOpen, setFilterOpen] = useState(false);
const [filters, setFilters] = useState<PatientFilters>({});
const filters: PatientFilters = useMemo(() => ({
...(urlState.gender ? { gender: urlState.gender } : {}),
...(urlState.insurance_id ? { insurance_id: urlState.insurance_id } : {}),
...(urlState.service_status ? { service_status: urlState.service_status } : {}),
...(urlState.has_debt ? { has_debt: true } : {}),
...(urlState.tags ? { tags: urlState.tags.split(',') } : {}),
...(urlState.admitted_from ? { admitted_from: urlState.admitted_from } : {}),
...(urlState.admitted_to ? { admitted_to: urlState.admitted_to } : {}),
}), [urlState]);
const setFilters = (f: PatientFilters) => setUrlState({
gender: f.gender ?? '',
insurance_id: f.insurance_id ?? '',
service_status: f.service_status ?? '',
has_debt: f.has_debt ? '1' : '',
tags: f.tags?.join(',') ?? '',
admitted_from: f.admitted_from ?? '',
admitted_to: f.admitted_to ?? '',
page: '1',
});
const pageSize = view === 'card' ? CARD_PAGE_SIZE : TABLE_PAGE_SIZE;
const switchView = (v: 'table' | 'card') => { setView(v); setPage(1); };
const switchView = (v: 'table' | 'card') => setUrlState({ view: v, page: '1' });
const qs = new URLSearchParams({ page: String(page), limit: String(pageSize) });
if (search) qs.set('search', search);