import { useEffect, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { ChevronDownIcon, ClipboardDocumentListIcon, CpuChipIcon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { api } from '../../lib/api'; import type { ApiResponse } from '../../lib/api'; import StatusBadge from '../ui/StatusBadge'; import Pagination from '../ui/Pagination'; import NewAppointmentModal from '../appointments/NewAppointmentModal'; import { useResourceBookingServices } from '../../hooks/useResourceBookingServices'; import { useClinicContext } from '../../hooks/useClinicContext'; import { formatDateTime, formatNumber } from '../../lib/utils'; import type { TreatmentCaseSummary, SessionAreaRecord } from '../../types'; interface PlanSession { uuid: string; session_number: number; total_sessions: number; status: string; planned_at: number | null; is_estimate: boolean; appointment: { uuid: string; slot_start: number } | null; performed_by: { uuid: string; name: string } | null; areas?: SessionAreaRecord[]; } interface PlanResponse { case: TreatmentCaseSummary & { patient_national_code: string | null }; /** دستگاهِ دوره — همان که جلسهٔ قبل رویش انجام شد. */ resource: { uuid: string; name: string } | null; sessions: PlanSession[]; } const CASE_STATUS_LABEL: Record = { active: 'در جریان', completed: 'تمام شده', abandoned: 'رها شده', }; /** `planned_at` (ثانیه) → `YYYY-MM-DD` میلادی، همان چیزی که مودال می‌خواهد. */ function isoDay(ts: number): string { const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } /** * درمان‌های چندجلسه‌ایِ همین بیمار، با تقویم کل دوره. * * تاریخ‌ها از `/plan` می‌آیند و بخشی‌شان تخمینی‌اند — همان‌جا برچسب می‌خورند تا با * نوبتِ ثبت‌شده اشتباه گرفته نشوند. کارت نوبت نیست؛ دکمه‌اش نوبت می‌سازد. */ export default function PatientTreatmentTab({ recordUuid }: { recordUuid: string }) { const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['patient-treatment-cases', recordUuid], queryFn: () => api.get>( `/api/v1/treatment-cases?record=${encodeURIComponent(recordUuid)}`, ), enabled: recordUuid !== '', staleTime: 30_000, }); // جستجو در همین صفحه فیلتر می‌شود نه روی سرور: `/plan` کل دوره را یک‌جا می‌دهد و // برای چند ده جلسه رفت‌وبرگشت اضافه هیچ چیزی جز تأخیر اضافه نمی‌کند. const [term, setTerm] = useState(''); const cases = data?.data ?? []; if (isLoading) { return (
{[0, 1].map((i) =>
)}
); } if (isError) { return (
خواندن درمان‌های این بیمار ناموفق بود.
); } if (cases.length === 0) { return (
این بیمار درمان چندجلسه‌ای ندارد.
پرونده وقتی ساخته می‌شود که نوبتِ سرویسی با «طول درمان» قطعی شود.
); } return (
setTerm(e.target.value)} placeholder="سرویس، پرسنل، وضعیت یا شمارهٔ جلسه" aria-label="جستجوی جلسات" /> {term !== '' && ( )}
{cases.map((c) => )}
); } const PAGE_SIZE = 8; const SESSION_STATUS_TEXT: Record = { planned: 'برنامه‌ریزی شده', booked: 'زمان‌بندی شده', in_progress: 'در حال انجام', done: 'انجام شد', cancelled: 'لغو شده', no_show: 'غیبت', }; /** جستجو روی همان چیزهایی که در کارت دیده می‌شوند، نه فیلدهای پنهان. */ function matches(s: PlanSession, serviceName: string, term: string): boolean { if (term === '') return true; const hay = [ serviceName, s.performed_by?.name ?? '', SESSION_STATUS_TEXT[s.status] ?? s.status, String(s.session_number), formatNumber(s.session_number), ...(s.areas ?? []).map((a) => a.area.name), ].join(' '); return hay.includes(term); } function CasePlan({ summary, term }: { summary: TreatmentCaseSummary; term: string }) { // دورهٔ باز پیش‌فرض باز است و دورهٔ بسته جمع — کارِ پیشِ رو مهم‌تر از سابقه است. const [open, setOpen] = useState(summary.status === 'active'); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['treatment-case-plan', summary.uuid], queryFn: () => api.get>(`/api/v1/treatment-case/${summary.uuid}/plan`), enabled: open, staleTime: 30_000, }); const [page, setPage] = useState(1); useEffect(() => setPage(1), [term]); const all = data?.data?.sessions ?? []; const sessions = all.filter((s) => matches(s, summary.service.name, term)); const paged = sessions.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return (
{open && (
{isLoading ? (
در حال خواندن تقویم دوره…
) : isError ? (
خواندن تقویم این دوره ناموفق بود.
) : ( sessions.length === 0 ? (
جلسه‌ای با «{term}» در این دوره پیدا نشد.
) : (
{paged.map((s) => ( ))}
) )}
)}
); } function SessionCard({ session: s, summary, resource, nationalCode }: { session: PlanSession; summary: TreatmentCaseSummary; resource: { uuid: string; name: string } | null; nationalCode: string | null; }) { const [booking, setBooking] = useState(false); const qc = useQueryClient(); const clinicUuid = useClinicContext(); const supervisor = summary.supervisor; // سرویس‌ها از تقویم منبع می‌آیند نه از برنامهٔ پزشک — دورهٔ لیزر روی دستگاه رزرو // می‌شود و `useDoctorBookingServices` برای این پزشک چیزی برنمی‌گرداند. const { services } = useResourceBookingServices(booking ? resource?.uuid : null); const settled = ['done', 'cancelled', 'no_show'].includes(s.status); const bookable = !settled && s.appointment === null && summary.status === 'active'; const areas = s.areas ?? []; return (
جلسهٔ {formatNumber(s.session_number)} از {formatNumber(s.total_sessions)} {s.planned_at === null ? '—' : formatDateTime(s.planned_at)} {/* تخمین با واقعیت یکی نیست و باید در خودِ کارت معلوم باشد. */} {s.is_estimate && ( تخمینی )} {s.performed_by && ( پرسنل: {s.performed_by.name} )}
{/* آنچه واقعاً انجام شد: ناحیه، دستگاه و خوانده‌هایش. */} {areas.length > 0 && (
{areas.map((a) => (
{a.area.name} {a.resource && ( {a.resource.name} )} {a.parameters && Object.entries(a.parameters).map(([k, v]) => ( {k}: {String(v)} ))} {a.note && یادداشت: {a.note}}
))}
)} {bookable && ( /* بدون دستگاه، فرم سرویسی برای انتخاب ندارد؛ دکمهٔ خاموشِ بی‌توضیح بدتر از نبودنش است، پس دلیلش نوشته می‌شود. */ resource === null ? ( هنوز هیچ جلسه‌ای از این دوره روی دستگاهی انجام نشده — نوبت را از صفحهٔ نوبت‌ها ثبت کنید. ) : ( ) )} {booking && resource && ( setBooking(false)} onSuccess={() => { qc.invalidateQueries({ queryKey: ['treatment-case-plan', summary.uuid] }); qc.invalidateQueries({ queryKey: ['patient-treatment-cases'] }); }} /> )}
); }