canina/frontend/admin-panel/src/pages/SmsSettingsPage.tsx
parsa aghaei f430c4931f
All checks were successful
Deploy Canina / deploy (push) Successful in 44s
feat(sms): display live MeliPayamak account balance/credit in SMS settings
2026-08-18 17:35:28 +03:30

1676 lines
77 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from 'react';
import {
MessageSquare,
Save,
Send,
ShieldCheck,
KeyRound,
Coins,
User,
Hash,
HelpCircle,
CheckCircle2,
AlertCircle,
Eye,
EyeOff,
Sparkles,
Smartphone,
Plus,
RefreshCw,
Edit3,
Copy,
Check,
Layers,
SlidersHorizontal,
Clock,
AlertTriangle,
X,
ArrowUpRight,
History,
Search,
Trash2,
TrendingUp,
RotateCcw,
Info,
ChevronLeft,
ChevronRight,
Filter,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
interface SmsConfigState {
enabled: boolean;
username: string;
password: string;
fromNumber: string;
otpBodyId: number | string;
orderBodyId: number | string;
shippingBodyId: number | string;
b2bBodyId: number | string;
petCareBodyId: number | string;
}
interface PatternItem {
id: number;
title: string;
body: string;
status: number; // 0 = Pending, 1 = Approved, 2 = Needs Edit
statusText?: string;
assignedTo?: string[];
}
interface SmsLogItem {
id: string;
receptor: string;
type: string;
patternId?: number | null;
args: string[];
messageText?: string | null;
status: 'SUCCESS' | 'FAILED' | 'DISABLED';
recId?: string | null;
errorMessage?: string | null;
createdAt: string;
}
interface SmsLogStats {
total: number;
success: number;
failed: number;
successRate: number;
}
export default function SmsSettingsPage() {
const [activeTab, setActiveTab] = useState<'settings' | 'patterns' | 'logs'>('settings');
// Config State
const [config, setConfig] = useState<SmsConfigState>({
enabled: true,
username: '',
password: '',
fromNumber: '',
otpBodyId: '508079',
orderBodyId: '508081',
shippingBodyId: '508082',
b2bBodyId: '508083',
petCareBodyId: '0',
});
const [patterns, setPatterns] = useState<PatternItem[]>([]);
const [credit, setCredit] = useState<number | null>(null);
const [isLoadingCredit, setIsLoadingCredit] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingPatterns, setIsLoadingPatterns] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [copiedId, setCopiedId] = useState<number | null>(null);
// Pattern Modals
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [newPatternTitle, setNewPatternTitle] = useState('');
const [newPatternBody, setNewPatternBody] = useState('');
const [isSubmittingPattern, setIsSubmittingPattern] = useState(false);
const [editingPattern, setEditingPattern] = useState<PatternItem | null>(null);
const [editPatternBody, setEditPatternBody] = useState('');
// Logs State
const [logs, setLogs] = useState<SmsLogItem[]>([]);
const [logStats, setLogStats] = useState<SmsLogStats>({
total: 0,
success: 0,
failed: 0,
successRate: 100,
});
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
const [logPage, setLogPage] = useState(1);
const [logTotalPages, setLogTotalPages] = useState(1);
const [logSearch, setLogSearch] = useState('');
const [logTypeFilter, setLogTypeFilter] = useState('ALL');
const [logStatusFilter, setLogStatusFilter] = useState('ALL');
const [logSortOrder, setLogSortOrder] = useState<'desc' | 'asc'>('desc');
const [selectedLog, setSelectedLog] = useState<SmsLogItem | null>(null);
const [isLogDetailModalOpen, setIsLogDetailModalOpen] = useState(false);
// Test SMS State
const [testPhone, setTestPhone] = useState('');
const [testPatternType, setTestPatternType] = useState('otp');
const [customPatternId, setCustomPatternId] = useState('');
const [testArg1, setTestArg1] = useState('123456');
const [testArg2, setTestArg2] = useState('150000');
const [isSendingTest, setIsSendingTest] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
const fetchCredit = async () => {
try {
setIsLoadingCredit(true);
const res = await api.get('/settings/sms/credit');
const data = res.data?.data || res.data;
if (data && data.success) {
setCredit(Number(data.credit) || 0);
}
} catch (err) {
console.error('Failed to load SMS credit:', err);
} finally {
setIsLoadingCredit(false);
}
};
const fetchSettingsAndPatterns = async () => {
try {
setIsLoading(true);
const [resConfig, resPatterns, resCredit] = await Promise.allSettled([
api.get('/settings/sms'),
api.get('/settings/sms/patterns'),
api.get('/settings/sms/credit'),
]);
if (resConfig.status === 'fulfilled') {
const data = resConfig.value.data?.data || resConfig.value.data;
if (data) {
setConfig({
enabled: data.enabled !== undefined ? Boolean(data.enabled) : true,
username: data.username || '',
password: data.password || '',
fromNumber: data.fromNumber || '',
otpBodyId: data.otpBodyId || '508079',
orderBodyId: data.orderBodyId || '508081',
shippingBodyId: data.shippingBodyId || '508082',
b2bBodyId: data.b2bBodyId || '508083',
petCareBodyId: data.petCareBodyId || '0',
});
}
}
if (resPatterns.status === 'fulfilled') {
const pList = resPatterns.value.data?.data || resPatterns.value.data || [];
setPatterns(Array.isArray(pList) ? pList : []);
}
if (resCredit.status === 'fulfilled') {
const cData = resCredit.value.data?.data || resCredit.value.data;
if (cData && cData.success) {
setCredit(Number(cData.credit) || 0);
}
}
} catch (err) {
console.error('Failed to load SMS data:', err);
} finally {
setIsLoading(false);
}
};
const fetchLogs = useCallback(async () => {
try {
setIsLoadingLogs(true);
const params: Record<string, any> = {
page: logPage,
limit: 15,
sortOrder: logSortOrder,
};
if (logSearch) params.search = logSearch;
if (logTypeFilter !== 'ALL') params.type = logTypeFilter;
if (logStatusFilter !== 'ALL') params.status = logStatusFilter;
const res = await api.get('/settings/sms/logs', { params });
const data = res.data?.data || res.data;
if (data) {
setLogs(data.logs || []);
setLogTotalPages(data.totalPages || 1);
if (data.stats) {
setLogStats(data.stats);
}
}
} catch (err) {
console.error('Failed to fetch SMS logs:', err);
toast.error('خطا در دریافت لاگ‌های پیامک');
} finally {
setIsLoadingLogs(false);
}
}, [logPage, logSearch, logTypeFilter, logStatusFilter, logSortOrder]);
useEffect(() => {
fetchSettingsAndPatterns();
}, []);
useEffect(() => {
if (activeTab === 'logs') {
fetchLogs();
}
}, [activeTab, fetchLogs]);
const refreshPatterns = async () => {
try {
setIsLoadingPatterns(true);
const res = await api.get('/settings/sms/patterns');
const pList = res.data?.data || res.data || [];
setPatterns(Array.isArray(pList) ? pList : []);
toast.success('لیست الگوها از سامانه ملی پیامک بروزرسانی شد');
} catch (err) {
console.error('Failed to refresh patterns:', err);
toast.error('خطا در دریافت لیست پترن‌ها از ملی پیامک');
} finally {
setIsLoadingPatterns(false);
}
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
try {
setIsSaving(true);
const payload = {
enabled: Boolean(config.enabled),
username: config.username.trim(),
password: config.password.trim(),
fromNumber: config.fromNumber.trim(),
otpBodyId: Number(config.otpBodyId) || 0,
orderBodyId: Number(config.orderBodyId) || 0,
shippingBodyId: Number(config.shippingBodyId) || 0,
b2bBodyId: Number(config.b2bBodyId) || 0,
petCareBodyId: Number(config.petCareBodyId) || 0,
};
await api.patch('/settings/sms', payload);
toast.success('تنظیمات درگاه پیامک با موفقیت در دیتابیس ذخیره شد');
refreshPatterns();
} catch (err) {
console.error('Failed to update SMS settings:', err);
toast.error('خطا در ذخیره‌سازی تنظیمات پیامک');
} finally {
setIsSaving(false);
}
};
const handleCreatePattern = async (e: React.FormEvent) => {
e.preventDefault();
if (!newPatternTitle || !newPatternBody) {
toast.error('لطفاً عنوان و متن الگو را وارد نمایید');
return;
}
try {
setIsSubmittingPattern(true);
const res = await api.post('/settings/sms/patterns', {
title: newPatternTitle.trim(),
body: newPatternBody.trim(),
});
const resData = res.data?.data || res.data;
if (resData.success) {
toast.success(resData.message || 'الگو با موفقیت ثبت شد');
setIsAddModalOpen(false);
setNewPatternTitle('');
setNewPatternBody('');
refreshPatterns();
} else {
toast.error(resData.message || 'خطا در ثبت الگو در سامانه ملی پیامک');
}
} catch (err: any) {
console.error('Failed to add pattern:', err);
toast.error(err.response?.data?.message || 'خطای سرور در ثبت الگو');
} finally {
setIsSubmittingPattern(false);
}
};
const handleEditPattern = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingPattern || !editPatternBody) return;
try {
setIsSubmittingPattern(true);
const res = await api.put(`/settings/sms/patterns/${editingPattern.id}`, {
body: editPatternBody.trim(),
});
const resData = res.data?.data || res.data;
if (resData.success) {
toast.success(resData.message || 'ویرایش الگو ثبت شد');
setIsEditModalOpen(false);
setEditingPattern(null);
refreshPatterns();
} else {
toast.error(resData.message || 'خطا در ویرایش الگو');
}
} catch (err: any) {
console.error('Failed to edit pattern:', err);
toast.error(err.response?.data?.message || 'خطای سرور در ویرایش الگو');
} finally {
setIsSubmittingPattern(false);
}
};
const handleAssignPattern = async (type: keyof SmsConfigState, patternId: number) => {
const updated = { ...config, [type]: String(patternId) };
setConfig(updated);
try {
await api.patch('/settings/sms', {
...updated,
[type]: Number(patternId),
});
toast.success(`الگوی ${patternId} به عنوان رویداد مورد نظر تنظیم شد`);
refreshPatterns();
} catch {
toast.error('خطا در ذخیره اتصال پترن');
}
};
const handleDeleteLog = async (id: string) => {
if (!window.confirm('آیا از حذف این لاگ پیامک اطمینان دارید؟')) return;
try {
await api.delete(`/settings/sms/logs/${id}`);
toast.success('لاگ با موفقیت حذف شد');
fetchLogs();
} catch {
toast.error('خطا در حذف لاگ');
}
};
const handleClearAllLogs = async () => {
if (!window.confirm('آیا از پاکسازی تمامی لاگ‌های پیامک اطمینان دارید؟ این عمل غیرقابل بازگشت است.')) return;
try {
await api.delete('/settings/sms/logs');
toast.success('تمام لاگ‌های پیامک پاکسازی شدند');
fetchLogs();
} catch {
toast.error('خطا در پاکسازی لاگ‌ها');
}
};
const copyToClipboard = (text: string | number, label: string = 'کد') => {
navigator.clipboard.writeText(String(text));
toast.success(`${label} کپی شد`);
};
const handleSendTestSms = async (e: React.FormEvent) => {
e.preventDefault();
if (!testPhone) {
toast.error('لطفاً شماره موبایل مقصد برای تست را وارد کنید');
return;
}
let patternId = Number(config.otpBodyId);
let args = [testArg1];
if (testPatternType === 'order') {
patternId = Number(config.orderBodyId);
args = ['ORD-9988', testArg2 || '150000'];
} else if (testPatternType === 'shipping') {
patternId = Number(config.shippingBodyId);
args = ['ORD-9988', 'POST-123456789'];
} else if (testPatternType === 'custom') {
patternId = Number(customPatternId);
args = [testArg1, testArg2].filter(Boolean);
}
try {
setIsSendingTest(true);
setTestResult(null);
const response = await api.post('/settings/sms/test', {
phone: testPhone.trim(),
patternId,
args,
});
const resData = response.data?.data || response.data;
setTestResult({
success: resData.success,
message: resData.message || (resData.success ? 'ارسال موفق بود' : 'خطا در ارسال'),
});
if (resData.success) {
toast.success('پیامک آزمایشی با موفقیت به شماره مقصد ارسال شد');
} else {
toast.error(resData.message || 'خطا در ارسال پیامک آزمایشی');
}
} catch (err: any) {
console.error('Failed to dispatch test SMS:', err);
const errMsg = err.response?.data?.message || 'خطای شبکه یا سرور در ارسال پیامک آزمایشی';
setTestResult({
success: false,
message: errMsg,
});
toast.error(errMsg);
} finally {
setIsSendingTest(false);
}
};
const getTypeName = (type: string) => {
switch (type) {
case 'OTP':
return { label: 'کد تایید OTP', bg: 'bg-purple-100 text-purple-800 border-purple-200' };
case 'ORDER_CONFIRMATION':
return { label: 'ثبت و تایید سفارش', bg: 'bg-emerald-100 text-emerald-800 border-emerald-200' };
case 'SHIPPING_TRACKING':
return { label: 'کد رهگیری پست', bg: 'bg-blue-100 text-blue-800 border-blue-200' };
case 'B2B_NOTIFICATION':
return { label: 'همکاران B2B', bg: 'bg-amber-100 text-amber-800 border-amber-200' };
case 'PET_CARE_REMINDER':
return { label: 'یادآور سلامت پت', bg: 'bg-pink-100 text-pink-800 border-pink-200' };
case 'TEST':
return { label: 'پیامک تستی', bg: 'bg-slate-100 text-slate-800 border-slate-200' };
default:
return { label: type || 'متنی آزاد', bg: 'bg-gray-100 text-gray-800 border-gray-200' };
}
};
const formatDate = (iso: string) => {
try {
const date = new Date(iso);
return new Intl.DateTimeFormat('fa-IR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(date);
} catch {
return iso;
}
};
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<MessageSquare className="w-6 h-6 text-purple-600" />
مرکز پیامک و اطلاعرسانی (MeliPayamak SMS Suite)
</h2>
<p className="text-gray-500 font-medium mt-1">
پیکربندی اتصال وبسرویس، مدیریت الگوهای خدماتی و رهگیری لاگهای لحظهای ارسال
</p>
</div>
{/* Tab Toggle Navigation */}
<div className="flex items-center bg-gray-100 p-1 rounded-2xl border border-gray-200 flex-wrap">
<button
type="button"
onClick={() => setActiveTab('settings')}
className={`px-3.5 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 ${
activeTab === 'settings'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<SlidersHorizontal className="w-4 h-4" />
پیکربندی حساب
</button>
<button
type="button"
onClick={() => setActiveTab('patterns')}
className={`px-3.5 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 ${
activeTab === 'patterns'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Layers className="w-4 h-4" />
مدیریت الگوها (Patterns)
{patterns.length > 0 && (
<span className="bg-purple-100 text-purple-800 text-[10px] px-1.5 py-0.2 rounded-full font-mono font-bold">
{patterns.length}
</span>
)}
</button>
<button
type="button"
onClick={() => setActiveTab('logs')}
className={`px-3.5 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 ${
activeTab === 'logs'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<History className="w-4 h-4" />
لاگها و رهگیری ارسال
{logStats.total > 0 && (
<span className="bg-purple-100 text-purple-800 text-[10px] px-1.5 py-0.2 rounded-full font-mono font-bold">
{logStats.total}
</span>
)}
</button>
</div>
</div>
{isLoading ? (
<div className="flex justify-center p-12">
<Spinner size="lg" className="text-purple-600" />
</div>
) : activeTab === 'settings' ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Settings Form (2 cols on large) */}
<form onSubmit={handleSave} className="lg:col-span-2 space-y-6">
{/* Status & Credentials Card */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6">
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-purple-600" />
وضعیت سامانه و مشخصات حساب کاربری
</h3>
{/* Master Enabled Switch */}
<div className="flex items-center justify-between p-4 bg-purple-50 rounded-xl border border-purple-100">
<div>
<h4 className="font-bold text-gray-900 text-sm">فعال بودن ارسال پیامک در سراسر سیستم</h4>
<p className="text-xs text-gray-500 mt-0.5">
در صورت غیرفعال بودن، پیامکهای ثبت سفارش و OTP به صورت واقعی ارسال نخواهند شد.
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={config.enabled}
onChange={(e) => setConfig({ ...config, enabled: e.target.checked })}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
</label>
</div>
{/* Username & Password */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1 flex items-center gap-1.5">
<User className="w-3.5 h-3.5 text-gray-500" />
نام کاربری یا شماره پنل ملی پیامک (Username) *
</label>
<input
type="text"
required
value={config.username}
onChange={(e) => setConfig({ ...config, username: e.target.value })}
placeholder="مثلاً: 09364100228"
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500 mt-1">شماره همراه یا نام کاربری ورود به پنل melipayamak.com</p>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1 flex items-center gap-1.5">
<KeyRound className="w-3.5 h-3.5 text-gray-500" />
کلمه عبور یا کلید API (Password / API Key) *
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
required
value={config.password}
onChange={(e) => setConfig({ ...config, password: e.target.value })}
placeholder="••••••••••••"
className="w-full border border-gray-200 rounded-xl p-3 pr-10 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 p-1"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
<p className="text-[11px] text-gray-500 mt-1">رمز عبور اختصاصی پنل یا کلید وبسرویس</p>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-bold text-gray-700 mb-1 flex items-center gap-1.5">
<Smartphone className="w-3.5 h-3.5 text-gray-500" />
شماره خط فرستنده پیامک (From Number - اختیاری)
</label>
<input
type="text"
value={config.fromNumber}
onChange={(e) => setConfig({ ...config, fromNumber: e.target.value })}
placeholder="مثلاً: 50004... (در پترن اشتراکی نیازی نیست)"
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500 mt-1">
در ارسال پترن خدماتی، ملی پیامک به صورت خودکار از خط اشتراکی استفاده میکند و پر کردن این فیلد اختیاری است.
</p>
</div>
</div>
</div>
{/* Pattern IDs Card */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
<Hash className="w-5 h-5 text-indigo-600" />
شناسههای قالب و الگوهای خدماتی فعال (Pattern Body IDs)
</h3>
<button
type="button"
onClick={() => setActiveTab('patterns')}
className="text-xs text-purple-600 hover:text-purple-800 font-bold flex items-center gap-1"
>
مشاهده و تعریف الگوها <ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* OTP Pattern */}
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-900">کد پترن ورود / ثبتنام (OTP)</label>
<span className="text-[10px] text-gray-500 font-mono bg-white px-2 py-0.5 rounded border border-gray-200">
متغیر: {'{0}'}
</span>
</div>
<input
type="number"
value={config.otpBodyId}
onChange={(e) => setConfig({ ...config, otpBodyId: e.target.value })}
placeholder="508079"
className="w-full border border-gray-200 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500">کد تایید اعتبار ورود و فراموشی رمز کاربران</p>
</div>
{/* Order Confirmation Pattern */}
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-900">کد پترن ثبت و تایید سفارش</label>
<span className="text-[10px] text-gray-500 font-mono bg-white px-2 py-0.5 rounded border border-gray-200">
متغیرها: {'{0};{1}'}
</span>
</div>
<input
type="number"
value={config.orderBodyId}
onChange={(e) => setConfig({ ...config, orderBodyId: e.target.value })}
placeholder="508081"
className="w-full border border-gray-200 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500">ارسال شماره فاکتور و مبلغ به خریدار پس از پرداخت</p>
</div>
{/* Shipping Tracking Pattern */}
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-900">کد پترن رهگیری پستی و ارسال</label>
<span className="text-[10px] text-gray-500 font-mono bg-white px-2 py-0.5 rounded border border-gray-200">
متغیرها: {'{0};{1}'}
</span>
</div>
<input
type="number"
value={config.shippingBodyId}
onChange={(e) => setConfig({ ...config, shippingBodyId: e.target.value })}
placeholder="508082"
className="w-full border border-gray-200 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500">ارسال بارکد ۲۴ رقمی پست به مشتری هنگام تحویل مرسوله</p>
</div>
{/* B2B Pattern */}
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-900">کد پترن درخواست همکاری B2B</label>
<span className="text-[10px] text-gray-500 font-mono bg-white px-2 py-0.5 rounded border border-gray-200">
متغیر: {'{0}'}
</span>
</div>
<input
type="number"
value={config.b2bBodyId}
onChange={(e) => setConfig({ ...config, b2bBodyId: e.target.value })}
placeholder="508083"
className="w-full border border-gray-200 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500">اطلاعرسانی ثبت یا تایید حساب متقاضیان عمدهفروشی</p>
</div>
{/* Pet Care Reminder Pattern */}
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-2 md:col-span-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-900">کد پترن یادآور سلامت و واکسیناسیون پت</label>
<span className="text-[10px] text-gray-500 font-mono bg-white px-2 py-0.5 rounded border border-gray-200">
متغیرها: {'{0};{1}'}
</span>
</div>
<input
type="number"
value={config.petCareBodyId}
onChange={(e) => setConfig({ ...config, petCareBodyId: e.target.value })}
placeholder="0 (در صورت تعریف نکردن 0 بگذارید)"
className="w-full border border-gray-200 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white"
dir="ltr"
/>
<p className="text-[11px] text-gray-500">یادآور نوبت واکسن، انگلزدایی یا مصرف مکمل پت برای سرپرست</p>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex justify-end pt-2">
<button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3.5 px-8 rounded-xl transition-all flex items-center gap-2 shadow-lg shadow-purple-200 disabled:opacity-50"
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات درگاه پیامک
</button>
</div>
</form>
{/* Balance & Test SMS Sidebar (1 col on large) */}
<div className="space-y-6">
{/* Realtime Credit / Balance Card */}
<div className="bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-900 p-6 rounded-2xl shadow-xl border border-purple-800/40 text-white space-y-4 relative overflow-hidden">
<div className="absolute -left-6 -bottom-6 w-28 h-28 bg-purple-500/20 rounded-full blur-2xl pointer-events-none" />
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<div className="flex items-center gap-2">
<Coins className="w-5 h-5 text-amber-400" />
<h3 className="text-base font-black">موجودی و شارژ پنل پیامک</h3>
</div>
<button
type="button"
onClick={fetchCredit}
disabled={isLoadingCredit}
className="p-1.5 text-white/70 hover:text-white hover:bg-white/10 rounded-xl transition-all border border-white/10 cursor-pointer"
title="استعلام مجدد شارژ"
>
<RefreshCw className={`w-3.5 h-3.5 ${isLoadingCredit ? 'animate-spin text-amber-400' : ''}`} />
</button>
</div>
<div className="space-y-2">
<p className="text-xs text-white/70 font-medium">اعتبار باقیمانده در درگاه ملی پیامک:</p>
<div className="flex items-baseline gap-2">
{isLoadingCredit ? (
<div className="flex items-center gap-2 py-1 text-amber-400 text-sm font-bold">
<Spinner size="sm" />
<span>در حال استعلام از وبسرویس...</span>
</div>
) : credit !== null ? (
<>
<span className="text-3xl font-black text-amber-400 font-mono tracking-tight" dir="ltr">
{toPersianDigits(credit.toLocaleString())}
</span>
<span className="text-xs font-bold text-white/80">ریال / پالس</span>
</>
) : (
<span className="text-sm font-bold text-rose-300">اطلاعات شارژ دریافت نشد</span>
)}
</div>
</div>
<div className="p-3 bg-white/5 rounded-xl border border-white/10 text-[11px] text-white/75 space-y-1">
<div className="flex items-center justify-between">
<span>وضعیت حساب:</span>
<span className="font-bold text-emerald-400">فعال و متصل به وبسرویس</span>
</div>
<div className="flex items-center justify-between">
<span>نام کاربری:</span>
<span className="font-mono text-white/90" dir="ltr">{config.username || 'تنظیم نشده'}</span>
</div>
</div>
<a
href="https://melipayamak.com"
target="_blank"
rel="noreferrer"
className="w-full bg-white/10 hover:bg-white/20 text-white font-bold py-2.5 px-4 rounded-xl text-xs flex items-center justify-center gap-1.5 transition-colors border border-white/15"
>
<span>شارژ و افزایش اعتبار در ملی پیامک</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</a>
</div>
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-5">
<div className="flex items-center gap-2 border-b border-gray-100 pb-3">
<Send className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-bold text-gray-900">تست زنده ارسال پیامک</h3>
</div>
<p className="text-xs text-gray-500 leading-relaxed">
برای اطمینان از صحت نام کاربری، رمز عبور و شناسههای پترن تایید شده در ملی پیامک، یک پیامک آزمایشی به شماره خود ارسال کنید.
</p>
<form onSubmit={handleSendTestSms} className="space-y-4">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">شماره موبایل مقصد *</label>
<input
type="text"
required
value={testPhone}
onChange={(e) => setTestPhone(e.target.value)}
placeholder="0912..."
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-blue-500 text-sm font-mono font-bold"
dir="ltr"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">نوع پترن برای تست</label>
<select
value={testPatternType}
onChange={(e) => setTestPatternType(e.target.value)}
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-blue-500 text-xs font-bold bg-gray-50"
>
<option value="otp">کد تایید OTP (BodyId: {config.otpBodyId || '508079'})</option>
<option value="order">تایید سفارش (BodyId: {config.orderBodyId || '508081'})</option>
<option value="shipping">رهگیری پستی (BodyId: {config.shippingBodyId || '508082'})</option>
<option value="custom">پترن سفارشی با کد دلخواه</option>
</select>
</div>
{testPatternType === 'custom' && (
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">شناسه پترن دلخواه (BodyId)</label>
<input
type="number"
required
value={customPatternId}
onChange={(e) => setCustomPatternId(e.target.value)}
placeholder="مثلاً: 508079"
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-blue-500 text-xs font-mono font-bold"
dir="ltr"
/>
</div>
)}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">متغیر تستی ۱ (کد یا متن)</label>
<input
type="text"
value={testArg1}
onChange={(e) => setTestArg1(e.target.value)}
placeholder="123456"
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-blue-500 text-xs font-mono font-bold"
dir="ltr"
/>
</div>
{testResult && (
<div
className={`p-3 rounded-xl border text-xs flex items-start gap-2 ${
testResult.success
? 'bg-emerald-50 border-emerald-200 text-emerald-800'
: 'bg-rose-50 border-rose-200 text-rose-800'
}`}
>
{testResult.success ? (
<CheckCircle2 className="w-4 h-4 shrink-0 text-emerald-600 mt-0.5" />
) : (
<AlertCircle className="w-4 h-4 shrink-0 text-rose-600 mt-0.5" />
)}
<span className="font-medium leading-tight">{testResult.message}</span>
</div>
)}
<button
type="submit"
disabled={isSendingTest}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-xl transition-all flex items-center justify-center gap-2 shadow-md shadow-blue-200 disabled:opacity-50 text-xs"
>
{isSendingTest ? <Spinner size="sm" /> : <Send className="w-4 h-4" />}
ارسال پیامک تستی
</button>
</form>
</div>
</div>
</div>
) : activeTab === 'patterns' ? (
/* Patterns Management Tab */
<div className="space-y-6">
{/* Action Bar */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-sm font-bold text-gray-700">
مجموع الگوهای ثبت شده: <span className="text-purple-600 font-mono">{patterns.length}</span>
</span>
<button
type="button"
onClick={refreshPatterns}
disabled={isLoadingPatterns}
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-all border border-gray-200 flex items-center gap-1.5 text-xs font-bold"
title="بازخوانی از وب‌سرویس ملی پیامک"
>
<RefreshCw className={`w-3.5 h-3.5 ${isLoadingPatterns ? 'animate-spin text-purple-600' : ''}`} />
<span>بروزرسانی از ملی پیامک</span>
</button>
</div>
<button
type="button"
onClick={() => setIsAddModalOpen(true)}
className="bg-purple-600 hover:bg-purple-700 text-white text-xs font-bold py-2.5 px-5 rounded-xl transition-all flex items-center gap-2 shadow-md shadow-purple-200"
>
<Plus className="w-4 h-4" />
درج الگوی جدید (Add Pattern)
</button>
</div>
{/* Patterns Table */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right text-sm">
<thead className="bg-gray-50 text-gray-600 font-bold border-b border-gray-200 text-xs">
<tr>
<th className="p-4">شناسه الگو (BodyId)</th>
<th className="p-4">عنوان الگو</th>
<th className="p-4">متن الگو و متغیرها</th>
<th className="p-4">وضعیت تایید</th>
<th className="p-4">اتصال به سیستم</th>
<th className="p-4 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-medium">
{patterns.length === 0 ? (
<tr>
<td colSpan={6} className="p-8 text-center text-gray-400 text-xs">
هیچ الگویی یافت نشد. میتوانید با دکمه «درج الگوی جدید» اولین پترن خود را ثبت کنید.
</td>
</tr>
) : (
patterns.map((p) => (
<tr key={p.id} className="hover:bg-purple-50/40 transition-colors">
<td className="p-4 font-mono font-bold text-gray-900">
<div className="flex items-center gap-2">
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded-lg text-xs">
{p.id}
</span>
<button
type="button"
onClick={() => {
copyToClipboard(p.id, 'کد پترن');
setCopiedId(p.id);
setTimeout(() => setCopiedId(null), 2000);
}}
className="text-gray-400 hover:text-purple-600 p-1"
title="کپی شناسه"
>
{copiedId === p.id ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
</td>
<td className="p-4 font-bold text-gray-900">{p.title}</td>
<td className="p-4 text-xs text-gray-600 max-w-md">
<div className="bg-slate-50 p-2.5 rounded-xl border border-slate-200/80 font-sans leading-relaxed">
{p.body}
</div>
</td>
<td className="p-4 text-xs">
{p.status === 1 ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-200">
<CheckCircle2 className="w-3.5 h-3.5" />
تایید شده
</span>
) : p.status === 2 || p.status === -1 ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-bold bg-rose-50 text-rose-700 border border-rose-200">
<AlertTriangle className="w-3.5 h-3.5" />
نیاز به ویرایش
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-bold bg-amber-50 text-amber-700 border border-amber-200">
<Clock className="w-3.5 h-3.5" />
در انتظار تایید ناظر
</span>
)}
</td>
<td className="p-4 text-xs">
{p.assignedTo && p.assignedTo.length > 0 ? (
<div className="flex flex-wrap gap-1">
{p.assignedTo.map((a, i) => (
<span
key={i}
className="bg-purple-100 text-purple-800 text-[10px] font-bold px-2 py-0.5 rounded-md"
>
{a}
</span>
))}
</div>
) : (
<span className="text-gray-400 text-[11px]">آزاد (بدون اتصال)</span>
)}
</td>
<td className="p-4 text-center">
<div className="flex items-center justify-center gap-1.5 flex-wrap">
<select
onChange={(e) => {
if (e.target.value) {
handleAssignPattern(e.target.value as keyof SmsConfigState, p.id);
e.target.value = '';
}
}}
defaultValue=""
className="text-[11px] font-bold bg-gray-50 border border-gray-200 rounded-lg p-1.5 text-gray-700 outline-none hover:border-purple-300"
>
<option value="" disabled>
انتساب به رویداد...
</option>
<option value="otpBodyId">کد تایید OTP</option>
<option value="orderBodyId">تایید ثبت سفارش</option>
<option value="shippingBodyId">کد رهگیری پست</option>
<option value="b2bBodyId">درخواست همکار B2B</option>
<option value="petCareBodyId">یادآور پرونده سلامت پت</option>
</select>
<button
type="button"
onClick={() => {
setEditingPattern(p);
setEditPatternBody(p.body);
setIsEditModalOpen(true);
}}
className="p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="ویرایش متن الگو"
>
<Edit3 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
) : (
/* Logs & Tracking Tab */
<div className="space-y-6">
{/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center text-purple-600">
<MessageSquare className="w-6 h-6" />
</div>
<div>
<span className="text-xs text-gray-500 font-medium">کل پیامکهای ارسالی</span>
<h4 className="text-xl font-black text-gray-900 font-mono mt-0.5">{logStats.total}</h4>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-emerald-50 flex items-center justify-center text-emerald-600">
<CheckCircle2 className="w-6 h-6" />
</div>
<div>
<span className="text-xs text-gray-500 font-medium">ارسالهای موفق</span>
<h4 className="text-xl font-black text-emerald-600 font-mono mt-0.5">{logStats.success}</h4>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-rose-50 flex items-center justify-center text-rose-600">
<AlertCircle className="w-6 h-6" />
</div>
<div>
<span className="text-xs text-gray-500 font-medium">ارسالهای ناموفق / خطا</span>
<h4 className="text-xl font-black text-rose-600 font-mono mt-0.5">{logStats.failed}</h4>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
<TrendingUp className="w-6 h-6" />
</div>
<div>
<span className="text-xs text-gray-500 font-medium">نرخ موفقیت تحویل</span>
<h4 className="text-xl font-black text-blue-600 font-mono mt-0.5">{logStats.successRate}%</h4>
</div>
</div>
</div>
{/* Filter and Actions Bar */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{/* Search */}
<div className="relative md:col-span-2">
<Search className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
<input
type="text"
value={logSearch}
onChange={(e) => {
setLogSearch(e.target.value);
setLogPage(1);
}}
placeholder="جستجو در شماره گیرنده، کد پیگیری یا متن خطا..."
className="w-full pr-9 pl-3 py-2 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 text-xs font-medium"
/>
</div>
{/* Type Filter */}
<div>
<select
value={logTypeFilter}
onChange={(e) => {
setLogTypeFilter(e.target.value);
setLogPage(1);
}}
className="w-full border border-gray-200 rounded-xl p-2 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold bg-white"
>
<option value="ALL">همه انواع پیامک</option>
<option value="OTP">کد تایید ورود (OTP)</option>
<option value="ORDER_CONFIRMATION">ثبت و تایید سفارش</option>
<option value="SHIPPING_TRACKING">کد رهگیری پست</option>
<option value="B2B_NOTIFICATION">همکاران B2B</option>
<option value="PET_CARE_REMINDER">یادآور سلامت پت</option>
<option value="TEST">پیامک تستی</option>
<option value="GENERIC_TEXT">متن آزاد</option>
</select>
</div>
{/* Status Filter */}
<div>
<select
value={logStatusFilter}
onChange={(e) => {
setLogStatusFilter(e.target.value);
setLogPage(1);
}}
className="w-full border border-gray-200 rounded-xl p-2 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold bg-white"
>
<option value="ALL">همه وضعیتها</option>
<option value="SUCCESS">موفق (SUCCESS)</option>
<option value="FAILED">ناموفق (FAILED)</option>
<option value="DISABLED">غیرفعال (DISABLED)</option>
</select>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100 flex-wrap gap-2 text-xs">
<div className="flex items-center gap-2">
<button
type="button"
onClick={fetchLogs}
disabled={isLoadingLogs}
className="p-2 text-gray-600 hover:text-purple-600 hover:bg-purple-50 rounded-xl border border-gray-200 flex items-center gap-1.5 font-bold"
>
<RefreshCw className={`w-3.5 h-3.5 ${isLoadingLogs ? 'animate-spin text-purple-600' : ''}`} />
<span>بروزرسانی لاگها</span>
</button>
<button
type="button"
onClick={() => setLogSortOrder(logSortOrder === 'desc' ? 'asc' : 'desc')}
className="p-2 text-gray-600 hover:bg-gray-100 rounded-xl border border-gray-200 flex items-center gap-1 font-bold"
>
<Filter className="w-3.5 h-3.5" />
<span>مرتبسازی: {logSortOrder === 'desc' ? 'جدیدترین' : 'قدیمی‌ترین'}</span>
</button>
</div>
<button
type="button"
onClick={handleClearAllLogs}
className="text-rose-600 hover:bg-rose-50 border border-rose-200 px-3 py-1.5 rounded-xl font-bold flex items-center gap-1.5"
>
<Trash2 className="w-3.5 h-3.5" />
پاکسازی کل لاگها
</button>
</div>
</div>
{/* Logs Table */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right text-sm">
<thead className="bg-gray-50 text-gray-600 font-bold border-b border-gray-200 text-xs">
<tr>
<th className="p-4">شماره گیرنده</th>
<th className="p-4">نوع پیامک</th>
<th className="p-4">کد پترن / متغیرها</th>
<th className="p-4">شناسه پیگیری (RecId)</th>
<th className="p-4">وضعیت</th>
<th className="p-4">تاریخ و ساعت</th>
<th className="p-4 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-medium">
{isLoadingLogs ? (
<tr>
<td colSpan={7} className="p-8 text-center text-gray-400">
<div className="flex justify-center"><Spinner size="md" className="text-purple-600" /></div>
</td>
</tr>
) : logs.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-gray-400 text-xs">
هیچ رکورد لاگ پیامکی متناسب با جستجو یافت نشد.
</td>
</tr>
) : (
logs.map((log) => {
const typeInfo = getTypeName(log.type);
return (
<tr key={log.id} className="hover:bg-purple-50/30 transition-colors">
<td className="p-4 font-mono font-bold text-gray-900" dir="ltr">
<div className="flex items-center justify-end gap-1.5">
<span>{log.receptor}</span>
<button
type="button"
onClick={() => copyToClipboard(log.receptor, 'شماره')}
className="text-gray-400 hover:text-purple-600"
title="کپی شماره"
>
<Copy className="w-3 h-3" />
</button>
</div>
</td>
<td className="p-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold border ${typeInfo.bg}`}>
{typeInfo.label}
</span>
</td>
<td className="p-4 text-xs font-mono">
{log.patternId ? (
<div className="flex items-center gap-1.5 flex-wrap">
<span className="bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded font-bold">
#{log.patternId}
</span>
{log.args && log.args.length > 0 && (
<span className="text-gray-500 font-sans text-[11px]">
[{log.args.join(' , ')}]
</span>
)}
</div>
) : (
<span className="text-gray-400 font-sans">متنی آزاد</span>
)}
</td>
<td className="p-4 text-xs font-mono font-bold text-gray-700">
{log.recId ? (
<div className="flex items-center gap-1">
<span>{log.recId}</span>
<button
type="button"
onClick={() => copyToClipboard(log.recId || '', 'شناسه پیگیری')}
className="text-gray-400 hover:text-purple-600"
>
<Copy className="w-3 h-3" />
</button>
</div>
) : (
<span className="text-gray-300">-</span>
)}
</td>
<td className="p-4 text-xs">
{log.status === 'SUCCESS' ? (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-200">
<CheckCircle2 className="w-3.5 h-3.5" />
موفق
</span>
) : log.status === 'DISABLED' ? (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-bold bg-slate-50 text-slate-700 border border-slate-200">
<Info className="w-3.5 h-3.5" />
غیرفعال
</span>
) : (
<span
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-bold bg-rose-50 text-rose-700 border border-rose-200"
title={log.errorMessage || 'خطا در ارسال'}
>
<AlertCircle className="w-3.5 h-3.5" />
ناموفق
</span>
)}
</td>
<td className="p-4 text-xs text-gray-500 font-mono">
{formatDate(log.createdAt)}
</td>
<td className="p-4 text-center">
<div className="flex items-center justify-center gap-1">
<button
type="button"
onClick={() => {
setSelectedLog(log);
setIsLogDetailModalOpen(true);
}}
className="p-1.5 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
title="مشاهده جزییات"
>
<Info className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleDeleteLog(log.id)}
className="p-1.5 text-gray-400 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
title="حذف لاگ"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Pagination Controls */}
{logTotalPages > 1 && (
<div className="p-4 bg-gray-50 border-t border-gray-100 flex items-center justify-between text-xs">
<span className="text-gray-500 font-medium">
صفحه {logPage} از {logTotalPages}
</span>
<div className="flex items-center gap-2">
<button
type="button"
disabled={logPage <= 1}
onClick={() => setLogPage((p) => Math.max(1, p - 1))}
className="p-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-40"
>
<ChevronRight className="w-4 h-4" />
</button>
<button
type="button"
disabled={logPage >= logTotalPages}
onClick={() => setLogPage((p) => Math.min(logTotalPages, p + 1))}
className="p-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-40"
>
<ChevronLeft className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* Modal: Add New Pattern */}
{isAddModalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-xl rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Plus className="w-5 h-5 text-purple-600" />
درج الگوی جدید در سامانه ملی پیامک
</h3>
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreatePattern} className="p-6 space-y-5">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">عنوان الگو (Title) *</label>
<input
type="text"
required
value={newPatternTitle}
onChange={(e) => setNewPatternTitle(e.target.value)}
placeholder="مثلاً: تایید سفارش کنینا"
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-bold"
/>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-bold text-gray-700">متن الگو (Body) *</label>
<div className="flex items-center gap-1 text-[11px]">
<span className="text-gray-400">درج سریع متغیر:</span>
{['{0}', '{1}', '{2}'].map((v) => (
<button
key={v}
type="button"
onClick={() => setNewPatternBody((prev) => `${prev} ${v}`)}
className="bg-purple-50 text-purple-700 hover:bg-purple-100 font-mono font-bold px-1.5 py-0.5 rounded border border-purple-200"
>
+{v}
</button>
))}
</div>
</div>
<textarea
rows={4}
required
value={newPatternBody}
onChange={(e) => setNewPatternBody(e.target.value)}
placeholder="{0} عزیز ، سفارش شما به شماره {1} به مبلغ {2} با موفقیت ثبت شد.&#10;کنینا ایران"
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-medium leading-relaxed"
/>
</div>
{/* Preview Box */}
<div className="p-3.5 bg-slate-50 rounded-xl border border-slate-200 space-y-1">
<span className="text-[11px] font-bold text-gray-500">پیشنمایش الگو:</span>
<p className="text-xs text-gray-800 font-sans leading-relaxed">
{newPatternBody || 'متن الگو پس از تایپ در اینجا نمایش داده خواهد شد...'}
</p>
</div>
<div className="flex justify-end gap-3 pt-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
>
انصراف
</button>
<button
type="submit"
disabled={isSubmittingPattern}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-purple-200 disabled:opacity-50"
>
{isSubmittingPattern ? <Spinner size="sm" /> : <Save className="w-4 h-4" />}
ارسال و ثبت الگو در ملی پیامک
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal: Edit Pattern */}
{isEditModalOpen && editingPattern && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-xl rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Edit3 className="w-5 h-5 text-blue-600" />
ویرایش متن الگو (BodyId: {editingPattern.id})
</h3>
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleEditPattern} className="p-6 space-y-5">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">عنوان الگو</label>
<input
type="text"
disabled
value={editingPattern.title}
className="w-full border border-gray-200 rounded-xl p-3 bg-gray-100 text-sm font-bold text-gray-600"
/>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-bold text-gray-700">متن اصلاح شده الگو *</label>
<div className="flex items-center gap-1 text-[11px]">
<span className="text-gray-400">درج سریع:</span>
{['{0}', '{1}', '{2}'].map((v) => (
<button
key={v}
type="button"
onClick={() => setEditPatternBody((prev) => `${prev} ${v}`)}
className="bg-purple-50 text-purple-700 hover:bg-purple-100 font-mono font-bold px-1.5 py-0.5 rounded border border-purple-200"
>
+{v}
</button>
))}
</div>
</div>
<textarea
rows={4}
required
value={editPatternBody}
onChange={(e) => setEditPatternBody(e.target.value)}
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-blue-500 text-sm font-medium leading-relaxed"
/>
</div>
<div className="flex justify-end gap-3 pt-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
>
انصراف
</button>
<button
type="submit"
disabled={isSubmittingPattern}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-blue-200 disabled:opacity-50"
>
{isSubmittingPattern ? <Spinner size="sm" /> : <Save className="w-4 h-4" />}
ذخیره ویرایش الگو
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal: Log Details */}
{isLogDetailModalOpen && selectedLog && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-lg rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Info className="w-5 h-5 text-purple-600" />
جزییات پیامک ارسالی
</h3>
<button
type="button"
onClick={() => setIsLogDetailModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4 text-xs">
<div className="grid grid-cols-2 gap-3 bg-gray-50 p-4 rounded-xl border border-gray-100">
<div>
<span className="text-gray-400 font-medium">شماره گیرنده:</span>
<p className="font-mono font-bold text-gray-900 mt-0.5">{selectedLog.receptor}</p>
</div>
<div>
<span className="text-gray-400 font-medium">نوع پیامک:</span>
<p className="font-bold text-gray-900 mt-0.5">{getTypeName(selectedLog.type).label}</p>
</div>
<div>
<span className="text-gray-400 font-medium">شناسه الگو (Pattern ID):</span>
<p className="font-mono font-bold text-gray-900 mt-0.5">
{selectedLog.patternId ? `#${selectedLog.patternId}` : 'ارسال مستقیم'}
</p>
</div>
<div>
<span className="text-gray-400 font-medium">کد رهگیری ملی پیامک:</span>
<p className="font-mono font-bold text-gray-900 mt-0.5">
{selectedLog.recId || 'ثبت نشده'}
</p>
</div>
<div>
<span className="text-gray-400 font-medium">وضعیت ارسال:</span>
<p className="font-bold mt-0.5">
{selectedLog.status === 'SUCCESS' ? (
<span className="text-emerald-600">ارسال موفق (SUCCESS)</span>
) : (
<span className="text-rose-600">{selectedLog.status}</span>
)}
</p>
</div>
<div>
<span className="text-gray-400 font-medium">تاریخ و زمان ثبت:</span>
<p className="font-mono font-bold text-gray-900 mt-0.5">{formatDate(selectedLog.createdAt)}</p>
</div>
</div>
{selectedLog.args && selectedLog.args.length > 0 && (
<div className="space-y-1">
<span className="font-bold text-gray-700">آرگومانها و مقادیر ارسالی به پترن:</span>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 font-mono text-[11px] space-y-1">
{selectedLog.args.map((arg, idx) => (
<div key={idx} className="flex items-center gap-2">
<span className="text-purple-600 font-bold">{`{${idx}}`}:</span>
<span className="text-gray-800 font-sans">{arg}</span>
</div>
))}
</div>
</div>
)}
{selectedLog.errorMessage && (
<div className="space-y-1">
<span className="font-bold text-rose-700">توضیحات خطا / پاسخ درگاه:</span>
<div className="bg-rose-50 text-rose-800 p-3 rounded-xl border border-rose-200 text-[11px] leading-relaxed">
{selectedLog.errorMessage}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsLogDetailModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
>
بستن
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}