The «اطلاعات پرونده» form only exposed basic insurance («نوع بیمه»). Add a
«بیمه تکمیلی» select next to it, wired to the supplementary insurances from
insurance-pricing. Backend PATCH /api/v1/patient/{uuid} already accepts
supplementary_insurance_id (UserProfile entity + controller) — frontend-only:
schema field, form select, profileToFormValues/formValuesToPayload mapping,
PatientProfileUpdate type, and the supplementary option passed from both the
case-file info tab (PatientDetailPage) and MyPatientsPage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
210 lines
7.5 KiB
TypeScript
210 lines
7.5 KiB
TypeScript
import React from 'react';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import Field from './ui/Field';
|
|
import Input from './ui/Input';
|
|
import MobileInput from './ui/MobileInput';
|
|
import SearchableSelect from './ui/SearchableSelect';
|
|
import type { SelectOption } from './ui/SearchableSelect';
|
|
import PersianDatePicker from './ui/PersianDatePicker';
|
|
|
|
/**
|
|
* اسکیمای فرم «اطلاعات پرونده». مطابق قواعد بکاند:
|
|
* نام الزامی؛ موبایل در صورت پرشدن باید ^09\d{9}$؛ کدملی در صورت پرشدن ۱۰ رقم.
|
|
* `birth_date` رشتهٔ میلادی YYYY-MM-DD است؛ تبدیل به timestamp در لایهٔ صفحه انجام میشود.
|
|
*/
|
|
export const patientFormSchema = z.object({
|
|
name: z.string().trim().min(1, 'نام و نام خانوادگی الزامی است'),
|
|
mobile: z.string().trim().regex(/^09\d{9}$/, 'شماره موبایل نامعتبر است').or(z.literal('')),
|
|
national_code: z.string().trim().regex(/^\d{10}$/, 'کد ملی باید ۱۰ رقم باشد').or(z.literal('')),
|
|
gender: z.string().nullable(),
|
|
birth_date: z.string(),
|
|
fathers_name: z.string(),
|
|
marital_status: z.string().nullable(),
|
|
basic_insurance_id: z.union([z.number(), z.string(), z.null()]),
|
|
supplementary_insurance_id: z.union([z.number(), z.string(), z.null()]),
|
|
field_of_study: z.string(),
|
|
education: z.string().nullable(),
|
|
job: z.string(),
|
|
province_id: z.union([z.number(), z.string(), z.null()]),
|
|
city_id: z.union([z.number(), z.string(), z.null()]),
|
|
home_phone: z.string(),
|
|
address: z.string(),
|
|
postal_code: z.string(),
|
|
referral_source: z.string().nullable(),
|
|
description: z.string(),
|
|
});
|
|
|
|
export type PatientFormValues = z.infer<typeof patientFormSchema>;
|
|
|
|
export interface PatientFormOptions {
|
|
gender: SelectOption[];
|
|
marital: SelectOption[];
|
|
education: SelectOption[];
|
|
insurance: SelectOption[];
|
|
supplementary: SelectOption[];
|
|
province: SelectOption[];
|
|
city: SelectOption[];
|
|
referral: SelectOption[];
|
|
}
|
|
|
|
interface Props {
|
|
defaultValues: PatientFormValues;
|
|
recordNumber?: string | null;
|
|
options: PatientFormOptions;
|
|
onSubmit: (values: PatientFormValues) => void;
|
|
isSubmitting?: boolean;
|
|
/** برای بارگذاری وابستهٔ شهرها هنگام تغییر استان (منطق در صفحه). */
|
|
onProvinceChange?: (provinceId: number | null) => void;
|
|
}
|
|
|
|
const grid: React.CSSProperties = {
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
gap: 'var(--gap)',
|
|
};
|
|
|
|
export default function PatientRecordInfoForm({
|
|
defaultValues,
|
|
recordNumber,
|
|
options,
|
|
onSubmit,
|
|
isSubmitting,
|
|
onProvinceChange,
|
|
}: Props) {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
control,
|
|
formState: { errors },
|
|
} = useForm<PatientFormValues>({
|
|
resolver: zodResolver(patientFormSchema),
|
|
defaultValues,
|
|
});
|
|
|
|
const sel = (name: keyof PatientFormValues, label: string, opts: SelectOption[], placeholder = 'انتخاب کنید...') => (
|
|
<Field label={label} error={errors[name]?.message as string | undefined}>
|
|
<Controller
|
|
name={name}
|
|
control={control}
|
|
render={({ field }) => (
|
|
<SearchableSelect
|
|
options={opts}
|
|
value={field.value as string | number | null}
|
|
onChange={field.onChange}
|
|
placeholder={placeholder}
|
|
/>
|
|
)}
|
|
/>
|
|
</Field>
|
|
);
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)} noValidate>
|
|
<div style={grid}>
|
|
<Field label="نام و نام خانوادگی مراجعه کننده" error={errors.name?.message}>
|
|
<Input {...register('name')} hasError={!!errors.name} placeholder="نام و نام خانوادگی" />
|
|
</Field>
|
|
|
|
<Field label="شماره پرونده">
|
|
<Input value={recordNumber ?? '—'} readOnly dir="ltr" />
|
|
</Field>
|
|
|
|
{sel('gender', 'جنسیت', options.gender)}
|
|
|
|
<Field label="کدملی" error={errors.national_code?.message}>
|
|
<Input {...register('national_code')} hasError={!!errors.national_code} dir="ltr" maxLength={10} placeholder="کد ملی" />
|
|
</Field>
|
|
|
|
<Field label="شماره تماس" error={errors.mobile?.message}>
|
|
<Controller
|
|
name="mobile"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<MobileInput value={field.value} onChange={field.onChange} hasError={!!errors.mobile} />
|
|
)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label="تاریخ تولد" error={errors.birth_date?.message}>
|
|
<Controller
|
|
name="birth_date"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<PersianDatePicker value={field.value} onChange={field.onChange} placeholder="تاریخ تولد" enableYearPicker />
|
|
)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label="نام پدر">
|
|
<Input {...register('fathers_name')} placeholder="نام پدر را وارد نمایید" />
|
|
</Field>
|
|
|
|
{sel('marital_status', 'وضعیت تاهل', options.marital)}
|
|
{sel('basic_insurance_id', 'بیمه پایه', options.insurance)}
|
|
{sel('supplementary_insurance_id', 'بیمه تکمیلی', options.supplementary)}
|
|
|
|
<Field label="رشته تحصیلی">
|
|
<Input {...register('field_of_study')} placeholder="رشته تحصیلی را وارد نمایید" />
|
|
</Field>
|
|
|
|
{sel('education', 'مقطع تحصیلی', options.education)}
|
|
|
|
<Field label="شغل">
|
|
<Input {...register('job')} placeholder="شغل را وارد نمایید" />
|
|
</Field>
|
|
|
|
<Field label="استان" error={errors.province_id?.message as string | undefined}>
|
|
<Controller
|
|
name="province_id"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<SearchableSelect
|
|
options={options.province}
|
|
value={field.value as string | number | null}
|
|
onChange={(v) => { field.onChange(v); onProvinceChange?.(v === null ? null : Number(v)); }}
|
|
placeholder="انتخاب کنید..."
|
|
/>
|
|
)}
|
|
/>
|
|
</Field>
|
|
|
|
{sel('city_id', 'شهر', options.city)}
|
|
|
|
<Field label="تلفن ثابت">
|
|
<Input {...register('home_phone')} dir="ltr" placeholder="تلفن ثابت را وارد نمایید..." />
|
|
</Field>
|
|
|
|
<Field label="آدرس" style={{ gridColumn: 'span 2' }}>
|
|
<Input {...register('address')} placeholder="آدرس محل سکونت را وارد نمایید..." />
|
|
</Field>
|
|
|
|
<Field label="کد پستی">
|
|
<Input {...register('postal_code')} dir="ltr" placeholder="کدپستی محل سکونت را وارد نمایید..." />
|
|
</Field>
|
|
|
|
{sel('referral_source', 'نحوه آشنایی', options.referral)}
|
|
</div>
|
|
|
|
<div style={{ marginTop: 'var(--gap)' }}>
|
|
<Field label="توضیحات">
|
|
<textarea
|
|
{...register('description')}
|
|
className="cp-input"
|
|
rows={4}
|
|
style={{ resize: 'vertical', minHeight: 96 }}
|
|
placeholder="توضیحات"
|
|
/>
|
|
</Field>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 'var(--gap)' }}>
|
|
<button type="submit" className="cp-btn cp-btn-primary" disabled={isSubmitting}>
|
|
{isSubmitting ? 'در حال ثبت...' : 'ثبت اطلاعات'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|