diff --git a/assets/admin/components/appointments/NewAppointmentModal.tsx b/assets/admin/components/appointments/NewAppointmentModal.tsx index 0c9bd041..201d2f5e 100644 --- a/assets/admin/components/appointments/NewAppointmentModal.tsx +++ b/assets/admin/components/appointments/NewAppointmentModal.tsx @@ -41,7 +41,7 @@ export interface BookingResource { */ export default function NewAppointmentModal({ slot, onClose, onSuccess, serviceMode = false, services = [], date, clinicUuid = null, resource = null, - treatmentSessionUuid = null, + treatmentSessionUuid = null, patient = null, }: { slot: BookingSlot; onClose: () => void; @@ -57,11 +57,20 @@ export default function NewAppointmentModal({ * اتصال از روی سرویس حدس زده می‌شود و سرویسِ اشتباه یک پروندهٔ موازی می‌سازد. */ treatmentSessionUuid?: string | null; + /** + * بیمارِ از پیش معلوم — وقتی مودال از پروندهٔ خودِ بیمار باز می‌شود. + * + * مرحلهٔ جستجو رد می‌شود: کسی که پروندهٔ بیمار را باز کرده نباید همان بیمار را + * دوباره با کد ملی پیدا کند. + */ + patient?: { name: string | null; mobile: string; national_code: string | null } | null; }) { - const [mobile, setMobile] = useState(''); - const [lookup, setLookup] = useState(null); - const [patientName, setPatientName] = useState(''); - const [nationalCode, setNationalCode] = useState(''); + const [mobile, setMobile] = useState(patient?.mobile ?? ''); + const [lookup, setLookup] = useState( + patient === null ? null : { found: true, name: patient.name, mobile: patient.mobile, national_code: patient.national_code }, + ); + const [patientName, setPatientName] = useState(patient?.name ?? ''); + const [nationalCode, setNationalCode] = useState(patient?.national_code ?? ''); // معیار جستجوی بیمار: کد ملی (پیش‌فرض) یا موبایل. const [searchBy, setSearchBy] = useState<'mobile' | 'national'>('national'); const [pick, setPick] = useState({ serviceUuids: [], durations: {}, slot: null }); @@ -329,7 +338,23 @@ export default function NewAppointmentModal({ )} -
+ {/* بیمارِ از پیش معلوم: فقط تأیید می‌شود، جستجو لازم نیست. */} + {patient !== null && ( +
+ +
+
{patient.name || 'بدون نام'}
+
+ {patient.mobile}{patient.national_code ? ` · ${patient.national_code}` : ''} +
+
+
+ )} + +
{/* انتخاب معیار جستجو: موبایل یا کد ملی */}
@@ -391,7 +416,8 @@ export default function NewAppointmentModal({
- {foundWithNationalCode && ( + {/* وقتی بیمار از قبل معلوم است، کارت بالا همین را می‌گوید؛ دو بار گفتنش نویز است. */} + {patient === null && foundWithNationalCode && (
- {cases.map((c) => )} +
+ + setTerm(e.target.value)} + placeholder="سرویس، پرسنل، وضعیت یا شمارهٔ جلسه" + aria-label="جستجوی جلسات" + /> + {term !== '' && ( + + )} +
+ + {cases.map((c) => )}
); } -function CasePlan({ summary }: { summary: TreatmentCaseSummary }) { +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'); @@ -108,7 +155,12 @@ function CasePlan({ summary }: { summary: TreatmentCaseSummary }) { staleTime: 30_000, }); - const sessions = data?.data?.sessions ?? []; + 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 (
@@ -150,11 +202,24 @@ function CasePlan({ summary }: { summary: TreatmentCaseSummary }) {
) : ( -
- {sessions.map((s) => ( - - ))} -
+ sessions.length === 0 ? ( +
+ جلسه‌ای با «{term}» در این دوره پیدا نشد. +
+ ) : ( +
+ {paged.map((s) => ( + + ))} + +
+ ) )}
)} @@ -162,10 +227,11 @@ function CasePlan({ summary }: { summary: TreatmentCaseSummary }) { ); } -function SessionCard({ session: s, summary, resource }: { +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(); @@ -254,6 +320,11 @@ function SessionCard({ session: s, summary, resource }: { date={s.planned_at === null ? undefined : isoDay(s.planned_at)} clinicUuid={clinicUuid} treatmentSessionUuid={s.uuid} + patient={{ + name: summary.patient.name, + mobile: summary.patient.mobile, + national_code: nationalCode, + }} onClose={() => setBooking(false)} onSuccess={() => { qc.invalidateQueries({ queryKey: ['treatment-case-plan', summary.uuid] }); diff --git a/assets/admin/pages/PatientDetailPage.tsx b/assets/admin/pages/PatientDetailPage.tsx index e62d599b..2b4ca8b9 100644 --- a/assets/admin/pages/PatientDetailPage.tsx +++ b/assets/admin/pages/PatientDetailPage.tsx @@ -49,7 +49,7 @@ const TABS: { key: TabKey; label: string; icon: (c: string) => React.ReactNode } { key: 'services', label: 'سرویس‌ها', icon: (c) => }, { key: 'info', label: 'اطلاعات پرونده', icon: (c) => }, { key: 'appointments', label: 'نوبت‌ها', icon: (c) => }, - { key: 'treatment', label: 'نوبت‌های بعدی', icon: (c) => }, + { key: 'treatment', label: 'دوره‌های درمان', icon: (c) => }, { key: 'payments', label: 'پرداخت‌ها', icon: (c) => }, { key: 'wallet', label: 'کیف پول', icon: (c) => }, { key: 'notes', label: 'یادداشت‌ها', icon: (c) => }, diff --git a/docs/api/treatment.md b/docs/api/treatment.md index f86873cf..3c796bc3 100644 --- a/docs/api/treatment.md +++ b/docs/api/treatment.md @@ -296,6 +296,10 @@ single-session again. Idempotent: deleting a service that has no protocol still | `is_estimate` | `false` یعنی به واقعیتی گره خورده، `true` یعنی محاسبهٔ لحظهٔ نمایش | | `areas[]` | نواحی با `parameters` (خوانده‌های دستگاه)، `resource`، `note`، زمان‌ها | +`case.patient_national_code` هم می‌آید: از پروفایل و در نبودش از کاربر (همان COALESCE +که `PatientController` می‌کند). فرم ثبت نوبت با آن بیمار را از پیش پر می‌کند تا منشی +کسی را که همین‌جا معلوم است دوباره جستجو نکند. + پاسخ یک `resource` هم دارد: دستگاهی که جلسهٔ قبلِ همین دوره رویش انجام شده (`NextSessionSlotFinder::preferredResource`). فرم ثبت نوبت بدونش کار نمی‌کند — سرویسِ دوره روی تقویم منبع رزرو می‌شود نه روی برنامهٔ پزشک. `null` یعنی هنوز هیچ جلسه‌ای روی diff --git a/src/Treatment/Controller/TreatmentCaseController.php b/src/Treatment/Controller/TreatmentCaseController.php index f116d641..78352b51 100644 --- a/src/Treatment/Controller/TreatmentCaseController.php +++ b/src/Treatment/Controller/TreatmentCaseController.php @@ -39,6 +39,7 @@ class TreatmentCaseController extends BaseController private readonly TreatmentCaseOpener $opener, private readonly TreatmentCaseEditor $editor, private readonly TreatmentPlanProjector $planner, + private readonly \App\UserProfile\Repository\UserProfileRepository $profiles, ) {} #[Route('/api/v1/treatment-cases', name: 'treatment_case_list', methods: ['GET'])] @@ -166,8 +167,19 @@ class TreatmentCaseController extends BaseController */ $resource = $this->slotFinder->preferredResource($case); + /** + * کد ملی برای پیش‌پرکردنِ فرم ثبت نوبت لازم است و روی پروفایل می‌نشیند، نه + * روی کاربر — همان COALESCE که `PatientController` هم می‌کند. بدونش منشی + * بیماری را دوباره جستجو می‌کند که همین‌جا معلوم است کیست. + */ + $patientUser = $case->getPatientRecord()->getUser(); + $nationalCode = $this->profiles->findOneBy(['user' => $patientUser])?->getNationalCode() + ?? $patientUser->getNationalCode(); + return $this->success([ - 'case' => $case->toArray(), + 'case' => $case->toArray() + [ + 'patient_national_code' => $nationalCode, + ], 'resource' => $resource === null ? null : [ 'uuid' => $resource->getUuid(), 'name' => $resource->getName(),