feat: enhance staff management and payment gateway features
- Fix national code handling in staff creation and updates to support Persian digits. - Update ClinicStaff entity to allow longer national codes (up to 15 characters). - Implement support for clinic secretaries in SecretaryController, allowing creation without a doctor UUID. - Add a new endpoint to retrieve doctors associated with a clinic for secretary management. - Improve appointment management by ensuring doctors are selectable even when no appointments exist. - Extend PatientController to allow secretaries to create patient records if they have the appropriate permissions. - Introduce a PriceInput component for better price formatting in forms, supporting Persian digits. - Add a MockGateway for testing payment processes without real transactions. - Enhance SMS settings management with an approval flow for post-visit text messages, including new fields for pending text and status. - Update migrations to reflect changes in database schema for national codes and SMS settings.
This commit is contained in:
@@ -134,35 +134,64 @@ const createSchema = z.object({
|
||||
});
|
||||
type CreateForm = z.infer<typeof createSchema>;
|
||||
|
||||
interface ClinicDoctor { uuid: string; name: string; }
|
||||
|
||||
// ── Main component ─────────────────────────────────────────────────────────
|
||||
|
||||
export default function MySecretariesPage() {
|
||||
const qc = useQueryClient();
|
||||
const { doctorUuid } = useAuthStore();
|
||||
const { doctorUuid, dbUuid, primaryRole } = useAuthStore();
|
||||
|
||||
const isClinic = primaryRole === 'clinic';
|
||||
|
||||
// for clinic: selected doctor to add secretary for
|
||||
const [selectedDoctorUuid, setSelectedDoctorUuid] = useState<string>('');
|
||||
|
||||
const activeDoctorUuid = isClinic ? selectedDoctorUuid : (doctorUuid ?? '');
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editTarget, setEditTarget] = useState<Secretary | null>(null);
|
||||
const [editPerms, setEditPerms] = useState<SecretaryPermissions>(DEFAULT_PERMISSIONS);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Secretary | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery<ApiResponse<Secretary[]>>({
|
||||
queryKey: ['my-secretaries', doctorUuid],
|
||||
queryFn: () => api.get(`/api/v1/secretaries/${doctorUuid}`),
|
||||
enabled: !!doctorUuid,
|
||||
// clinic: load clinic's doctors
|
||||
const { data: clinicDoctorsData, isLoading: clinicDoctorsLoading } = useQuery<ApiResponse<{ data: ClinicDoctor[] }>>({
|
||||
queryKey: ['clinic-doctors', dbUuid],
|
||||
queryFn: () => api.get(`/api/v1/clinic/doctor-list/${dbUuid}`),
|
||||
enabled: isClinic && !!dbUuid,
|
||||
});
|
||||
const clinicDoctors: ClinicDoctor[] = clinicDoctorsData?.data?.data ?? [];
|
||||
|
||||
// clinic: load ALL secretaries across all its doctors
|
||||
const { data: clinicSecrData, isLoading: clinicSecrLoading } = useQuery<ApiResponse<Secretary[]>>({
|
||||
queryKey: ['my-secretaries-clinic', dbUuid],
|
||||
queryFn: () => api.get(`/api/v1/secretaries/clinic/${dbUuid}`),
|
||||
enabled: isClinic && !!dbUuid,
|
||||
});
|
||||
|
||||
const secretaries = data?.data ?? [];
|
||||
// doctor: load secretaries for the doctor
|
||||
const { data: doctorSecrData, isLoading: doctorSecrLoading } = useQuery<ApiResponse<Secretary[]>>({
|
||||
queryKey: ['my-secretaries', activeDoctorUuid],
|
||||
queryFn: () => api.get(`/api/v1/secretaries/${activeDoctorUuid}`),
|
||||
enabled: !isClinic && !!activeDoctorUuid,
|
||||
});
|
||||
|
||||
const secretaries = isClinic
|
||||
? (clinicSecrData?.data ?? [])
|
||||
: (doctorSecrData?.data ?? []);
|
||||
const isLoading = isClinic ? clinicSecrLoading : doctorSecrLoading;
|
||||
|
||||
const createForm = useForm<CreateForm>({ resolver: zodResolver(createSchema) });
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (body: CreateForm) =>
|
||||
api.post('/api/v1/secretary', { ...body, doctor_uuid: doctorUuid }),
|
||||
api.post('/api/v1/secretary', { ...body, doctor_uuid: activeDoctorUuid }),
|
||||
onSuccess: () => {
|
||||
toast.success('منشی اضافه شد');
|
||||
setCreateOpen(false);
|
||||
createForm.reset();
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries'] });
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries-clinic'] });
|
||||
},
|
||||
onError: (e: any) => toast.error(e.message),
|
||||
});
|
||||
@@ -174,6 +203,7 @@ export default function MySecretariesPage() {
|
||||
toast.success('دسترسیها بروزرسانی شد');
|
||||
setEditTarget(null);
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries'] });
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries-clinic'] });
|
||||
},
|
||||
onError: (e: any) => toast.error(e.message),
|
||||
});
|
||||
@@ -184,6 +214,7 @@ export default function MySecretariesPage() {
|
||||
toast.success('منشی غیرفعال شد');
|
||||
setDeleteTarget(null);
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries'] });
|
||||
qc.invalidateQueries({ queryKey: ['my-secretaries-clinic'] });
|
||||
},
|
||||
onError: (e: any) => toast.error(e.message),
|
||||
});
|
||||
@@ -193,7 +224,28 @@ export default function MySecretariesPage() {
|
||||
setEditPerms(s.permissions ?? DEFAULT_PERMISSIONS);
|
||||
};
|
||||
|
||||
const columns: Column<Secretary>[] = [
|
||||
const handleCreateOpen = () => {
|
||||
if (isClinic && !selectedDoctorUuid) {
|
||||
toast.error('ابتدا یک پزشک را انتخاب کنید');
|
||||
return;
|
||||
}
|
||||
setCreateOpen(true);
|
||||
};
|
||||
|
||||
const selectedDoctorName = clinicDoctors.find(d => d.uuid === selectedDoctorUuid)?.name ?? '';
|
||||
|
||||
// for clinic: show doctor column in the table
|
||||
const clinicColumns: Column<Secretary>[] = isClinic ? [
|
||||
{
|
||||
key: 'doctor_name',
|
||||
header: 'پزشک',
|
||||
render: (s) => (
|
||||
<span style={{ fontSize: 13, color: 'var(--text-2)', fontWeight: 500 }}>{s.doctor_name}</span>
|
||||
),
|
||||
},
|
||||
] : [];
|
||||
|
||||
const allColumns: Column<Secretary>[] = [
|
||||
{
|
||||
key: 'user_name',
|
||||
header: 'منشی',
|
||||
@@ -212,6 +264,7 @@ export default function MySecretariesPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...clinicColumns,
|
||||
{
|
||||
key: 'mobile_number',
|
||||
header: 'موبایل',
|
||||
@@ -256,14 +309,39 @@ export default function MySecretariesPage() {
|
||||
title="منشیان من"
|
||||
description="مدیریت منشیان و دسترسیهای آنها"
|
||||
action={
|
||||
<button className="btn primary sm" onClick={() => setCreateOpen(true)}>
|
||||
<button className="btn primary sm" onClick={handleCreateOpen} disabled={isClinic && !selectedDoctorUuid}>
|
||||
<PlusIcon style={{ width: 16 }} />
|
||||
افزودن منشی
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{!doctorUuid ? (
|
||||
{/* کلینیک: انتخاب پزشک برای افزودن منشی */}
|
||||
{isClinic && (
|
||||
<div className="card card-pad" style={{ marginBottom: 16 }}>
|
||||
<div className="field" style={{ marginBottom: 0 }}>
|
||||
<label>پزشک مورد نظر برای افزودن منشی جدید</label>
|
||||
{clinicDoctorsLoading ? (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-3)' }}>در حال بارگذاری...</p>
|
||||
) : clinicDoctors.length === 0 ? (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-3)' }}>هیچ پزشکی در این کلینیک تعریف نشده است. ابتدا پزشک اضافه کنید.</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedDoctorUuid}
|
||||
onChange={(e) => setSelectedDoctorUuid(e.target.value)}
|
||||
style={{ width: '100%', maxWidth: 360 }}
|
||||
>
|
||||
<option value="">— یک پزشک را انتخاب کنید —</option>
|
||||
{clinicDoctors.map((d) => (
|
||||
<option key={d.uuid} value={d.uuid}>{d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isClinic && !doctorUuid ? (
|
||||
<div className="card card-pad" style={{ textAlign: 'center', color: 'var(--text-3)' }}>
|
||||
پروفایل پزشک یافت نشد
|
||||
</div>
|
||||
@@ -274,20 +352,29 @@ export default function MySecretariesPage() {
|
||||
<IdentificationIcon style={{ width: 48, color: 'var(--text-3)', margin: '0 auto 16px', display: 'block', opacity: 0.4 }} />
|
||||
<div style={{ fontWeight: 600, fontSize: 15, marginBottom: 8, color: 'var(--text-2)' }}>هنوز منشیای اضافه نشده</div>
|
||||
<div style={{ color: 'var(--text-3)', fontSize: 13.5, marginBottom: 20 }}>
|
||||
منشی میتواند نوبتها و اطلاعات کلینیک را مدیریت کند
|
||||
{isClinic
|
||||
? 'برای افزودن منشی، ابتدا یک پزشک را از لیست بالا انتخاب کنید'
|
||||
: 'منشی میتواند نوبتها و اطلاعات کلینیک را مدیریت کند'}
|
||||
</div>
|
||||
<button className="btn primary sm" onClick={() => setCreateOpen(true)}>
|
||||
<PlusIcon style={{ width: 16 }} /> افزودن اولین منشی
|
||||
</button>
|
||||
{!isClinic && (
|
||||
<button className="btn primary sm" onClick={handleCreateOpen}>
|
||||
<PlusIcon style={{ width: 16 }} /> افزودن اولین منشی
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable columns={columns} data={secretaries} loading={isLoading} emptyMessage="منشیای ثبت نشده است" />
|
||||
<DataTable columns={allColumns} data={secretaries} loading={isLoading} emptyMessage="منشیای ثبت نشده است" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal افزودن منشی */}
|
||||
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="افزودن منشی جدید">
|
||||
{isClinic && selectedDoctorName && (
|
||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: 'var(--primary-subtle)', borderRadius: 8, fontSize: 13, color: 'var(--primary)' }}>
|
||||
منشی برای دکتر {selectedDoctorName} اضافه میشود
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
||||
<div className="field">
|
||||
<label>شماره موبایل *</label>
|
||||
|
||||
Reference in New Issue
Block a user