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:
@@ -21,7 +21,7 @@ const templateSchema = z.object({
|
||||
});
|
||||
type TemplateFormData = z.infer<typeof templateSchema>;
|
||||
|
||||
type Tab = 'samples' | 'pending' | 'logs';
|
||||
type Tab = 'samples' | 'pending' | 'logs' | 'post-visit-review';
|
||||
|
||||
const STATUS_LOG_META: Record<string, { label: string; cls: string }> = {
|
||||
sent: { label: 'ارسال شده', cls: 'green' },
|
||||
@@ -38,6 +38,8 @@ export default function SmsPage() {
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<SmsTemplate | null>(null);
|
||||
const [postVisitRejectId, setPostVisitRejectId] = useState<number | null>(null);
|
||||
const [postVisitRejectReason, setPostVisitRejectReason] = useState('');
|
||||
const limit = 15;
|
||||
|
||||
const sampleTemplatesQuery = useQuery({
|
||||
@@ -114,6 +116,33 @@ export default function SmsPage() {
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
});
|
||||
|
||||
const postVisitReviewQuery = useQuery<ApiResponse<{ data: Array<{ id: number; entity_type: string; entity_id: number; post_visit_text_pending: string; post_visit_text_status: string }> }>>({
|
||||
queryKey: ['sms-post-visit-review'],
|
||||
queryFn: () => api.get('/api/v1/admin/sms/settings/review'),
|
||||
enabled: activeTab === 'post-visit-review',
|
||||
});
|
||||
|
||||
const postVisitApproveMutation = useMutation({
|
||||
mutationFn: (id: number) => api.post(`/api/v1/admin/sms/settings/${id}/approve`, {}),
|
||||
onSuccess: () => {
|
||||
toast.success('متن پیامک تأیید شد');
|
||||
qc.invalidateQueries({ queryKey: ['sms-post-visit-review'] });
|
||||
},
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
});
|
||||
|
||||
const postVisitRejectMutation = useMutation({
|
||||
mutationFn: ({ id, reason }: { id: number; reason: string }) =>
|
||||
api.post(`/api/v1/admin/sms/settings/${id}/reject`, { reason }),
|
||||
onSuccess: () => {
|
||||
toast.success('متن پیامک رد شد');
|
||||
setPostVisitRejectId(null);
|
||||
setPostVisitRejectReason('');
|
||||
qc.invalidateQueries({ queryKey: ['sms-post-visit-review'] });
|
||||
},
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
});
|
||||
|
||||
const templateColumns: Column<SmsTemplate>[] = [
|
||||
{ key: 'name', header: 'نام', render: (t) => <b>{t.name}</b> },
|
||||
{
|
||||
@@ -153,9 +182,12 @@ export default function SmsPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const postVisitPendingCount = (postVisitReviewQuery.data?.data as any)?.data?.length ?? 0;
|
||||
|
||||
const TABS: { key: Tab; label: string; badge?: number }[] = [
|
||||
{ key: 'samples', label: 'قالبهای نمونه' },
|
||||
{ key: 'pending', label: 'در انتظار تأیید', badge: pendingCount },
|
||||
{ key: 'post-visit-review', label: 'متن پیامک ویزیت', badge: postVisitPendingCount },
|
||||
{ key: 'logs', label: 'لاگهای ارسال' },
|
||||
];
|
||||
|
||||
@@ -262,6 +294,48 @@ export default function SmsPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'post-visit-review' && (
|
||||
<div style={{ padding: '16px' }}>
|
||||
{postVisitReviewQuery.isLoading ? (
|
||||
<div className="muted" style={{ fontSize: 13 }}>در حال بارگذاری...</div>
|
||||
) : ((postVisitReviewQuery.data?.data as any)?.data ?? []).length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '32px 16px', color: 'var(--text-3)', fontSize: 13 }}>
|
||||
متنی برای بررسی وجود ندارد
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{((postVisitReviewQuery.data?.data as any)?.data ?? []).map((item: any) => (
|
||||
<div key={item.id} className="card card-pad">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 10 }}>
|
||||
<div>
|
||||
<span className="badge gray" style={{ fontSize: 11 }}>{item.entity_type} #{item.entity_id}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
className="btn primary sm"
|
||||
disabled={postVisitApproveMutation.isPending}
|
||||
onClick={() => postVisitApproveMutation.mutate(item.id)}
|
||||
>
|
||||
<CheckIcon style={{ width: 14 }} /> تأیید
|
||||
</button>
|
||||
<button
|
||||
className="btn danger sm"
|
||||
onClick={() => { setPostVisitRejectId(item.id); setPostVisitRejectReason(''); }}
|
||||
>
|
||||
<XMarkIcon style={{ width: 14 }} /> رد
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.8, padding: '10px 14px', background: 'var(--surface)', borderRadius: 8, border: '1px solid var(--border)', direction: 'rtl' }}>
|
||||
{item.post_visit_text_pending}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'logs' && (
|
||||
<>
|
||||
<DataTable<SmsLog>
|
||||
@@ -328,6 +402,28 @@ export default function SmsPage() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal open={!!postVisitRejectId} title="رد متن پیامک ویزیت"
|
||||
onClose={() => { setPostVisitRejectId(null); setPostVisitRejectReason(''); }}
|
||||
footer={
|
||||
<>
|
||||
<button onClick={() => { setPostVisitRejectId(null); setPostVisitRejectReason(''); }} className="btn ghost sm">لغو</button>
|
||||
<button
|
||||
onClick={() => postVisitRejectId && postVisitRejectMutation.mutate({ id: postVisitRejectId, reason: postVisitRejectReason })}
|
||||
disabled={!postVisitRejectReason || postVisitRejectMutation.isPending}
|
||||
className="btn danger sm">
|
||||
رد کردن
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="form-row">
|
||||
<label>دلیل رد</label>
|
||||
<textarea value={postVisitRejectReason} onChange={(e) => setPostVisitRejectReason(e.target.value)}
|
||||
rows={4} placeholder="دلیل رد را بنویسید..."
|
||||
className="input" style={{ resize: 'none', height: 'auto' }} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!approveTarget}
|
||||
title="تأیید قالب پیامک"
|
||||
|
||||
Reference in New Issue
Block a user