import { useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate, Link } from 'react-router'; import { ChevronRightIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { PatientRecord } from '../types'; import PersianDateInput from '../components/ui/PersianDateInput'; import SearchableSelect from '../components/ui/SearchableSelect'; import { numericField } from '../lib/forms'; import BackButton from '../components/ui/BackButton'; import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils'; import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings'; const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر']; const schema = z.object({ name: z.string().min(1, 'نام و نام خانوادگی الزامی است'), // الزامی‌بودنش شرطی است: با الگوی فعال، سرور شماره را می‌سازد و این فیلد اصلاً // فرستاده نمی‌شود. اعتبارسنجیِ شرطی در `superRefine` پایین‌تر است. record_number: z.string(), gender: z.enum(['male', 'female'], { errorMap: () => ({ message: 'جنسیت را انتخاب کنید' }) }), national_code: iranNationalCodeSchema, mobile: iranMobileSchema, birth_date: z.string().optional(), referral_source: z.string().optional(), description: z.string().optional(), }); type Form = z.infer; const toEpoch = (iso?: string) => (iso ? Math.floor(new Date(iso).getTime() / 1000) : null); const fromEpoch = (ts?: number | null) => unixToIso(ts); /** تشکیل/ویرایش پرونده — patient record create & edit form (Figma "تشکیل پرونده"). */ export default function PatientRecordFormPage() { const { uuid } = useParams<{ uuid: string }>(); const isEdit = !!uuid; const navigate = useNavigate(); const qc = useQueryClient(); const { settings } = useRecordNumberSettings(); /** الگو روشن است ⇒ شماره را سرور می‌سازد. */ const autoNumbered = !!settings?.enabled; /** ورود دستی: وقتی الگویی نیست، یا کاربر صاحب مجموعه است (سرور هم همین را می‌سنجد). */ const manualAllowed = !autoNumbered || !!settings?.can_edit; const formSchema = useMemo( () => schema.superRefine((v, ctx) => { // فقط وقتی شماره دستی است الزامی می‌ماند؛ وگرنه کاربر با فیلدی که نمی‌تواند // پرش کند پشت فرمِ قفل‌شده می‌ماند. if (!autoNumbered && !(v.record_number ?? '').trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['record_number'], message: 'شماره پرونده الزامی است' }); } }), [autoNumbered], ); const form = useForm
({ resolver: zodResolver(formSchema), defaultValues: { name: '', record_number: '', gender: undefined as any, national_code: '', mobile: '', birth_date: '', referral_source: '', description: '' }, }); const { data: recordData } = useQuery>({ queryKey: ['patient', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}`), enabled: isEdit, }); useEffect(() => { const r = recordData?.data; if (!r) return; const p: any = r.profile ?? {}; form.reset({ name: r.user_name ?? '', record_number: r.record_number ?? '', gender: (p.gender as 'male' | 'female') ?? undefined, national_code: r.user_national_code ?? '', mobile: r.user_mobile ?? '', birth_date: fromEpoch(p.date_of_birth), referral_source: p.referral_source ?? '', description: p.description ?? '', }); }, [recordData]); // eslint-disable-line react-hooks/exhaustive-deps const save = useMutation({ mutationFn: async (d: Form) => { const profilePayload = { gender: d.gender, date_of_birth: toEpoch(d.birth_date), referral_source: d.referral_source || null, description: d.description || null, }; // شمارهٔ پرونده فقط وقتی فرستاده می‌شود که ورود دستی مجاز باشد؛ وگرنه سرور // ۴۰۳ می‌دهد و سرورست که شماره را از الگو می‌سازد. const numberPayload = manualAllowed ? { record_number: d.record_number ?? '' } : {}; if (isEdit) { return api.patch(`/api/v1/patient/${uuid}`, { name: d.name, national_code: d.national_code, mobile: d.mobile, ...numberPayload, ...profilePayload, }); } // create: POST creates the record + identity, then PATCH applies the profile demographics const created = await api.post>('/api/v1/patient', { name: d.name, mobile: d.mobile, national_code: d.national_code, ...numberPayload, }); const newUuid = (created as any)?.data?.uuid; if (newUuid) await api.patch(`/api/v1/patient/${newUuid}`, profilePayload); return created; }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['patients'] }); toast.success(isEdit ? 'پرونده ویرایش شد' : 'پرونده تشکیل شد'); navigate('/admin/patients'); }, onError: (e: any) => toast.error(e.message), }); const Field = ({ label, required, error, children }: { label: string; required?: boolean; error?: string; children: React.ReactNode }) => (
{children} {error && {error}}
); return (
پرونده › {isEdit ? 'ویرایش پرونده' : 'تشکیل پرونده'}
save.mutate(d))} style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 24 }}>
{manualAllowed ? (
) : (
)} {autoNumbered && ( {manualAllowed ? `خالی بماند تا از الگو ساخته شود (شمارهٔ بعدی: ${settings?.next_preview ?? '—'})` : 'شماره از الگوی مجموعه ساخته می‌شود'} )}
form.setValue('gender', v as Form['gender'], { shouldValidate: true, shouldDirty: true })} placeholder="انتخاب..." height={38} />
form.setValue('birth_date', v)} enableYearPicker /> ({ value: o, label: o }))} value={form.watch('referral_source') || null} onChange={(v) => form.setValue('referral_source', v ? String(v) : '', { shouldDirty: true })} placeholder="انتخاب کنید..." isClearable height={38} />