canina/frontend/admin-panel/src/pages/SmsSettingsPage.tsx

2760 lines
131 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,
Zap,
CheckSquare,
Square,
Users,
Play,
FileText,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import { useSearchParams } from 'react-router-dom';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface SmsRule {
id: string;
title: string;
event: string;
enabled: boolean;
recipientType: 'customer' | 'admin' | 'custom';
customPhone?: string;
patternId: number;
variables: string[];
description?: string;
}
export interface SmsEventVariable {
key: string;
label: string;
sample: string;
}
export interface SmsEventDefinition {
event: string;
label: string;
description: string;
variables: SmsEventVariable[];
}
interface SmsConfigState {
enabled: boolean;
username: string;
password: string;
fromNumber: string;
adminPhone: string;
otpBodyId: number | string;
orderBodyId: number | string;
shippingBodyId: number | string;
b2bBodyId: number | string;
petCareBodyId: number | string;
rules: SmsRule[];
}
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;
}
const toPersianDigits = (n: string | number | null | undefined): string => {
if (n === null || n === undefined) return '';
const farsiDigits = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
return n.toString().replace(/\d/g, (x) => farsiDigits[parseInt(x, 10)]);
};
export default function SmsSettingsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const urlTab = searchParams.get('tab');
const validTabs: ('settings' | 'patterns' | 'triggers' | 'logs')[] = ['settings', 'patterns', 'triggers', 'logs'];
const activeTab: 'settings' | 'patterns' | 'triggers' | 'logs' =
urlTab && validTabs.includes(urlTab as any) ? (urlTab as any) : 'settings';
const setActiveTab = (tab: 'settings' | 'patterns' | 'triggers' | 'logs') => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('tab', tab);
return next;
}, { replace: false });
};
// Config State
const [config, setConfig] = useState<SmsConfigState>({
enabled: true,
username: '',
password: '',
fromNumber: '',
adminPhone: '',
otpBodyId: '508079',
orderBodyId: '508081',
shippingBodyId: '508082',
b2bBodyId: '508083',
petCareBodyId: '0',
rules: [],
});
const [patterns, setPatterns] = useState<PatternItem[]>([]);
const [eventDefinitions, setEventDefinitions] = useState<SmsEventDefinition[]>([]);
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);
// Trigger Rules Modal State
const [isRuleModalOpen, setIsRuleModalOpen] = useState(false);
const [editingRule, setEditingRule] = useState<SmsRule | null>(null);
const [ruleFormTitle, setRuleFormTitle] = useState('');
const [ruleFormEvent, setRuleFormEvent] = useState('ORDER_PAID');
const [ruleFormEnabled, setRuleFormEnabled] = useState(true);
const [ruleFormRecipientType, setRuleFormRecipientType] = useState<'customer' | 'admin' | 'custom'>('customer');
const [ruleFormCustomPhone, setRuleFormCustomPhone] = useState('');
const [ruleFormPatternId, setRuleFormPatternId] = useState('');
const [ruleFormVariables, setRuleFormVariables] = useState<string[]>([]);
const [ruleFormDescription, setRuleFormDescription] = useState('');
const [isTestingRuleId, setIsTestingRuleId] = useState<string | null>(null);
// User Picker for Custom Recipient
const [systemUsers, setSystemUsers] = useState<Array<{ id: string; firstName?: string; lastName?: string; mobile?: string; role?: string }>>([]);
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState('');
const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
// 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('');
const [patternFilter, setPatternFilter] = useState('');
const [patternStatusFilter, setPatternStatusFilter] = useState<'ALL' | 'APPROVED' | 'PENDING' | 'REJECTED'>('ALL');
// 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, resEvents] = await Promise.allSettled([
api.get('/settings/sms'),
api.get('/settings/sms/patterns'),
api.get('/settings/sms/credit'),
api.get('/settings/sms/events'),
]);
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 || '',
adminPhone: data.adminPhone || '',
otpBodyId: data.otpBodyId || '508079',
orderBodyId: data.orderBodyId || '508081',
shippingBodyId: data.shippingBodyId || '508082',
b2bBodyId: data.b2bBodyId || '508083',
petCareBodyId: data.petCareBodyId || '0',
rules: Array.isArray(data.rules) ? data.rules : [],
});
}
}
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);
}
}
if (resEvents.status === 'fulfilled') {
const eData = resEvents.value.data?.data || resEvents.value.data || [];
setEventDefinitions(Array.isArray(eData) ? eData : []);
}
} 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(),
adminPhone: (config.adminPhone || '').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,
rules: config.rules || [],
};
await api.patch('/settings/sms', payload);
toast.success('تنظیمات درگاه پیامک با موفقیت در دیتابیس ذخیره شد');
refreshPatterns();
} catch (err) {
console.error('Failed to update SMS settings:', err);
toast.error('خطا در ذخیره‌سازی تنظیمات پیامک');
} finally {
setIsSaving(false);
}
};
// Rule Handlers
const handleOpenAddRuleModal = (initialPatternId?: number, initialEvent?: string, initialTitle?: string) => {
setEditingRule(null);
setRuleFormTitle(initialTitle || '');
setRuleFormEvent(initialEvent || 'ORDER_PAID');
setRuleFormEnabled(true);
setRuleFormRecipientType('customer');
setRuleFormCustomPhone('');
setRuleFormPatternId(initialPatternId ? String(initialPatternId) : String(config.orderBodyId || ''));
setRuleFormVariables(['customerName', 'orderNumber', 'amount']);
setRuleFormDescription('');
setUserSearchQuery('');
setIsUserDropdownOpen(false);
setIsRuleModalOpen(true);
fetchSystemUsers();
};
const fetchSystemUsers = async (searchQuery = '') => {
try {
setIsLoadingUsers(true);
const res = await api.get(`/admin/users?limit=30${searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ''}`);
const list = res.data?.data || [];
setSystemUsers(list);
} catch (err) {
console.error('Failed to fetch system users', err);
} finally {
setIsLoadingUsers(false);
}
};
const handleOpenEditRuleModal = (rule: SmsRule) => {
setEditingRule(rule);
setRuleFormTitle(rule.title);
setRuleFormEvent(rule.event);
setRuleFormEnabled(rule.enabled);
setRuleFormRecipientType(rule.recipientType);
setRuleFormCustomPhone(rule.customPhone || '');
setRuleFormPatternId(String(rule.patternId || ''));
setRuleFormVariables(rule.variables || []);
setRuleFormDescription(rule.description || '');
setUserSearchQuery('');
setIsUserDropdownOpen(false);
setIsRuleModalOpen(true);
fetchSystemUsers();
};
const handleDuplicateRule = (rule: SmsRule) => {
setEditingRule(null); // Create new rule on save
setRuleFormTitle(`${rule.title} (کپی)`);
setRuleFormEvent(rule.event);
setRuleFormEnabled(rule.enabled);
setRuleFormRecipientType(rule.recipientType);
setRuleFormCustomPhone(rule.customPhone || '');
setRuleFormPatternId(String(rule.patternId || ''));
setRuleFormVariables(rule.variables ? [...rule.variables] : []);
setRuleFormDescription(rule.description ? `${rule.description} (رونوشت)` : '');
setIsRuleModalOpen(true);
toast.info('اطلاعات سناریو تکثیر شد. پس از بازبینی، ذخیره نمایید.');
};
const handleSaveRule = async (e: React.FormEvent) => {
e.preventDefault();
if (!ruleFormTitle.trim()) {
toast.error('لطفاً عنوان سناریو را وارد کنید');
return;
}
const pid = Number(ruleFormPatternId);
if (!pid || pid <= 0) {
toast.error('لطفاً شناسه الگو (Pattern ID) را مشخص کنید');
return;
}
if (ruleFormRecipientType === 'custom' && !ruleFormCustomPhone.trim()) {
toast.error('لطفاً شماره موبایل دلخواه گیرنده را وارد نمایید');
return;
}
const updatedRule: SmsRule = {
id: editingRule ? editingRule.id : `rule_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
title: ruleFormTitle.trim(),
event: ruleFormEvent,
enabled: ruleFormEnabled,
recipientType: ruleFormRecipientType,
customPhone: ruleFormRecipientType === 'custom' ? ruleFormCustomPhone.trim() : undefined,
patternId: pid,
variables: ruleFormVariables,
description: ruleFormDescription.trim() || undefined,
};
const newRules: SmsRule[] = editingRule
? (config.rules || []).map((r) => (r.id === editingRule.id ? updatedRule : r))
: [...(config.rules || []), updatedRule];
const newConfig = { ...config, rules: newRules };
setConfig(newConfig);
setIsRuleModalOpen(false);
try {
await api.patch('/settings/sms', {
...newConfig,
otpBodyId: Number(newConfig.otpBodyId) || 0,
orderBodyId: Number(newConfig.orderBodyId) || 0,
shippingBodyId: Number(newConfig.shippingBodyId) || 0,
b2bBodyId: Number(newConfig.b2bBodyId) || 0,
petCareBodyId: Number(newConfig.petCareBodyId) || 0,
});
toast.success(editingRule ? 'سناریو با موفقیت ویرایش و ذخیره شد' : 'سناریوی جدید اضافه و ذخیره شد');
} catch (err) {
console.error('Failed to save rules to backend:', err);
toast.error('خطا در ذخیره سناریو در سرور');
}
};
const handleToggleRule = async (ruleId: string) => {
const newRules = (config.rules || []).map((r) => {
if (r.id === ruleId) {
return { ...r, enabled: !r.enabled };
}
return r;
});
const newConfig = { ...config, rules: newRules };
setConfig(newConfig);
try {
await api.patch('/settings/sms', {
...newConfig,
otpBodyId: Number(newConfig.otpBodyId) || 0,
orderBodyId: Number(newConfig.orderBodyId) || 0,
shippingBodyId: Number(newConfig.shippingBodyId) || 0,
b2bBodyId: Number(newConfig.b2bBodyId) || 0,
petCareBodyId: Number(newConfig.petCareBodyId) || 0,
});
toast.success('وضعیت سناریو بروزرسانی شد');
} catch (err) {
console.error('Failed to toggle rule:', err);
toast.error('خطا در به‌روزرسانی وضعیت سناریو');
}
};
const handleDeleteRule = async (ruleId: string) => {
if (!window.confirm('آیا از حذف این سناریوی پیامکی اطمینان دارید؟')) return;
const newRules = (config.rules || []).filter((r) => r.id !== ruleId);
const newConfig = { ...config, rules: newRules };
setConfig(newConfig);
try {
await api.patch('/settings/sms', {
...newConfig,
otpBodyId: Number(newConfig.otpBodyId) || 0,
orderBodyId: Number(newConfig.orderBodyId) || 0,
shippingBodyId: Number(newConfig.shippingBodyId) || 0,
b2bBodyId: Number(newConfig.b2bBodyId) || 0,
petCareBodyId: Number(newConfig.petCareBodyId) || 0,
});
toast.success('سناریوی پیامک با موفقیت حذف شد');
} catch (err) {
console.error('Failed to delete rule:', err);
toast.error('خطا در حذف سناریو');
}
};
const handleTestRule = async (rule: SmsRule) => {
try {
setIsTestingRuleId(rule.id);
const res = await api.post('/settings/sms/test-rule', {
rule,
testPhone: testPhone ? testPhone.trim() : undefined,
});
const data = res.data?.data || res.data;
if (data && data.success) {
toast.success(`تست سناریو ارسال شد به: ${data.targetPhone || ''}`);
} else {
toast.error(data?.message || 'خطا در ارسال تست سناریو');
}
} catch (err: any) {
console.error('Failed to test rule:', err);
toast.error(err.response?.data?.message || 'خطای شبکه در تست سناریو');
} finally {
setIsTestingRuleId(null);
}
};
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('triggers')}
className={`px-3.5 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 ${
activeTab === 'triggers'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Zap className="w-4 h-4 text-amber-500" />
سناریوهای پیامک (Triggers)
{config.rules && config.rules.length > 0 && (
<span className="bg-amber-100 text-amber-800 text-[10px] px-1.5 py-0.2 rounded-full font-mono font-bold">
{config.rules.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 pl-11 pr-4 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-mono font-bold bg-white text-left"
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 rounded-lg hover:bg-gray-100 transition-colors"
title={showPassword ? 'پنهان کردن رمز' : 'نمایش رمز'}
>
{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 className="md:col-span-2">
<label className="block text-xs font-bold text-gray-700 mb-1 flex items-center gap-1.5">
<Users className="w-3.5 h-3.5 text-purple-600" />
شماره همراه مدیران جهت دریافت پیامکهای سیستم (Admin Phone)
</label>
<input
type="text"
value={config.adminPhone}
onChange={(e) => setConfig({ ...config, adminPhone: 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">
سناریوهایی که گیرنده آنها «مدیر سایت» تنظیم شده است، به این شمارهها پیامک ارسال خواهند کرد.
</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"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
ذخیره تنظیمات درگاه پیامک
</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="space-y-1.5">
{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 ? (
<>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-black text-amber-400 font-mono tracking-tight" dir="ltr">
{toPersianDigits(Math.floor(credit).toLocaleString())}
</span>
<span className="text-xs font-bold text-white/90">پالس (تعداد پیامک قابل ارسال)</span>
</div>
<div className="flex items-center gap-2 text-[11px] text-amber-300/90 font-medium bg-amber-500/10 border border-amber-500/20 px-2.5 py-1.5 rounded-lg">
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 1118 0z" />
</svg>
<span>
معادل تقریبی: <strong className="font-bold font-mono text-white" dir="ltr">{toPersianDigits(Math.round(credit * 292.7).toLocaleString())}</strong> تومان موجودی کیفپول پنل
</span>
</div>
</>
) : (
<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 === 'triggers' ? (
/* Triggers / Scenarios Tab */
<div className="space-y-6">
{/* Top Actions & Overview */}
<div className="bg-white p-5 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row items-center justify-between gap-4">
<div>
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Zap className="w-5 h-5 text-amber-500" />
سناریوها و قوانین ارسال پیامک خودکار (SMS Automation Rules)
</h3>
<p className="text-xs text-gray-500 mt-1">
برای هر رویداد (مانند ثبت یا پرداخت سفارش) میتوانید بینهایت پیامک همزمان به مشتری، مدیران یا شمارههای دلخواه (نظیر انبار و شرکت پست) با پترنهای اختصاصی تعریف کنید.
</p>
</div>
<button
type="button"
onClick={handleOpenAddRuleModal}
className="bg-amber-600 hover:bg-amber-700 text-white text-xs font-bold py-3 px-5 rounded-xl transition-all flex items-center gap-2 shadow-md shadow-amber-200 shrink-0"
>
<Plus className="w-4 h-4" />
تعریف سناریوی جدید (Add Rule)
</button>
</div>
{/* Rules List / Grid */}
{(!config.rules || config.rules.length === 0) ? (
<div className="bg-white p-12 rounded-2xl border border-gray-200 text-center space-y-4">
<div className="w-16 h-16 bg-amber-50 text-amber-600 rounded-2xl flex items-center justify-center mx-auto">
<Zap className="w-8 h-8" />
</div>
<div className="space-y-1">
<h4 className="font-bold text-gray-800 text-sm">هیچ سناریوی پیامکی فعالی وجود ندارد</h4>
<p className="text-xs text-gray-500 max-w-md mx-auto">
سیستم در این حالت از تنظیمات پیشفرض پترنها در تب پیکربندی استفاده میکند. با کلیک بر روی دکمه زیر میتوانید سناریوهای چندمقصدی بسازید.
</p>
</div>
<button
type="button"
onClick={handleOpenAddRuleModal}
className="bg-amber-600 hover:bg-amber-700 text-white text-xs font-bold py-2.5 px-5 rounded-xl transition-all inline-flex items-center gap-2 shadow-sm"
>
<Plus className="w-4 h-4" />
تعریف اولین سناریوی پیامکی
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{config.rules.map((rule) => {
const eventDef = eventDefinitions.find((e) => e.event === rule.event);
const matchedPattern = patterns.find((p) => Number(p.id) === Number(rule.patternId));
return (
<div
key={rule.id}
className={`bg-white rounded-2xl border transition-all shadow-sm flex flex-col justify-between overflow-hidden ${
rule.enabled ? 'border-gray-200 hover:border-amber-400' : 'border-gray-200 opacity-60 bg-gray-50/50'
}`}
>
<div className="p-5 space-y-4">
{/* Header: Title, Active Switch, Badges */}
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span
className={`text-[10px] font-black px-2 py-0.5 rounded-full border ${
rule.recipientType === 'customer'
? 'bg-blue-50 text-blue-700 border-blue-200'
: rule.recipientType === 'admin'
? 'bg-purple-50 text-purple-700 border-purple-200'
: 'bg-emerald-50 text-emerald-700 border-emerald-200'
}`}
>
{rule.recipientType === 'customer'
? 'خریدار / مشتری'
: rule.recipientType === 'admin'
? 'مدیران سایت'
: `شماره دلخواه: ${rule.customPhone || ''}`}
</span>
<span className="text-[10px] bg-slate-100 text-slate-700 font-bold px-2 py-0.5 rounded-full border border-slate-200">
{eventDef ? eventDef.label : rule.event}
</span>
</div>
<h4 className="font-bold text-gray-900 text-sm truncate pt-1">{rule.title}</h4>
{rule.description && (
<p className="text-xs text-gray-500 leading-relaxed line-clamp-2">
{rule.description}
</p>
)}
</div>
{/* Toggle Button */}
<button
type="button"
onClick={() => handleToggleRule(rule.id)}
className={`p-1.5 rounded-xl border transition-colors shrink-0 ${
rule.enabled
? 'bg-emerald-50 border-emerald-200 text-emerald-600 hover:bg-emerald-100'
: 'bg-gray-100 border-gray-300 text-gray-400 hover:bg-gray-200'
}`}
title={rule.enabled ? 'سناریو فعال است (کلیک برای غیرفعال کردن)' : 'سناریو غیرفعال است (کلیک برای فعال‌سازی)'}
>
{rule.enabled ? <CheckSquare className="w-5 h-5" /> : <Square className="w-5 h-5" />}
</button>
</div>
{/* Pattern ID & Variables Details */}
<div className="bg-gray-50 rounded-xl p-3 border border-gray-100 space-y-2.5 text-xs">
<div className="flex items-center justify-between">
<span className="text-gray-500 font-medium">شناسه پترن ملی پیامک:</span>
<span className="font-mono font-black text-purple-700 bg-purple-50 px-2 py-0.5 rounded border border-purple-200">
#{rule.patternId}
</span>
</div>
{matchedPattern && (
<div className="text-[11px] text-gray-600 bg-white p-2 rounded-lg border border-gray-200 leading-relaxed font-sans">
<span className="font-bold text-gray-700 block mb-0.5">متن الگو:</span>
{matchedPattern.body}
</div>
)}
<div className="space-y-1">
<span className="text-gray-500 font-medium block">ترتیب متغیرهای انتسابی:</span>
{rule.variables && rule.variables.length > 0 ? (
<div className="flex flex-wrap gap-1.5 pt-0.5">
{rule.variables.map((vKey, vIdx) => {
const varMeta = eventDef?.variables.find((v) => v.key === vKey);
return (
<span
key={vIdx}
className="bg-white border border-gray-200 text-gray-700 text-[11px] font-mono font-bold px-2 py-0.5 rounded shadow-2xs flex items-center gap-1"
>
<span className="text-amber-600 font-black">{`{${vIdx}}`}:</span>
<span className="font-sans font-medium text-[10px]">
{varMeta ? varMeta.label : vKey}
</span>
</span>
);
})}
</div>
) : (
<span className="text-gray-400 text-[11px]">بدون متغیر داینامیک</span>
)}
</div>
</div>
</div>
{/* Bottom Action Footer */}
<div className="px-5 py-3 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
<button
type="button"
disabled={isTestingRuleId === rule.id}
onClick={() => handleTestRule(rule)}
className="text-xs font-bold text-amber-700 hover:text-amber-900 flex items-center gap-1.5 transition-colors disabled:opacity-50"
>
{isTestingRuleId === rule.id ? (
<Spinner size="sm" className="text-amber-600" />
) : (
<Play className="w-3.5 h-3.5 fill-current" />
)}
<span>تست ارسال سناریو</span>
</button>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => handleDuplicateRule(rule)}
className="p-1.5 text-gray-500 hover:text-purple-600 hover:bg-white rounded-lg border border-transparent hover:border-gray-200 transition-all text-xs font-bold flex items-center gap-1"
title="تکثیر / ساخت کپی از سناریو"
>
<Copy className="w-3.5 h-3.5" />
<span>تکثیر</span>
</button>
<button
type="button"
onClick={() => handleOpenEditRuleModal(rule)}
className="p-1.5 text-gray-500 hover:text-blue-600 hover:bg-white rounded-lg border border-transparent hover:border-gray-200 transition-all text-xs font-bold flex items-center gap-1"
title="ویرایش سناریو"
>
<Edit3 className="w-3.5 h-3.5" />
<span>ویرایش</span>
</button>
<button
type="button"
onClick={() => handleDeleteRule(rule.id)}
className="p-1.5 text-gray-400 hover:text-rose-600 hover:bg-white rounded-lg border border-transparent hover:border-gray-200 transition-all"
title="حذف سناریو"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
) : activeTab === 'patterns' ? (
/* Patterns Management Tab */
<div className="space-y-6">
{/* Guide Banner for Multi-Pattern Scenarios */}
<div className="bg-gradient-to-r from-purple-50 via-blue-50 to-indigo-50 p-4 sm:p-5 rounded-2xl border border-purple-200/80 shadow-xs flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-600 text-white flex items-center justify-center shrink-0 shadow-sm mt-0.5">
<Zap className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-black text-purple-950 font-vazir mb-1">
ارسال چند پیامک همزمان با الگوهای مختلف روی یک رویداد
</h4>
<p className="text-xs text-purple-800/90 leading-relaxed font-sans">
برای یک رویداد (مانند <strong>«تایید و ثبت سفارش»</strong>)، میتوانید نامحدود الگوی پیامکی مجزا تعریف کنید: مثلاً یک پیامک به خریدار، یک پیامک اختصاصی به مدیر فروشگاه، و یک پیامک به شرکت حملونقل با شماره دلخواه! کافیست از دکمه <strong>«افزودن به سناریوها»</strong> در جلوی هر الگو استفاده نمایید.
</p>
</div>
</div>
<button
type="button"
onClick={() => setActiveTab('triggers')}
className="whitespace-nowrap px-3.5 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all shadow-sm shrink-0 flex items-center gap-1.5"
>
<span>مشاهده و مدیریت سناریوها</span>
<ArrowUpRight className="w-4 h-4" />
</button>
</div>
{/* Action & Filter Bar */}
{(() => {
const trimmedFilter = patternFilter.trim().toLowerCase();
const filteredPatterns = patterns.filter((p) => {
if (patternStatusFilter === 'APPROVED' && p.status !== 1) return false;
if (patternStatusFilter === 'PENDING' && p.status !== 0) return false;
if (patternStatusFilter === 'REJECTED' && p.status !== 2 && p.status !== -1) return false;
if (!trimmedFilter) return true;
const matchesId = String(p.id).includes(trimmedFilter);
const matchesTitle = (p.title || '').toLowerCase().includes(trimmedFilter);
const matchesBody = (p.body || '').toLowerCase().includes(trimmedFilter);
const matchesAssigned = (p.assignedTo || []).some((a) => a.toLowerCase().includes(trimmedFilter));
return matchesId || matchesTitle || matchesBody || matchesAssigned;
});
return (
<>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4">
{/* Search and Filters */}
<div className="flex flex-1 flex-col sm:flex-row items-stretch sm:items-center gap-3">
{/* Search Input */}
<div className="relative flex-1 max-w-md">
<div className="absolute inset-y-0 right-0 pr-3.5 flex items-center pointer-events-none text-gray-400">
<Search className="w-4 h-4" />
</div>
<input
type="text"
value={patternFilter}
onChange={(e) => setPatternFilter(e.target.value)}
placeholder="جستجو در متن، عنوان یا کد الگو..."
className="w-full pr-10 pl-9 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium text-gray-800 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all"
/>
{patternFilter && (
<button
type="button"
onClick={() => setPatternFilter('')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* Status Filter */}
<div className="flex items-center gap-1.5 bg-gray-100/80 p-1 rounded-xl border border-gray-200/60 text-xs font-bold shrink-0">
<button
type="button"
onClick={() => setPatternStatusFilter('ALL')}
className={`px-3 py-1.5 rounded-lg transition-all ${
patternStatusFilter === 'ALL'
? 'bg-white text-purple-700 shadow-xs'
: 'text-gray-600 hover:text-gray-900'
}`}
>
همه ({patterns.length})
</button>
<button
type="button"
onClick={() => setPatternStatusFilter('APPROVED')}
className={`px-3 py-1.5 rounded-lg transition-all ${
patternStatusFilter === 'APPROVED'
? 'bg-white text-emerald-700 shadow-xs'
: 'text-gray-600 hover:text-gray-900'
}`}
>
تایید شده
</button>
<button
type="button"
onClick={() => setPatternStatusFilter('PENDING')}
className={`px-3 py-1.5 rounded-lg transition-all ${
patternStatusFilter === 'PENDING'
? 'bg-white text-amber-700 shadow-xs'
: 'text-gray-600 hover:text-gray-900'
}`}
>
در انتظار
</button>
<button
type="button"
onClick={() => setPatternStatusFilter('REJECTED')}
className={`px-3 py-1.5 rounded-lg transition-all ${
patternStatusFilter === 'REJECTED'
? 'bg-white text-rose-700 shadow-xs'
: 'text-gray-600 hover:text-gray-900'
}`}
>
نیازمند ویرایش
</button>
</div>
</div>
{/* Actions and Counter */}
<div className="flex items-center justify-between sm:justify-end gap-3 shrink-0">
<button
type="button"
onClick={refreshPatterns}
disabled={isLoadingPatterns}
className="p-2.5 text-gray-600 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 className="hidden sm:inline">بروزرسانی از ملی پیامک</span>
</button>
<button
type="button"
onClick={() => setIsAddModalOpen(true)}
className="bg-purple-600 hover:bg-purple-700 text-white text-xs font-bold py-2.5 px-4 rounded-xl transition-all flex items-center gap-2 shadow-md shadow-purple-200"
>
<Plus className="w-4 h-4" />
<span>درج الگوی جدید</span>
</button>
</div>
</div>
{/* Filter info banner if filtering */}
{(patternFilter || patternStatusFilter !== 'ALL') && (
<div className="flex items-center justify-between px-3 py-2 bg-purple-50/60 rounded-xl border border-purple-100 text-xs text-purple-900 font-medium">
<div className="flex items-center gap-2">
<Filter className="w-3.5 h-3.5 text-purple-600" />
<span>
نمایش <strong>{filteredPatterns.length}</strong> از <strong>{patterns.length}</strong> الگو
{patternFilter && (
<> برای عبارت «<span className="font-bold text-purple-700">{patternFilter}</span>»</>
)}
</span>
</div>
<button
type="button"
onClick={() => {
setPatternFilter('');
setPatternStatusFilter('ALL');
}}
className="text-purple-600 hover:text-purple-800 font-bold hover:underline text-[11px]"
>
پاک کردن فیلترها
</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">سناریوهای متصل (Triggers)</th>
<th className="p-4 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-medium">
{filteredPatterns.length === 0 ? (
<tr>
<td colSpan={6} className="p-8 text-center text-gray-400 text-xs">
{patterns.length === 0
? 'هیچ الگویی یافت نشد. می‌توانید با دکمه «درج الگوی جدید» اولین پترن خود را ثبت کنید.'
: 'هیچ الگویی با شرایط فیلتر جستجو شده مطابقت ندارد.'}
</td>
</tr>
) : (
filteredPatterns.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.5 max-w-xs">
{p.assignedTo.map((a, i) => (
<span
key={i}
className="inline-flex items-center gap-1 bg-purple-50 text-purple-800 text-[11px] font-bold px-2 py-0.5 rounded-lg border border-purple-200"
>
<Zap className="w-2.5 h-2.5 text-purple-600" />
<span>{a}</span>
</span>
))}
</div>
) : (
<span className="text-gray-400 text-[11px] italic">آزاد (بدون سناریو)</span>
)}
</td>
<td className="p-4 text-center">
<div className="flex items-center justify-center gap-2">
<button
type="button"
onClick={() => {
handleOpenAddRuleModal(p.id, 'ORDER_PAID', `ارسال با الگوی ${p.id} - ${p.title}`);
}}
className="px-3 py-1.5 bg-amber-50 text-amber-800 hover:bg-amber-100 rounded-xl text-xs font-bold transition-all border border-amber-200 flex items-center gap-1.5 shadow-2xs"
title="اتصال این الگو به یک سناریوی ارسال پیامک (مانند ارسال به مشتری، ادمین، انبار یا شخص خاص)"
>
<Zap className="w-3.5 h-3.5 text-amber-600" />
<span>اتصال به سناریو</span>
</button>
<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-xl border border-gray-200 hover:border-blue-200 transition-all"
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 max-w-xs">
{(() => {
let snippet = log.messageText;
if (!snippet && log.patternId) {
const pat = patterns.find((p) => p.id === log.patternId);
if (pat && pat.body) {
let rendered = pat.body;
(log.args || []).forEach((val, idx) => {
rendered = rendered.replace(new RegExp(`\\{${idx}\\}`, 'g'), val || '');
});
snippet = rendered;
}
}
return (
<div>
{log.patternId ? (
<div className="flex items-center gap-1.5 flex-wrap mb-1">
<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-[10px]">
[{log.args.join(' , ')}]
</span>
)}
</div>
) : (
<span className="text-gray-400 font-sans text-[11px] block mb-1">متنی مستقیم</span>
)}
{snippet && (
<p className="font-sans text-[11px] text-gray-600 line-clamp-1 truncate" title={snippet}>
{snippet}
</p>
)}
</div>
);
})()}
</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/Edit SMS Automation Rule */}
<Modal
isOpen={isRuleModalOpen}
onClose={() => setIsRuleModalOpen(false)}
title={editingRule ? 'ویرایش سناریوی پیامک' : 'تعریف سناریوی پیامکی جدید'}
icon={Zap}
maxWidth="2xl"
>
<form onSubmit={handleSaveRule} className="space-y-4">
{/* Rule Title */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">
عنوان سناریو (جهت تفکیک در لیست) *
</label>
<input
type="text"
required
value={ruleFormTitle}
onChange={(e) => setRuleFormTitle(e.target.value)}
placeholder="مثلاً: ارسال مشخصات گیرنده به شرکت پست / انبار"
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-amber-500 text-sm font-bold"
/>
</div>
{/* Event & Recipient Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Trigger Event */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">رویداد تحریککننده (Trigger Event) *</label>
<select
value={ruleFormEvent}
onChange={(e) => {
const newEv = e.target.value;
setRuleFormEvent(newEv);
const def = eventDefinitions.find((ed) => ed.event === newEv);
if (def && def.variables.length > 0) {
setRuleFormVariables(def.variables.slice(0, 3).map((v) => v.key));
}
}}
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-bold bg-white"
>
{eventDefinitions.length > 0 ? (
eventDefinitions.map((ed) => (
<option key={ed.event} value={ed.event}>
{ed.label}
</option>
))
) : (
<>
<option value="ORDER_PAID">پرداخت موفق سفارش (Order Paid)</option>
<option value="ORDER_CREATED">ثبت سفارش کارت به کارت / در انتظار</option>
<option value="ORDER_SHIPPED">ارسال مرسوله و صدور بارکد پستی</option>
<option value="WALLET_CHARGED">شارژ و افزایش موجودی کیف پول</option>
<option value="B2B_SUBMITTED">ثبت درخواست همکاری B2B</option>
<option value="CONTACT_SUBMITTED">ثبت پیام در فرم تماس با ما</option>
</>
)}
</select>
</div>
{/* Recipient Type */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">گیرنده پیامک (Recipient) *</label>
<select
value={ruleFormRecipientType}
onChange={(e) => setRuleFormRecipientType(e.target.value as any)}
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-bold bg-white"
>
<option value="customer">خریدار / مشتری (شماره ثبتشده در سفارش یا فرم)</option>
<option value="admin">مدیر سایت (شمارههای تنظیمشده در پنل)</option>
<option value="custom">شماره همراه دلخواه (انبار، واحد ارسال، حسابداری)</option>
</select>
</div>
</div>
{/* Custom Phone Number Input / User Picker if custom */}
{ruleFormRecipientType === 'custom' && (
<div className="bg-amber-50/70 p-4 rounded-xl border border-amber-200 space-y-3">
<div className="flex items-center justify-between">
<label className="block text-xs font-bold text-amber-900">
شمارههای موبایل دریافتکننده (Custom Phone Numbers) *
</label>
<span className="text-[11px] text-amber-700 font-medium">
انتخاب از کاربران/مدیران سیستم یا درج مستقیم
</span>
</div>
{/* Searchable User Selector */}
<div className="relative">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="w-3.5 h-3.5 absolute right-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
value={userSearchQuery}
onChange={(e) => {
const q = e.target.value;
setUserSearchQuery(q);
setIsUserDropdownOpen(true);
fetchSystemUsers(q);
}}
onFocus={() => {
setIsUserDropdownOpen(true);
if (systemUsers.length === 0) fetchSystemUsers();
}}
placeholder="جستجوی کاربر یا مدیر در سیستم (نام، شماره تماس)..."
className="w-full pr-8 pl-3 py-2 bg-white border border-amber-300 rounded-xl text-xs outline-none focus:ring-2 focus:ring-amber-500"
/>
</div>
<button
type="button"
onClick={() => setIsUserDropdownOpen((prev) => !prev)}
className="px-3 py-2 bg-amber-200/70 hover:bg-amber-200 text-amber-900 rounded-xl text-xs font-bold shrink-0 transition-all flex items-center gap-1 cursor-pointer"
>
<Users className="w-3.5 h-3.5" />
<span>لیست کاربران</span>
</button>
</div>
{/* Dropdown list */}
{isUserDropdownOpen && (
<div className="absolute z-50 mt-1.5 w-full bg-white border border-gray-200 rounded-xl shadow-xl max-h-56 overflow-y-auto divide-y divide-gray-100">
<div className="p-2 bg-gray-50 flex items-center justify-between text-[11px] text-gray-500 font-bold">
<span>انتخاب کاربر برای اضافه شدن به لیست شمارهها:</span>
<button
type="button"
onClick={() => setIsUserDropdownOpen(false)}
className="text-gray-400 hover:text-gray-700 cursor-pointer"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
{isLoadingUsers ? (
<div className="p-4 text-center text-xs text-gray-500">در حال بارگذاری کاربران...</div>
) : systemUsers.length === 0 ? (
<div className="p-4 text-center text-xs text-gray-500">کاربری یافت نشد</div>
) : (
systemUsers.map((u) => {
const uName = (u.firstName ? `${u.firstName} ${u.lastName || ''}` : 'بدون نام').trim();
const uPhone = u.mobile || '';
const isAlreadySelected = uPhone && ruleFormCustomPhone.includes(uPhone);
const roleBadge = u.role === 'Admin' ? 'مدیر' : u.role === 'Vet' ? 'دامپزشک' : 'کاربر';
return (
<button
key={u.id}
type="button"
onClick={() => {
if (!uPhone) {
toast.error('این کاربر شماره همراه ثبت‌شده ندارد');
return;
}
const currentPhones = ruleFormCustomPhone
.split(/[,;\s]+/)
.map((p) => p.trim())
.filter(Boolean);
if (currentPhones.includes(uPhone)) {
// Remove
const filtered = currentPhones.filter((p) => p !== uPhone);
setRuleFormCustomPhone(filtered.join(' ، '));
toast.success(`${uName} از گیرندگان حذف شد`);
} else {
// Add
const next = [...currentPhones, uPhone];
setRuleFormCustomPhone(next.join(' ، '));
toast.success(`${uName} (${uPhone}) به گیرندگان اضافه شد`);
}
}}
className={`w-full p-2.5 text-right flex items-center justify-between hover:bg-amber-50/50 transition-colors text-xs cursor-pointer ${
isAlreadySelected ? 'bg-amber-50' : ''
}`}
>
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-amber-100 text-amber-800 font-bold flex items-center justify-center text-xs shrink-0">
{uName.charAt(0)}
</div>
<div>
<div className="font-bold text-gray-900 flex items-center gap-1.5">
<span>{uName}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-600 font-normal">
{roleBadge}
</span>
</div>
<div className="text-[11px] text-gray-500 font-mono" dir="ltr">
{uPhone || 'بدون شماره'}
</div>
</div>
</div>
<div className="shrink-0">
{isAlreadySelected ? (
<span className="text-emerald-600 font-bold flex items-center gap-1 text-[11px]">
<Check className="w-3.5 h-3.5" /> انتخاب شده
</span>
) : (
<span className="text-purple-600 hover:underline text-[11px] font-bold">
+ افزودن
</span>
)}
</div>
</button>
);
})
)}
</div>
)}
</div>
<div>
<input
type="text"
required
value={ruleFormCustomPhone}
onChange={(e) => setRuleFormCustomPhone(e.target.value)}
placeholder="مثلاً: 09123456789 (برای چند شماره با کاما یا ویرگول جدا کنید)"
className="w-full border border-amber-300 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white"
dir="ltr"
/>
</div>
<p className="text-[11px] text-amber-700">
💡 میتوانید از جستجوی بالا مدیر یا کاربر مدنظرتان را انتخاب کنید یا شمارهها را مستقیماً تایپ نمایید.
</p>
</div>
)}
{/* Pattern ID Selection */}
<div className="space-y-2 bg-gray-50 p-4 rounded-xl border border-gray-200">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-800">
شناسه الگوی ملی پیامک (Pattern Body ID) *
</label>
<span className="text-[11px] text-gray-500">انتخاب از الگوها یا درج دستی</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* Select from registered patterns */}
<div>
<select
value={ruleFormPatternId}
onChange={(e) => setRuleFormPatternId(e.target.value)}
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-bold bg-white"
>
<option value="">انتخاب از لیست الگوهای موجود...</option>
{patterns.map((p) => (
<option key={p.id} value={p.id}>
#{p.id} - {p.title}
</option>
))}
</select>
</div>
{/* Or Manual Body ID Input */}
<div>
<input
type="number"
required
value={ruleFormPatternId}
onChange={(e) => setRuleFormPatternId(e.target.value)}
placeholder="یا وارد کردن شناسه پترن جدید (مثلاً: 508081)"
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white"
dir="ltr"
/>
</div>
</div>
{/* Show Preview of Selected Pattern Text if found */}
{(() => {
const selPat = patterns.find((p) => String(p.id) === String(ruleFormPatternId));
if (!selPat) return null;
return (
<div className="text-[11px] text-gray-600 bg-white p-2.5 rounded-lg border border-gray-200 leading-relaxed font-sans mt-1">
<span className="font-bold text-gray-700 block mb-0.5">متن الگو:</span>
{selPat.body}
</div>
);
})()}
</div>
{/* Dynamic Variables Mapper */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-800">
تخصیص متغیرها به ترتیب پارامترهای پترن ({'{0}'}, {'{1}'}, ...)
</label>
<span className="text-[11px] text-amber-700 font-medium">
کلیک روی متغیرها برای افزودن یا حذف
</span>
</div>
{/* Current mapped variables */}
<div className="bg-amber-50/40 p-3 rounded-xl border border-amber-200/80 min-h-[50px] flex items-center flex-wrap gap-2">
{ruleFormVariables.length === 0 ? (
<span className="text-xs text-gray-400 italic">
هیچ متغیری انتخاب نشده است. از لیست زیر متغیرهای مورد نظر را اضافه کنید.
</span>
) : (
ruleFormVariables.map((vKey, idx) => {
const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent);
const varMeta = curEventDef?.variables.find((v) => v.key === vKey);
return (
<div
key={idx}
className="bg-white border border-amber-300 text-gray-800 text-xs font-bold px-2.5 py-1 rounded-lg shadow-2xs flex items-center gap-1.5 animate-in fade-in zoom-in duration-100"
>
<span className="text-amber-600 font-mono font-black">{`{${idx}}`}:</span>
<span>{varMeta ? varMeta.label : vKey}</span>
<button
type="button"
onClick={() => {
const updated = [...ruleFormVariables];
updated.splice(idx, 1);
setRuleFormVariables(updated);
}}
className="text-gray-400 hover:text-rose-600 p-0.5 rounded"
title="حذف این متغیر"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
);
})
)}
</div>
{/* Available Variables for the selected event */}
{(() => {
const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent);
if (!curEventDef || curEventDef.variables.length === 0) return null;
return (
<div className="space-y-1.5 pt-1">
<span className="text-[11px] text-gray-500 font-bold block">
متغیرهای در دسترس برای رویداد «{curEventDef.label}»:
</span>
<div className="flex flex-wrap gap-1.5">
{curEventDef.variables.map((v) => (
<button
key={v.key}
type="button"
onClick={() => {
setRuleFormVariables([...ruleFormVariables, v.key]);
}}
className="text-[11px] bg-gray-100 hover:bg-amber-100 hover:text-amber-900 text-gray-700 px-2.5 py-1 rounded-lg border border-gray-200 transition-colors flex items-center gap-1 font-medium"
>
<Plus className="w-3 h-3 text-gray-500" />
<span>{v.label}</span>
<span className="text-[9px] text-gray-400 font-mono">({v.sample})</span>
</button>
))}
</div>
</div>
);
})()}
</div>
{/* Optional Description */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">
توضیحات و یادداشت داخلی (اختیاری)
</label>
<textarea
rows={2}
value={ruleFormDescription}
onChange={(e) => setRuleFormDescription(e.target.value)}
placeholder="مثلاً: ارسال فاکتور خرید و آدرس مشتری به واحد بسته‌بندی انبار تهران"
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs leading-relaxed font-medium"
/>
</div>
{/* Enabled toggle */}
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200">
<span className="text-xs font-bold text-gray-800">فعال بودن این سناریو</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={ruleFormEnabled}
onChange={(e) => setRuleFormEnabled(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-amber-600"></div>
</label>
</div>
{/* Submit Buttons */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100 shrink-0">
<button
type="button"
onClick={() => setIsRuleModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50 cursor-pointer"
>
انصراف
</button>
<button
type="submit"
className="bg-amber-600 hover:bg-amber-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-amber-200 cursor-pointer"
>
<Save className="w-4 h-4" />
{editingRule ? 'ذخیره تغییرات سناریو' : 'ثبت و فعال‌سازی سناریو'}
</button>
</div>
</form>
</Modal>
{/* Modal: Add New Pattern */}
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="درج الگوی جدید در سامانه ملی پیامک"
icon={Plus}
maxWidth="xl"
>
<form onSubmit={handleCreatePattern} className="space-y-4">
<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"
variant="outline"
onClick={() => setIsAddModalOpen(false)}
className="px-5 py-2 text-xs font-bold"
>
انصراف
</Button>
<Button
type="submit"
variant="primary"
disabled={isSubmittingPattern}
isLoading={isSubmittingPattern}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold px-6 py-2 text-xs"
>
ارسال و ثبت الگو در ملی پیامک
</Button>
</div>
</form>
</Modal>
{/* Modal: Edit Pattern */}
<Modal
isOpen={isEditModalOpen && !!editingPattern}
onClose={() => setIsEditModalOpen(false)}
title={`ویرایش متن الگو (BodyId: ${editingPattern?.id || ''})`}
icon={Edit3}
maxWidth="xl"
>
{editingPattern && (
<form onSubmit={handleEditPattern} className="space-y-4">
<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"
variant="outline"
onClick={() => setIsEditModalOpen(false)}
className="px-5 py-2 text-xs font-bold"
>
انصراف
</Button>
<Button
type="submit"
variant="primary"
disabled={isSubmittingPattern}
isLoading={isSubmittingPattern}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-2 text-xs"
>
ذخیره ویرایش الگو
</Button>
</div>
</form>
)}
</Modal>
{/* Modal: Log Details */}
<Modal
isOpen={isLogDetailModalOpen && !!selectedLog}
onClose={() => setIsLogDetailModalOpen(false)}
title="جزییات پیامک ارسالی"
icon={Info}
maxWidth="2xl"
footer={
<Button
variant="outline"
onClick={() => setIsLogDetailModalOpen(false)}
className="px-5 py-2 text-xs font-bold"
>
بستن
</Button>
}
>
{selectedLog && (
<div className="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>
{/* Full SMS Message Text */}
{(() => {
let fullText = selectedLog.messageText;
if (!fullText && selectedLog.patternId) {
const pat = patterns.find((p) => p.id === selectedLog.patternId);
if (pat && pat.body) {
let rendered = pat.body;
(selectedLog.args || []).forEach((val, idx) => {
rendered = rendered.replace(new RegExp(`\\{${idx}\\}`, 'g'), val || '');
});
fullText = rendered;
}
}
return (
<div className="space-y-1.5">
<span className="font-bold text-gray-800 flex items-center gap-1.5">
<FileText className="w-3.5 h-3.5 text-purple-600" />
<span>متن کامل پیامک ارسالی:</span>
</span>
<div className="bg-purple-50/50 p-4 rounded-xl border border-purple-200/70 font-sans text-xs text-gray-800 leading-relaxed whitespace-pre-wrap select-text max-h-60 overflow-y-auto">
{fullText || (selectedLog.args && selectedLog.args.length > 0 ? selectedLog.args.join(' ') : 'متن ثبت نشده است.')}
</div>
</div>
);
})()}
{selectedLog.args && selectedLog.args.length > 0 && (
<div className="space-y-1.5">
<span className="font-bold text-gray-700">آرگومانها و مقادیر ارسالی به پترن:</span>
<div className="bg-slate-50 p-3.5 rounded-xl border border-slate-200 font-mono text-[11px] space-y-1.5 max-h-48 overflow-y-auto">
{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.5">
<span className="font-bold text-rose-700">توضیحات خطا / پاسخ درگاه:</span>
<div className="bg-rose-50 text-rose-800 p-3.5 rounded-xl border border-rose-200 text-[11px] leading-relaxed max-h-36 overflow-y-auto">
{selectedLog.errorMessage}
</div>
</div>
)}
</div>
)}
</Modal>
</div>
);
}