import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { CheckIcon, XMarkIcon, TrashIcon, PlusIcon, PencilIcon } from '@heroicons/react/24/outline'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import type { SmsTemplate, SmsLog, SmsMessageText } from '../types'; import { formatDate, formatDateTime } from '../lib/utils'; import DataTable, { Column } from '../components/ui/DataTable'; import StatusBadge from '../components/ui/StatusBadge'; import Pagination from '../components/ui/Pagination'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Modal from '../components/ui/Modal'; import SearchableSelect from '../components/ui/SearchableSelect'; const templateSchema = z.object({ name: z.string().min(2, 'نام قالب الزامی است'), body: z.string().min(5, 'متن قالب الزامی است'), }); type TemplateFormData = z.infer; type Tab = 'samples' | 'pending' | 'logs' | 'post-visit-approved' | 'messages'; const STATUS_LOG_META: Record = { sent: { label: 'ارسال شده', cls: 'green' }, failed: { label: 'ناموفق', cls: 'red' }, queued: { label: 'در صف', cls: 'amber' }, }; const TAG_LABELS: Record = { global: 'سیستمی', otp: 'کد تأیید', payment: 'پرداخت', clinic_invitation: 'دعوت کلینیک', pre_registration: 'پیش‌ثبت‌نام', notification_mobile: 'تأیید موبایل', user_template: 'قالب کاربر', }; const TAG_FILTER_OPTIONS = [ { value: '', label: 'همه تگ‌ها' }, ...Object.entries(TAG_LABELS).map(([value, label]) => ({ value, label })), ]; export default function SmsPage() { const qc = useQueryClient(); const [activeTab, setActiveTab] = useState('samples'); const [page, setPage] = useState(1); const [approveTarget, setApproveTarget] = useState(null); const [rejectTarget, setRejectTarget] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [addOpen, setAddOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [postVisitRejectId, setPostVisitRejectId] = useState(null); const [postVisitRejectReason, setPostVisitRejectReason] = useState(''); const [tagFilter, setTagFilter] = useState(''); const [viewLog, setViewLog] = useState(null); const [editMsg, setEditMsg] = useState(null); const [editBody, setEditBody] = useState(''); const [editTemplate, setEditTemplate] = useState(null); const [editTplName, setEditTplName] = useState(''); const [editTplBody, setEditTplBody] = useState(''); const limit = 15; const sampleTemplatesQuery = useQuery({ queryKey: ['sms-samples', page], queryFn: () => api.get>( `/api/v1/admin/sms/sample-templates?status=approved&page=${page}&limit=${limit}`, ), enabled: activeTab === 'samples', }); const pendingTemplatesQuery = useQuery({ queryKey: ['sms-pending', activeTab === 'pending' ? page : 1], queryFn: () => api.get>( `/api/v1/admin/sms/sample-templates?status=pending&page=${activeTab === 'pending' ? page : 1}&limit=${limit}`, ), }); const logsQuery = useQuery({ queryKey: ['sms-logs', page, tagFilter], queryFn: () => api.get>( `/api/v1/admin/sms/logs?page=${page}&limit=${limit}` + (tagFilter ? `&tag=${tagFilter}` : ''), ), enabled: activeTab === 'logs', }); const messagesQuery = useQuery({ queryKey: ['sms-messages'], queryFn: () => api.get>('/api/v1/admin/sms/messages'), enabled: activeTab === 'messages', }); const updateMessageMut = useMutation({ mutationFn: ({ tag, body }: { tag: string; body: string }) => api.patch>(`/api/v1/admin/sms/messages/${tag}`, { body }), onSuccess: () => { toast.success('متن پیامک به‌روزرسانی شد'); setEditMsg(null); qc.invalidateQueries({ queryKey: ['sms-messages'] }); }, onError: (err: Error) => toast.error(err.message), }); const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(templateSchema), }); const createMutation = useMutation({ mutationFn: (d: TemplateFormData) => api.post>('/api/v1/sms/template', d), onSuccess: () => { toast.success('قالب اضافه شد'); setAddOpen(false); reset(); qc.invalidateQueries({ queryKey: ['sms-samples'] }); }, onError: (err: Error) => toast.error(err.message), }); const updateTemplateMut = useMutation({ mutationFn: ({ uuid, name, body }: { uuid: string; name: string; body: string }) => api.patch>(`/api/v1/sms/template/${uuid}`, { name, body }), onSuccess: () => { toast.success('قالب ویرایش شد'); setEditTemplate(null); qc.invalidateQueries({ queryKey: ['sms-samples'] }); }, onError: (err: Error) => toast.error(err.message), }); const approveMutation = useMutation({ mutationFn: (t: SmsTemplate) => api.post>(`/api/v1/admin/sms/template/${t.uuid}/approve`, {}), onSuccess: () => { toast.success('قالب تأیید شد'); setApproveTarget(null); qc.invalidateQueries({ queryKey: ['sms-pending'] }); }, onError: (err: Error) => toast.error(err.message), }); const rejectMutation = useMutation({ mutationFn: ({ t, reason }: { t: SmsTemplate; reason: string }) => api.post>(`/api/v1/admin/sms/template/${t.uuid}/reject`, { reason }), onSuccess: () => { toast.success('قالب رد شد'); setRejectTarget(null); setRejectReason(''); qc.invalidateQueries({ queryKey: ['sms-pending'] }); }, onError: (err: Error) => toast.error(err.message), }); const deleteMutation = useMutation({ mutationFn: (t: SmsTemplate) => api.delete>(`/api/v1/sms/template/${t.uuid}`), onSuccess: () => { toast.success('قالب حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['sms-samples'] }); }, onError: (err: Error) => toast.error(err.message), }); type PostVisitItem = { id: number; entity_type: string; entity_id: number; entity_name?: string | null; post_visit_text_pending: string; post_visit_text: string | null; post_visit_text_status: string }; const postVisitReviewQuery = useQuery>({ queryKey: ['sms-post-visit-review'], queryFn: () => api.get('/api/v1/admin/sms/settings/review?status=pending'), enabled: activeTab === 'pending', }); const postVisitApprovedQuery = useQuery>({ queryKey: ['sms-post-visit-approved'], queryFn: () => api.get('/api/v1/admin/sms/settings/review?status=approved'), enabled: activeTab === 'post-visit-approved', }); 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'] }); qc.invalidateQueries({ queryKey: ['sms-post-visit-approved'] }); }, 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[] = [ { key: 'name', header: 'نام', render: (t) => {t.name} }, { key: 'body', header: 'محتوا', render: (t) => {t.body}, }, { key: 'status', header: 'وضعیت', render: (t) => }, { key: 'created_at', header: 'تاریخ', render: (t) => formatDate(t.created_at) }, ]; const pendingColumns: Column[] = templateColumns.filter((c) => c.key !== 'status'); const logColumns: Column[] = [ { key: 'recipient', header: 'گیرنده', render: (l) => {l.recipient} }, { key: 'message', header: 'پیام', render: (l) => ( ), }, { key: 'status', header: 'وضعیت', render: (l) => { const meta = STATUS_LOG_META[l.status] ?? { label: l.status, cls: 'gray' }; return {meta.label}; }, }, { key: 'tag', header: 'تگ', render: (l) => {TAG_LABELS[l.tag] ?? l.tag} }, { key: 'provider', header: 'سرویس‌دهنده' }, { key: 'sent_at', header: 'زمان ارسال', render: (l) => formatDateTime(l.sent_at) }, ]; const pendingCount = pendingTemplatesQuery.data?.meta?.totalRecords ?? 0; const handleTabChange = (tab: Tab) => { setActiveTab(tab); setPage(1); }; const postVisitPending = (postVisitReviewQuery.data?.data as any)?.data ?? []; const postVisitApproved = (postVisitApprovedQuery.data?.data as any)?.data ?? []; const pendingBadge = pendingCount + postVisitPending.length; const TABS: { key: Tab; label: string; badge?: number }[] = [ { key: 'samples', label: 'قالب‌های نمونه' }, { key: 'pending', label: 'در انتظار تأیید', badge: pendingBadge }, { key: 'post-visit-approved', label: 'پیامک‌های تأیید شده' }, { key: 'logs', label: 'لاگ‌های ارسال' }, { key: 'messages', label: 'متن پیامک‌ها' }, ]; return (

پیامک

مدیریت قالب‌های پیامک
{activeTab === 'samples' && ( )}
{/* Tabs */}
{TABS.map((t) => ( ))}
{activeTab === 'samples' && ( <> columns={templateColumns} data={sampleTemplatesQuery.data?.data ?? []} loading={sampleTemplatesQuery.isLoading} emptyMessage="هیچ قالبی یافت نشد" actions={(t) => ( <> )} /> )} {activeTab === 'pending' && ( <> {pendingCount > 0 && (
قالب‌های نمونه
)} columns={pendingColumns} data={pendingTemplatesQuery.data?.data ?? []} loading={pendingTemplatesQuery.isLoading} emptyMessage="قالب نمونه‌ای در انتظار تأیید نیست" actions={(t) => ( <> )} />
متن پیامک ویزیت
{postVisitReviewQuery.isLoading ? (
در حال بارگذاری...
) : postVisitPending.length === 0 ? (
متن ویزیتی در انتظار تأیید نیست
) : (
{postVisitPending.map((item: PostVisitItem) => (
{item.entity_type === 'doctor' ? 'پزشک' : item.entity_type === 'clinic' ? 'کلینیک' : item.entity_type} {item.entity_name ? `: ${item.entity_name}` : ` #${item.entity_id}`}
{item.post_visit_text_pending}
))}
)} )} {activeTab === 'post-visit-approved' && ( <> {postVisitApprovedQuery.isLoading ? (
در حال بارگذاری...
) : postVisitApproved.length === 0 ? (
متن تأییدشده‌ای وجود ندارد
) : (
{postVisitApproved.map((item: PostVisitItem) => (
{item.entity_type === 'doctor' ? 'پزشک' : item.entity_type === 'clinic' ? 'کلینیک' : item.entity_type} {item.entity_name ? `: ${item.entity_name}` : ` #${item.entity_id}`} تأیید شده
{item.post_visit_text}
))}
)} )} {activeTab === 'logs' && ( <>
({ value: o.value, label: o.label }))} value={tagFilter} onChange={(v) => { setTagFilter(v ? String(v) : ''); setPage(1); }} placeholder="همه تگ‌ها" />
columns={logColumns} data={logsQuery.data?.data ?? []} loading={logsQuery.isLoading} emptyMessage="لاگی یافت نشد" /> )} {activeTab === 'messages' && (
{(((messagesQuery.data?.data as any)?.data ?? []) as SmsMessageText[]).map((m) => (
{m.title}
{m.body}
{m.variables.length > 0 && (
{m.variables.map((v) => {`{${v}}`})}
)}
))} {messagesQuery.isLoading &&

در حال بارگذاری...

}
)}
setEditMsg(null)} footer={ <> } >