From 9638f363afec0651c4cde321e6a83dbb854143ee Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 16 Jul 2026 10:52:31 +0330 Subject: [PATCH] =?UTF-8?q?feat:=20wire=20editable=20=C2=AB=D8=A7=D8=B7?= =?UTF-8?q?=D9=84=D8=A7=D8=B9=D8=A7=D8=AA=20=D9=BE=D8=B1=D9=88=D9=86=D8=AF?= =?UTF-8?q?=D9=87=C2=BB=20form=20into=20patient=20case-file=20info=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port tauri /files-services FileInfoSection into PatientDetailPage's info tab. The tab previously rendered a stripped read-only InfoRow list; it now renders the inline editable PatientRecordInfoForm (18 fields + ثبت اطلاعات), matching the tauri source. Reuses the existing form component, patientForm helpers, and the already-present patient/provinces/cities/insurance-pricing endpoints (no new API). PATCH /api/v1/patient/{uuid} on submit. Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/admin/pages/PatientDetailPage.test.tsx | 38 ++++++-- assets/admin/pages/PatientDetailPage.tsx | 87 ++++++++++++++----- 2 files changed, 97 insertions(+), 28 deletions(-) diff --git a/assets/admin/pages/PatientDetailPage.test.tsx b/assets/admin/pages/PatientDetailPage.test.tsx index 123151f5..22186b17 100644 --- a/assets/admin/pages/PatientDetailPage.test.tsx +++ b/assets/admin/pages/PatientDetailPage.test.tsx @@ -20,7 +20,16 @@ beforeEach(() => { if (url === '/api/v1/patient/r1') return Promise.resolve({ success: true, data: { uuid: 'r1', user_name: 'ساغر صابری', record_number: 'P-1001', created_at: 1700000000, user_mobile: '09120000000', user_national_code: '1234567890', tags: [], - profile: { gender: 'female', referral_source: 'اینستاگرام', address: 'یزد', description: 'یادداشت' }, + profile: { + name: 'ساغر صابری', national_code: '1234567890', mobile: '09120000000', + gender: 'female', fathers_name: 'رضا', job: 'مهندس', + referral_source: 'اینستاگرام', address: 'یزد', description: 'یادداشت', + }, + } }); + if (url === '/api/v1/provinces') return Promise.resolve({ success: true, data: [{ id: 10, name: 'یزد' }] }); + if (url.startsWith('/api/v1/cities')) return Promise.resolve({ success: true, data: [{ id: 100, name: 'یزد' }] }); + if (url === '/api/v1/insurance-pricing') return Promise.resolve({ success: true, data: { + insurances: [{ insurance_id: 1, insurance_name: 'تأمین اجتماعی', type: 'basic' }], } }); if (url === '/api/v1/patient/r1/sessions') return Promise.resolve({ success: true, data: [ { uuid: 's1', services: [{ service_name: 'اسکیلینگ' }], doctor_name: 'دکتر فتحی', final_price_rials: 2500000, is_paid: false, patient_debt_rials: 1500000, notes: 'یادداشت', created_at: 1700000000, visit_price_rials: 0 }, @@ -88,13 +97,32 @@ describe('PatientDetailPage (پرونده تب‌دار)', () => { expect(await screen.findByText('اطلاعات فاکتور')).toBeInTheDocument(); }); - it('shows patient info on the info tab', async () => { + it('renders the editable «اطلاعات پرونده» form prefilled from the profile', async () => { renderDetail(); await loaded(); fireEvent.click(screen.getByText('اطلاعات پرونده')); - expect(screen.getByText('کد ملی')).toBeInTheDocument(); - expect(screen.getByText('1234567890')).toBeInTheDocument(); - expect(screen.getByText('اینستاگرام')).toBeInTheDocument(); + // لیبل فیلدهای فرم (معادل tauri FileInfoSection) + expect(await screen.findByText('نام و نام خانوادگی مراجعه کننده')).toBeInTheDocument(); + expect(screen.getByText('کدملی')).toBeInTheDocument(); + // مقادیر پیش‌پرشده از پروفایل + expect(screen.getByDisplayValue('1234567890')).toBeInTheDocument(); + expect(screen.getByDisplayValue('ساغر صابری')).toBeInTheDocument(); + expect(screen.getByDisplayValue('P-1001')).toBeInTheDocument(); // شماره پرونده readonly + // دکمهٔ ثبت + expect(screen.getByRole('button', { name: 'ثبت اطلاعات' })).toBeInTheDocument(); + }); + + it('renders the info form with placeholders when the profile is empty', async () => { + get.mockImplementation((url: string) => { + if (url === '/api/v1/patient/r1') return Promise.resolve({ success: true, data: { + uuid: 'r1', user_name: 'بدون پروفایل', record_number: 'P-9', created_at: 1700000000, profile: null, + } }); + return Promise.resolve({ success: true, data: [] }); + }); + renderDetail(); + fireEvent.click(await screen.findByText('اطلاعات پرونده')); + expect(await screen.findByPlaceholderText('نام و نام خانوادگی')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'ثبت اطلاعات' })).toBeInTheDocument(); }); it('renders the call-center tab with a register form and history', async () => { diff --git a/assets/admin/pages/PatientDetailPage.tsx b/assets/admin/pages/PatientDetailPage.tsx index 57c234f9..362cf082 100644 --- a/assets/admin/pages/PatientDetailPage.tsx +++ b/assets/admin/pages/PatientDetailPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useSearchParams, Link } from 'react-router-dom'; import { @@ -25,6 +25,11 @@ import { TurnsFilter, AddTurn } from '../components/icons/FilesToolbarIcons'; import { TabServices, TabInfo, TabCalendar, TabCard, TabWallet, TabSMS, TabCall, TabAttach, TabBody, } from '../components/icons/FilesServiceIcons'; +import PatientRecordInfoForm from '../components/PatientRecordInfoForm'; +import { + profileToFormValues, formValuesToPayload, + GENDER_OPTS, MARITAL_OPTS, EDUCATION_OPTS, REFERRAL_OPTS, +} from '../lib/patientForm'; type TabKey = 'services' | 'info' | 'appointments' | 'payments' | 'wallet' | 'messages' | 'callcenter' | 'attach' | 'records'; @@ -40,8 +45,6 @@ const TABS: { key: TabKey; label: string; icon: (c: string) => React.ReactNode } { key: 'records', label: 'پرونده پزشکی', icon: (c) => }, ]; -const GENDER_LABEL: Record = { male: 'مرد', female: 'زن' }; - const PAYMENT_STATUS: Record = { pending: 'در انتظار', success: 'موفق', failed: 'ناموفق', canceled: 'لغو شده', refunded: 'بازگشت', }; @@ -54,15 +57,6 @@ function Placeholder({ label }: { label: string }) { ); } -function InfoRow({ label, value }: { label: string; value?: string | null }) { - return ( -
- {label} - {value || '—'} -
- ); -} - /** پرونده — the tabbed patient case-file (Figma "جزئیات پرونده"). */ export default function PatientDetailPage() { const { uuid } = useParams<{ uuid: string }>(); @@ -79,11 +73,51 @@ export default function PatientDetailPage() { enabled: !!uuid, }); const record = data?.data; - const profile: any = record?.profile ?? {}; const qc = useQueryClient(); const [invoiceUuid, setInvoiceUuid] = useState(null); + // ── فرم «اطلاعات پرونده» (اینلاین، معادل tauri FileInfoSection) ────────────── + // استان/شهر (Location) و بیمهٔ پایه (insurance-pricing) برای گزینه‌های فرم. + const [editProvinceId, setEditProvinceId] = useState(null); + const seededProvince = useRef(false); + useEffect(() => { + if (!seededProvince.current && record?.profile) { + setEditProvinceId(record.profile.province_id ?? null); + seededProvince.current = true; + } + }, [record]); + + const provincesQ = useQuery({ + queryKey: ['provinces'], + queryFn: () => api.get('/api/v1/provinces'), + staleTime: 600_000, + }); + const citiesQ = useQuery({ + queryKey: ['cities', editProvinceId], + queryFn: () => api.get(`/api/v1/cities${editProvinceId ? `?province_id=${editProvinceId}` : ''}`), + staleTime: 300_000, + }); + const pricingQ = useQuery>({ + queryKey: ['insurance-pricing'], + queryFn: () => api.get('/api/v1/insurance-pricing'), + enabled: !!uuid, + }); + const locOpts = (raw: any) => + (raw?.data?.data ?? raw?.data ?? []).map((x: any) => ({ value: Number(x.id), label: x.name })); + const baseInsuranceOptions = ((pricingQ.data?.data as any)?.insurances ?? []) + .filter((i: any) => i.type === 'basic') + .map((i: any) => ({ value: String(i.insurance_id), label: i.insurance_name })); + + const updateProfileMut = useMutation({ + mutationFn: (body: object) => api.patch(`/api/v1/patient/${uuid}`, body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['patient', uuid] }); + toast.success('اطلاعات بیمار به‌روزرسانی شد'); + }, + onError: (e: any) => toast.error(e?.message || 'خطا در به‌روزرسانی اطلاعات بیمار'), + }); + // sessions + appointments load eagerly so the banner (debt / next visit) is ready. const sessionsQ = useQuery>({ queryKey: ['patient-sessions', uuid], @@ -151,16 +185,23 @@ export default function PatientDetailPage() { {isLoading ? (
در حال بارگذاری...
) : tab === 'info' ? ( -
- - - - - - - - - +
+ updateProfileMut.mutate(formValuesToPayload(v))} + isSubmitting={updateProfileMut.isPending} + onProvinceChange={setEditProvinceId} + />
) : tab === 'services' ? (