Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -9,7 +9,6 @@ import { api } from '../lib/api';
|
||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||
import type { SmsTemplate, SmsLog } from '../types';
|
||||
import { formatDate, formatDateTime } from '../lib/utils';
|
||||
import PageHeader from '../components/ui/PageHeader';
|
||||
import DataTable, { Column } from '../components/ui/DataTable';
|
||||
import StatusBadge from '../components/ui/StatusBadge';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
@@ -24,6 +23,12 @@ type TemplateFormData = z.infer<typeof templateSchema>;
|
||||
|
||||
type Tab = 'samples' | 'pending' | 'logs';
|
||||
|
||||
const STATUS_LOG_META: Record<string, { label: string; cls: string }> = {
|
||||
sent: { label: 'ارسال شده', cls: 'green' },
|
||||
failed: { label: 'ناموفق', cls: 'red' },
|
||||
queued: { label: 'در صف', cls: 'amber' },
|
||||
};
|
||||
|
||||
export default function SmsPage() {
|
||||
const qc = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('samples');
|
||||
@@ -110,11 +115,11 @@ export default function SmsPage() {
|
||||
});
|
||||
|
||||
const templateColumns: Column<SmsTemplate>[] = [
|
||||
{ key: 'name', header: 'نام', render: (t) => <span className="font-medium">{t.name}</span> },
|
||||
{ key: 'name', header: 'نام', render: (t) => <b>{t.name}</b> },
|
||||
{
|
||||
key: 'body',
|
||||
header: 'محتوا',
|
||||
render: (t) => <span className="text-xs text-gray-500 line-clamp-1 max-w-xs">{t.body}</span>,
|
||||
render: (t) => <span className="muted" style={{ fontSize: 12, display: 'block', maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.body}</span>,
|
||||
},
|
||||
{ key: 'status', header: 'وضعیت', render: (t) => <StatusBadge type="sms" value={t.status} /> },
|
||||
{ key: 'created_at', header: 'تاریخ', render: (t) => formatDate(t.created_at) },
|
||||
@@ -127,20 +132,15 @@ export default function SmsPage() {
|
||||
{
|
||||
key: 'message',
|
||||
header: 'پیام',
|
||||
render: (l) => <span className="text-xs text-gray-500 line-clamp-1 max-w-xs">{l.message}</span>,
|
||||
render: (l) => <span className="muted" style={{ fontSize: 12, display: 'block', maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.message}</span>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'وضعیت',
|
||||
render: (l) => (
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
l.status === 'sent' ? 'bg-green-100 text-green-700'
|
||||
: l.status === 'failed' ? 'bg-red-100 text-red-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{l.status === 'sent' ? 'ارسال شده' : l.status === 'failed' ? 'ناموفق' : 'در صف'}
|
||||
</span>
|
||||
),
|
||||
render: (l) => {
|
||||
const meta = STATUS_LOG_META[l.status] ?? { label: l.status, cls: 'gray' };
|
||||
return <span className={`badge ${meta.cls}`}><span className="bdot" />{meta.label}</span>;
|
||||
},
|
||||
},
|
||||
{ key: 'provider', header: 'سرویسدهنده' },
|
||||
{ key: 'sent_at', header: 'زمان ارسال', render: (l) => formatDateTime(l.sent_at) },
|
||||
@@ -153,38 +153,58 @@ export default function SmsPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="پیامک"
|
||||
breadcrumbs={[{ label: 'داشبورد', to: '/admin/dashboard' }, { label: 'پیامک' }]}
|
||||
action={
|
||||
activeTab === 'samples' ? (
|
||||
<button onClick={() => setAddOpen(true)}
|
||||
className="cp-btn-primary">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
افزودن قالب
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
const TABS: { key: Tab; label: string; badge?: number }[] = [
|
||||
{ key: 'samples', label: 'قالبهای نمونه' },
|
||||
{ key: 'pending', label: 'در انتظار تأیید', badge: pendingCount },
|
||||
{ key: 'logs', label: 'لاگهای ارسال' },
|
||||
];
|
||||
|
||||
<div className="cp-card">
|
||||
<div className="flex border-b border-slate-200 dark:border-gray-700 px-6 pt-4">
|
||||
{([
|
||||
{ key: 'samples' as Tab, label: 'قالبهای نمونه' },
|
||||
{ key: 'pending' as Tab, label: 'در انتظار تأیید', badge: pendingCount },
|
||||
{ key: 'logs' as Tab, label: 'لاگهای ارسال' },
|
||||
]).map((t) => (
|
||||
<button key={t.key} onClick={() => handleTabChange(t.key)}
|
||||
className={`pb-3 px-4 text-sm font-medium border-b-2 transition-colors -mb-px flex items-center gap-1.5 ${
|
||||
activeTab === t.key
|
||||
? 'border-primary-600 text-primary-600'
|
||||
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'
|
||||
}`}>
|
||||
return (
|
||||
<div className="fade-in">
|
||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||
<div>
|
||||
<h1 className="section-title">پیامک</h1>
|
||||
<div className="muted">مدیریت قالبهای پیامک</div>
|
||||
</div>
|
||||
{activeTab === 'samples' && (
|
||||
<button onClick={() => setAddOpen(true)} className="btn primary sm">
|
||||
<PlusIcon style={{ width: 16, height: 16 }} />
|
||||
افزودن قالب
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)', padding: '0 var(--card-pad)' }}>
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => handleTabChange(t.key)}
|
||||
style={{
|
||||
padding: '14px 16px',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
borderBottom: `2px solid ${activeTab === t.key ? 'var(--primary)' : 'transparent'}`,
|
||||
color: activeTab === t.key ? 'var(--primary)' : 'var(--text-2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginBottom: -1,
|
||||
transition: 'color .15s',
|
||||
}}
|
||||
>
|
||||
{t.label}
|
||||
{t.badge != null && t.badge > 0 && (
|
||||
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-bold bg-orange-500 text-white leading-none">
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
minWidth: 18, height: 18, padding: '0 4px', borderRadius: 9,
|
||||
fontSize: 10, fontWeight: 700,
|
||||
background: 'oklch(0.62 0.22 30)', color: '#fff', lineHeight: 1,
|
||||
}}>
|
||||
{t.badge}
|
||||
</span>
|
||||
)}
|
||||
@@ -192,7 +212,7 @@ export default function SmsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="card-pad" style={{ paddingTop: 'var(--card-pad)' }}>
|
||||
{activeTab === 'samples' && (
|
||||
<>
|
||||
<DataTable<SmsTemplate>
|
||||
@@ -201,9 +221,8 @@ export default function SmsPage() {
|
||||
loading={sampleTemplatesQuery.isLoading}
|
||||
emptyMessage="هیچ قالبی یافت نشد"
|
||||
actions={(t) => (
|
||||
<button onClick={() => setDeleteTarget(t)}
|
||||
className="cp-action-delete" title="حذف">
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
<button onClick={() => setDeleteTarget(t)} className="mini-btn danger" title="حذف">
|
||||
<TrashIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
@@ -225,13 +244,11 @@ export default function SmsPage() {
|
||||
emptyMessage="هیچ قالبی در انتظار تأیید نیست"
|
||||
actions={(t) => (
|
||||
<>
|
||||
<button onClick={() => setApproveTarget(t)}
|
||||
className="cp-action-approve" title="تأیید">
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
<button onClick={() => setApproveTarget(t)} className="mini-btn" title="تأیید">
|
||||
<CheckIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
<button onClick={() => setRejectTarget(t)}
|
||||
className="cp-action-delete" title="رد">
|
||||
<XMarkIcon className="w-4 h-4" />
|
||||
<button onClick={() => setRejectTarget(t)} className="mini-btn danger" title="رد">
|
||||
<XMarkIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -267,29 +284,24 @@ export default function SmsPage() {
|
||||
<Modal open={addOpen} title="افزودن قالب نمونه" onClose={() => setAddOpen(false)}
|
||||
footer={
|
||||
<>
|
||||
<button onClick={() => setAddOpen(false)}
|
||||
className="cp-btn-secondary">
|
||||
لغو
|
||||
</button>
|
||||
<button form="add-sms-form" type="submit" disabled={isSubmitting}
|
||||
className="cp-btn-primary">
|
||||
<button onClick={() => setAddOpen(false)} className="btn ghost sm">لغو</button>
|
||||
<button form="add-sms-form" type="submit" disabled={isSubmitting} className="btn primary sm">
|
||||
{isSubmitting ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="add-sms-form" onSubmit={handleSubmit((d) => createMutation.mutate(d))} className="space-y-4">
|
||||
<div>
|
||||
<label className="cp-label">نام قالب</label>
|
||||
<input {...register('name')} placeholder="مثال: تأیید نوبت"
|
||||
className="cp-input h-11" />
|
||||
{errors.name && <p className="text-red-500 text-xs mt-1">{errors.name.message}</p>}
|
||||
<form id="add-sms-form" onSubmit={handleSubmit((d) => createMutation.mutate(d))}>
|
||||
<div className="form-row">
|
||||
<label>نام قالب</label>
|
||||
<input {...register('name')} placeholder="مثال: تأیید نوبت" className="input" />
|
||||
{errors.name && <p className="err-text">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="cp-label">متن قالب</label>
|
||||
<div className="form-row" style={{ marginTop: 12 }}>
|
||||
<label>متن قالب</label>
|
||||
<textarea {...register('body')} rows={4} placeholder="متن پیامک..."
|
||||
className="cp-textarea resize-none" />
|
||||
{errors.body && <p className="text-red-500 text-xs mt-1">{errors.body.message}</p>}
|
||||
className="input" style={{ resize: 'none', height: 'auto' }} />
|
||||
{errors.body && <p className="err-text">{errors.body.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -298,23 +310,22 @@ export default function SmsPage() {
|
||||
onClose={() => { setRejectTarget(null); setRejectReason(''); }}
|
||||
footer={
|
||||
<>
|
||||
<button onClick={() => { setRejectTarget(null); setRejectReason(''); }}
|
||||
className="cp-btn-secondary">
|
||||
لغو
|
||||
</button>
|
||||
<button onClick={() => { setRejectTarget(null); setRejectReason(''); }} className="btn ghost sm">لغو</button>
|
||||
<button
|
||||
onClick={() => rejectTarget && rejectMutation.mutate({ t: rejectTarget, reason: rejectReason })}
|
||||
disabled={!rejectReason || rejectMutation.isPending}
|
||||
className="cp-btn-danger">
|
||||
className="btn danger sm">
|
||||
رد کردن
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label className="cp-label mb-2">دلیل رد:</label>
|
||||
<textarea value={rejectReason} onChange={(e) => setRejectReason(e.target.value)}
|
||||
rows={4} placeholder="دلیل رد را بنویسید..."
|
||||
className="cp-textarea resize-none" />
|
||||
<div className="form-row">
|
||||
<label>دلیل رد</label>
|
||||
<textarea value={rejectReason} onChange={(e) => setRejectReason(e.target.value)}
|
||||
rows={4} placeholder="دلیل رد را بنویسید..."
|
||||
className="input" style={{ resize: 'none', height: 'auto' }} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user