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
+24 -12
View File
@@ -8,6 +8,7 @@ import {
import { XMarkIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import { useUrlState, pageOf } from '../hooks/useUrlState';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatDate, formatNumber, displayDoctorName } from '../lib/utils';
import ConfirmDialog from '../components/ui/ConfirmDialog';
@@ -101,26 +102,37 @@ export default function DoctorsPage() {
const primaryRole = useAuthStore(s => s.primaryRole);
const isRepresentation = primaryRole === 'representation';
const [page, setPage] = useState(1);
// وضعیت لیست در URL می‌ماند تا «بازگشت» از پروفایل پزشک، همین فیلترها و صفحه را برگرداند.
const [urlState, setUrlState] = useUrlState({
page: '1', search: '', status: '', owner: '', specialty: '', province: '', city: '', view: 'table',
});
const page = pageOf(urlState.page);
const search = urlState.search;
const status = urlState.status;
const ownerStatus = urlState.owner;
const specialtyId = urlState.specialty;
const provinceId = urlState.province;
const cityId = urlState.city;
const view: 'table' | 'grid' = urlState.view === 'grid' ? 'grid' : 'table';
const setPage = (p: number) => setUrlState({ page: String(p) });
const setStatus = (v: string) => setUrlState({ status: v, page: '1' });
const setOwnerStatus = (v: string) => setUrlState({ owner: v, page: '1' });
const setSpecialty = (v: string) => setUrlState({ specialty: v, page: '1' });
const setProvinceId = (v: string) => setUrlState({ province: v, city: '', page: '1' });
const setCityId = (v: string) => setUrlState({ city: v, page: '1' });
const setView = (v: 'table' | 'grid') => setUrlState({ view: v });
const [limit] = useState(25);
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [status, setStatus] = useState('');
const [ownerStatus, setOwnerStatus] = useState('');
const [specialtyId, setSpecialty] = useState('');
const [provinceId, setProvinceId] = useState('');
const [cityId, setCityId] = useState('');
const [view, setView] = useState<'table' | 'grid'>('table');
// فیلد جستجو local می‌ماند (تایپ روان)؛ مقدارِ debounce‌شده به URL می‌رود.
const [searchInput, setSearchInput] = useState(urlState.search);
const [deleteTarget, setDeleteTarget] = useState<AdminDoctor | null>(null);
const [mobileTarget, setMobileTarget] = useState<{ uuid: string; name: string; mobile_number?: string | null } | null>(null);
useEffect(() => {
const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350);
const t = setTimeout(() => setUrlState({ search: searchInput, page: '1' }), 350);
return () => clearTimeout(t);
}, [searchInput]);
useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId, provinceId, cityId]);
// ── Queries ──
const statsQ = useQuery({