import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router-dom'; import { PencilIcon, EyeIcon, ExclamationCircleIcon, IdentificationIcon, 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'; import Pagination from '../components/ui/Pagination'; import PatientTagsCell from '../components/PatientTagsCell'; import PatientsFilterModal, { type PatientFilters } from '../components/PatientsFilterModal'; import { usePermissions } from '../hooks/usePermissions'; import { SearchHeaderP, TurnsFilter, PatientsGridView, PatientsCategoryView, AddTurn, } from '../components/icons/FilesToolbarIcons'; // tauri /files paginates card view by 16 and table view by 12. const CARD_PAGE_SIZE = 16; const TABLE_PAGE_SIZE = 12; const EMPTY: PatientRecord[] = []; /** unix start-of-day for `from`, end-of-day for `to`, from a gregorian Y-m-d. */ function dayBound(value: string | undefined, end: boolean): number | undefined { if (!value) return undefined; const d = toDate(value); if (!d) return undefined; const secs = Math.floor(d.setHours(0, 0, 0, 0) / 1000); return end ? secs + 86399 : secs; } /** Number of filters currently applied (for the button badge). */ function countFilters(f: PatientFilters): number { return [f.gender, f.insurance_id, f.admitted_from, f.admitted_to, f.service_status, f.has_debt || undefined, f.tags?.length ? '1' : undefined] .filter(Boolean).length; } /** * A single patient card — mirrors tauri `files/list/CardView` pixel-for-pixel * (avatar + name + ⋮ menu header, file-number/mobile rows, tags footer). */ function PatientCard({ r, onView, onEdit, canUpdate }: { r: PatientRecord; onView: () => void; onEdit: () => void; canUpdate: boolean }) { const [menu, setMenu] = useState(false); return (
{ if ((e.target as HTMLElement).closest('.more-square-icon')) return; onView(); }} >

{r.user_name || '—'}

{menu && ( <>
{ e.stopPropagation(); setMenu(false); }} />
{canUpdate && ( )}
)}
شماره پرونده: {r.record_number || '—'}
موبایل: {r.user_mobile || '—'}
e.stopPropagation()} > برچسب‌ها:
); } /** پرونده‌ها — patient records list. Ported from tauri /files (default card view). */ export default function PatientsListPage() { const navigate = useNavigate(); // مجوزهای منشی؛ برای owner/پزشک همیشه true (usePermissions بدون context آزاد است). const { can } = usePermissions(); const canCreate = can('patients', 'create'); const canUpdate = can('patients', 'update'); // وضعیت لیست (جستجو، فیلترها، نما، صفحه) در 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: 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') => setUrlState({ view: v, page: '1' }); const qs = new URLSearchParams({ page: String(page), limit: String(pageSize) }); if (search) qs.set('search', search); if (filters.gender) qs.set('gender', filters.gender); if (filters.insurance_id) qs.set('insurance_id', filters.insurance_id); if (filters.service_status) qs.set('service_status', filters.service_status); if (filters.has_debt) qs.set('has_debt', '1'); if (filters.tags?.length) qs.set('tags', filters.tags.join(',')); const af = dayBound(filters.admitted_from, false); const at = dayBound(filters.admitted_to, true); if (af) qs.set('admitted_from', String(af)); if (at) qs.set('admitted_to', String(at)); const { data, isLoading, error } = useQuery & { meta?: { totalRecords: number } }>({ queryKey: ['patients', qs.toString()], queryFn: () => api.get(`/api/v1/patients?${qs.toString()}`), }); const activeFilters = countFilters(filters); const records = data?.data ?? EMPTY; const total = data?.meta?.totalRecords ?? 0; const editHref = (r: PatientRecord) => `/admin/patients/${r.uuid}/edit`; const viewHref = (r: PatientRecord) => `/admin/patients/${r.uuid}`; // نمایش جدولی/کارتی — دو دکمه با آیکون SVG سفارشی (عین tauri ViewModeToggle). const viewToggle = (
); return (
{/* Head — تیتر تنها (tauri files/head) */}

پرونده‌ها

{/* Inputs — ردیف کنترل‌ها (tauri files/inputs) */}
{/* جستجو + سوییچ نما */}
{ setSearch(e.target.value); setPage(1); }} placeholder="کد ملی، شماره پرونده یا نام مراجعه کننده را وارد کنید..." className="text-[var(--text)]" style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, textAlign: 'left' }} />
{viewToggle}
{/* فیلتر + تشکیل پرونده */}
{canCreate && ( )}
{isLoading ? (
در حال بارگذاری...
) : error ? ( /* «دسترسی ندارید» با «بیماری یافت نشد» یکی نیست — پیام سرور را نشان بده. */
{(error as any)?.message || 'دسترسی به پرونده‌ها امکان‌پذیر نیست'}
اگر به‌تازگی محیط کاری‌تان تغییر کرده، از منوی بالا محیط درست را انتخاب کنید.
) : records.length === 0 ? (
بیماری یافت نشد
) : view === 'table' ? (
{['ردیف', 'مراجعه کننده', 'برچسب ها', 'شماره پرونده', 'شماره تماس', 'کد ملی', 'عملیات'].map((h) => ( ))} {records.map((r, i) => ( ))}
{h}
{formatNumber((page - 1) * pageSize + i + 1)} {r.user_name || '—'} {!r.user_national_code && } {r.record_number || '—'} {r.user_mobile || '—'} {r.user_national_code || '—'} {canUpdate && ( )}
) : (
{records.map((r) => ( navigate(viewHref(r))} onEdit={() => navigate(editHref(r))} canUpdate={canUpdate} /> ))}
)}
setFilterOpen(false)} onApply={(f) => { setFilters(f); setPage(1); setFilterOpen(false); }} />
); }