- Implemented ClinicFormPage for adding new clinics with validation. - Created MyFinancialPage to display financial summaries and charts. - Developed MyPatientsPage for managing patient data with search and pagination. - Added PreRegistrationsPage for handling pre-registration requests with approval and rejection functionalities. - Introduced database migration for pre_registrations table. - Built PreRegistrationController for managing pre-registration logic, including submission, approval, and rejection. - Created PreRegistration entity and repository for handling pre-registration data.
239 lines
9.9 KiB
TypeScript
239 lines
9.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { CheckIcon, XMarkIcon, PhoneIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { PaginatedResponse } from '../lib/api';
|
|
import { formatDate } from '../lib/utils';
|
|
import PageHeader from '../components/ui/PageHeader';
|
|
import Pagination from '../components/ui/Pagination';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
|
|
interface PreRegistration {
|
|
uuid: string;
|
|
type: 'independent_doctor' | 'doctor_with_clinic' | 'clinic_manager';
|
|
name: string;
|
|
mobile: string;
|
|
info: string | null;
|
|
status: 'pending' | 'approved' | 'rejected';
|
|
admin_note: string | null;
|
|
created_at: number;
|
|
}
|
|
|
|
const EMPTY: PreRegistration[] = [];
|
|
|
|
const TYPE_META: Record<string, { label: string; color: string }> = {
|
|
independent_doctor: { label: 'دکتر', color: 'blue' },
|
|
doctor_with_clinic: { label: 'دکتر + کلینیک', color: 'purple' },
|
|
clinic_manager: { label: 'مدیر کلینیک', color: 'orange' },
|
|
};
|
|
|
|
const STATUS_META: Record<string, { label: string; color: string }> = {
|
|
pending: { label: 'در انتظار', color: 'yellow' },
|
|
approved: { label: 'تأیید شده', color: 'green' },
|
|
rejected: { label: 'رد شده', color: 'red' },
|
|
};
|
|
|
|
const STATUS_TABS = [
|
|
{ value: 'pending', label: 'در انتظار' },
|
|
{ value: 'approved', label: 'تأیید شده' },
|
|
{ value: 'rejected', label: 'رد شده' },
|
|
{ value: 'all', label: 'همه' },
|
|
];
|
|
|
|
export default function PreRegistrationsPage() {
|
|
const qc = useQueryClient();
|
|
const [page, setPage] = useState(1);
|
|
const [statusFilter, setStatusFilter] = useState('pending');
|
|
const [approveTarget, setApproveTarget] = useState<PreRegistration | null>(null);
|
|
const [rejectTarget, setRejectTarget] = useState<PreRegistration | null>(null);
|
|
const [rejectNote, setRejectNote] = useState('');
|
|
const limit = 20;
|
|
|
|
const { data, isLoading } = useQuery<PaginatedResponse<PreRegistration>>({
|
|
queryKey: ['pre-registrations', page, statusFilter],
|
|
queryFn: () => api.get(`/api/v1/admin/pre-registrations?page=${page}&limit=${limit}&status=${statusFilter}`),
|
|
});
|
|
|
|
const items = data?.data ?? EMPTY;
|
|
const total = data?.meta?.totalRecords ?? 0;
|
|
|
|
const approveMutation = useMutation({
|
|
mutationFn: (uuid: string) => api.post(`/api/v1/admin/pre-registrations/${uuid}/approve`, {}),
|
|
onSuccess: () => {
|
|
toast.success('درخواست تأیید شد و اطلاعات ورود ارسال گردید');
|
|
setApproveTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['pre-registrations'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const rejectMutation = useMutation({
|
|
mutationFn: ({ uuid, note }: { uuid: string; note: string }) =>
|
|
api.post(`/api/v1/admin/pre-registrations/${uuid}/reject`, { note: note || undefined }),
|
|
onSuccess: () => {
|
|
toast.success('درخواست رد شد');
|
|
setRejectTarget(null);
|
|
setRejectNote('');
|
|
qc.invalidateQueries({ queryKey: ['pre-registrations'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
return (
|
|
<div className="page">
|
|
<PageHeader title="درخواستهای ثبتنام" description={`${total} درخواست`} />
|
|
|
|
{/* Tabs */}
|
|
<div className="seg" style={{ marginBottom: 20, width: 'fit-content' }}>
|
|
{STATUS_TABS.map((tab) => (
|
|
<button
|
|
key={tab.value}
|
|
className={statusFilter === tab.value ? 'active' : ''}
|
|
onClick={() => { setStatusFilter(tab.value); setPage(1); }}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
|
<table className="t">
|
|
<thead>
|
|
<tr>
|
|
<th>نام</th>
|
|
<th>موبایل</th>
|
|
<th>نوع حساب</th>
|
|
<th>توضیحات</th>
|
|
<th>وضعیت</th>
|
|
<th>تاریخ درخواست</th>
|
|
{statusFilter === 'pending' && <th />}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{isLoading
|
|
? Array.from({ length: 8 }).map((_, i) => (
|
|
<tr key={i}>
|
|
{Array.from({ length: 6 }).map((_, j) => (
|
|
<td key={j}><div className="skeleton" style={{ height: 16, borderRadius: 6 }} /></td>
|
|
))}
|
|
</tr>
|
|
))
|
|
: items.length === 0
|
|
? (
|
|
<tr>
|
|
<td colSpan={7} style={{ textAlign: 'center', padding: 48, color: 'var(--text-3)' }}>
|
|
درخواستی یافت نشد
|
|
</td>
|
|
</tr>
|
|
)
|
|
: items.map((item) => {
|
|
const typeMeta = TYPE_META[item.type] ?? { label: item.type, color: 'gray' };
|
|
const statusMeta = STATUS_META[item.status] ?? { label: item.status, color: 'gray' };
|
|
return (
|
|
<tr key={item.uuid}>
|
|
<td>
|
|
<div style={{ fontWeight: 600 }}>{item.name}</div>
|
|
</td>
|
|
<td>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, direction: 'ltr' }}>
|
|
<PhoneIcon style={{ width: 13, height: 13, color: 'var(--text-3)' }} />
|
|
<span style={{ fontSize: 13 }}>{item.mobile}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span className={`badge ${typeMeta.color}`}>
|
|
<span className="bdot" />{typeMeta.label}
|
|
</span>
|
|
</td>
|
|
<td style={{ maxWidth: 200 }}>
|
|
<span style={{ fontSize: 12, color: 'var(--text-2)', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
|
{item.info || '—'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span className={`badge ${statusMeta.color}`}>
|
|
<span className="bdot" />{statusMeta.label}
|
|
</span>
|
|
{item.admin_note && (
|
|
<div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 3 }}>{item.admin_note}</div>
|
|
)}
|
|
</td>
|
|
<td style={{ fontSize: 13 }}>{formatDate(String(item.created_at))}</td>
|
|
{statusFilter === 'pending' && (
|
|
<td>
|
|
<div className="row-actions">
|
|
<button
|
|
className="mini-btn"
|
|
title="تأیید"
|
|
style={{ color: 'var(--green)' }}
|
|
onClick={() => setApproveTarget(item)}
|
|
>
|
|
<CheckIcon style={{ width: 16, height: 16 }} />
|
|
</button>
|
|
<button
|
|
className="mini-btn"
|
|
title="رد"
|
|
style={{ color: 'var(--red)' }}
|
|
onClick={() => { setRejectTarget(item); setRejectNote(''); }}
|
|
>
|
|
<XMarkIcon style={{ width: 16, height: 16 }} />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
|
|
|
{/* Approve Dialog */}
|
|
<ConfirmDialog
|
|
open={!!approveTarget}
|
|
title="تأیید درخواست"
|
|
message={`آیا درخواست "${approveTarget?.name}" را تأیید میکنید؟ اطلاعات ورود از طریق SMS ارسال خواهد شد.`}
|
|
confirmLabel="تأیید و ارسال SMS"
|
|
loading={approveMutation.isPending}
|
|
onConfirm={() => approveTarget && approveMutation.mutate(approveTarget.uuid)}
|
|
onCancel={() => setApproveTarget(null)}
|
|
/>
|
|
|
|
{/* Reject Dialog */}
|
|
{rejectTarget && (
|
|
<div className="overlay" onClick={() => setRejectTarget(null)}>
|
|
<div className="modal" style={{ maxWidth: 420 }} onClick={(e) => e.stopPropagation()}>
|
|
<div className="modal-head">
|
|
<b>رد درخواست — {rejectTarget.name}</b>
|
|
</div>
|
|
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-2)' }}>در صورت تمایل دلیل رد را وارد کنید (اختیاری)</p>
|
|
<textarea
|
|
className="field"
|
|
rows={3}
|
|
placeholder="دلیل رد..."
|
|
value={rejectNote}
|
|
onChange={(e) => setRejectNote(e.target.value)}
|
|
style={{ resize: 'vertical' }}
|
|
/>
|
|
</div>
|
|
<div className="modal-foot">
|
|
<button className="btn ghost sm" onClick={() => setRejectTarget(null)}>انصراف</button>
|
|
<button
|
|
className="btn danger sm"
|
|
disabled={rejectMutation.isPending}
|
|
onClick={() => rejectMutation.mutate({ uuid: rejectTarget.uuid, note: rejectNote })}
|
|
>
|
|
{rejectMutation.isPending ? 'در حال رد...' : 'رد درخواست'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|