feat(patient): implement record number pattern management
- Add RecordNumberSettingsController for managing patient record number patterns. - Create RecordNumberPattern entity to represent the pattern configuration. - Implement RecordNumberPatternRepository for database interactions. - Develop RecordNumberGenerator service for generating and validating record numbers. - Add tests for record number generation, backfilling, and API interactions. - Ensure proper access control for viewing and updating patterns based on user roles.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
@@ -14,12 +14,15 @@ 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, 'نام و نام خانوادگی الزامی است'),
|
||||
record_number: z.string().min(1, 'شماره پرونده الزامی است'),
|
||||
// الزامیبودنش شرطی است: با الگوی فعال، سرور شماره را میسازد و این فیلد اصلاً
|
||||
// فرستاده نمیشود. اعتبارسنجیِ شرطی در `superRefine` پایینتر است.
|
||||
record_number: z.string(),
|
||||
gender: z.enum(['male', 'female'], { errorMap: () => ({ message: 'جنسیت را انتخاب کنید' }) }),
|
||||
national_code: iranNationalCodeSchema,
|
||||
mobile: iranMobileSchema,
|
||||
@@ -39,8 +42,25 @@ export default function PatientRecordFormPage() {
|
||||
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<Form>({
|
||||
resolver: zodResolver(schema),
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { name: '', record_number: '', gender: undefined as any, national_code: '', mobile: '', birth_date: '', referral_source: '', description: '' },
|
||||
});
|
||||
|
||||
@@ -74,14 +94,18 @@ export default function PatientRecordFormPage() {
|
||||
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, record_number: d.record_number, ...profilePayload,
|
||||
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<ApiResponse<PatientRecord>>('/api/v1/patient', {
|
||||
name: d.name, mobile: d.mobile, national_code: d.national_code, record_number: d.record_number,
|
||||
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);
|
||||
@@ -116,11 +140,34 @@ export default function PatientRecordFormPage() {
|
||||
<Field label="نام و نام خانوادگی مراجعه کننده" required error={form.formState.errors.name?.message}>
|
||||
<div className="field"><input {...form.register('name')} placeholder="نام و نام خانوادگی را وارد نمایید" /></div>
|
||||
</Field>
|
||||
<Field label="شماره پرونده" required error={form.formState.errors.record_number?.message}>
|
||||
<div className="field"><input {...form.register('record_number')} placeholder="شماره پرونده" /></div>
|
||||
<Field
|
||||
label="شماره پرونده"
|
||||
required={!autoNumbered}
|
||||
error={form.formState.errors.record_number?.message}
|
||||
>
|
||||
{manualAllowed ? (
|
||||
<div className="field">
|
||||
<input
|
||||
{...form.register('record_number')}
|
||||
placeholder={autoNumbered ? (settings?.next_preview ?? 'شماره پرونده') : 'شماره پرونده'}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field" style={{ opacity: 0.7 }}>
|
||||
<input value={isEdit ? (recordData?.data?.record_number ?? '') : (settings?.next_preview ?? '')} readOnly dir="ltr" />
|
||||
</div>
|
||||
)}
|
||||
{autoNumbered && (
|
||||
<span style={{ fontSize: 11.5, color: 'var(--text-3)', display: 'block', marginTop: 4 }}>
|
||||
{manualAllowed
|
||||
? `خالی بماند تا از الگو ساخته شود (شمارهٔ بعدی: ${settings?.next_preview ?? '—'})`
|
||||
: 'شماره از الگوی مجموعه ساخته میشود'}
|
||||
</span>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="جنسیت" required error={form.formState.errors.gender?.message}>
|
||||
<SearchableSelect
|
||||
inputId="patient-gender-select"
|
||||
options={[{ value: 'female', label: 'زن' }, { value: 'male', label: 'مرد' }]}
|
||||
value={form.watch('gender') ?? null}
|
||||
onChange={(v) => form.setValue('gender', v as Form['gender'], { shouldValidate: true, shouldDirty: true })}
|
||||
|
||||
Reference in New Issue
Block a user