Files
clinicpro/assets/admin/pages/PreRegistrationsPage.tsx
T

242 lines
10 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 Portal from '../components/ui/Portal';
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' }}>
<div className="table-wrap"><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>
</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 && (
<Portal>
<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>
</Portal>
)}
</div>
);
}