From 2f41d4afe7b4b9b93510ca399ff1bbf5edb7b544 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sat, 5 Sep 2026 09:21:01 +0330 Subject: [PATCH] fix(sms): parse melipayamak xml pattern tags and add search filter in admin panel --- backend/src/common/services/sms.service.ts | 32 ++- .../admin-panel/src/pages/SmsSettingsPage.tsx | 229 +++++++++++++----- 2 files changed, 201 insertions(+), 60 deletions(-) diff --git a/backend/src/common/services/sms.service.ts b/backend/src/common/services/sms.service.ts index bc8ed18..25bff53 100644 --- a/backend/src/common/services/sms.service.ts +++ b/backend/src/common/services/sms.service.ts @@ -494,26 +494,30 @@ export class SmsService { */ private parsePatternsXml(xml: string): MeliPayamakPattern[] { const patterns: MeliPayamakPattern[] = []; - const itemRegex = /([\s\S]*?)<\/SharedServiceBody>/gi; + // MeliPayamak uses and child tags , , <Body>, <BodyStatus> + // We match both <ShareServiceBody> and <SharedServiceBody> for backwards/cross compatibility + const itemRegex = /<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi; let match: RegExpExecArray | null; while ((match = itemRegex.exec(xml)) !== null) { const block = match[1]; - const idMatch = block.match(/<Id>(\d+)<\/Id>/i); + const idMatch = block.match(/<(?:BodyID|Id)>(\d+)<\/(?:BodyID|Id)>/i); const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i); const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i); - const statusMatch = block.match(/<Status>(-?\d+)<\/Status>/i); + const statusMatch = block.match(/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i); if (idMatch) { const id = parseInt(idMatch[1], 10); const title = (titleMatch ? titleMatch[1] : '') .replace(/</g, '<') .replace(/>/g, '>') - .replace(/&/g, '&'); + .replace(/&/g, '&') + .trim(); const body = (bodyMatch ? bodyMatch[1] : '') .replace(/</g, '<') .replace(/>/g, '>') - .replace(/&/g, '&'); + .replace(/&/g, '&') + .trim(); const status = statusMatch ? parseInt(statusMatch[1], 10) : 0; let statusText = 'در انتظار تایید'; @@ -611,6 +615,22 @@ export class SmsService { password: config.password, }); remotePatterns = this.parsePatternsXml(rawXml); + if (remotePatterns.length > 0) { + // Cache patterns asynchronously in database + this.prisma.setting + .upsert({ + where: { key: 'sms_patterns_cache' }, + update: { value: remotePatterns as any }, + create: { + key: 'sms_patterns_cache', + category: 'sms_patterns', + value: remotePatterns as any, + }, + }) + .catch((err) => { + this.logger.warn(`Failed to cache SMS patterns: ${err.message}`); + }); + } } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); this.logger.warn( @@ -619,7 +639,7 @@ export class SmsService { } } - // Load local stored patterns from settings table + // Load local stored patterns from settings table (fallback if offline or newly added locally) const storedSetting = await this.prisma.setting.findFirst({ where: { category: 'sms_patterns' }, }); diff --git a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx index 2a6cf77..eee3a0b 100644 --- a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx +++ b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx @@ -170,6 +170,8 @@ export default function SmsSettingsPage() { 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[]>([]); @@ -1419,58 +1421,174 @@ export default function SmsSettingsPage() { </button> </div> - {/* 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> + {/* 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; - <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> + 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; + }); - {/* 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"> + 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">اتصال به سیستم</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"> @@ -1587,10 +1705,13 @@ export default function SmsSettingsPage() { </tr> )) )} - </tbody> - </table> - </div> - </div> + </tbody> + </table> + </div> + </div> + </> + ); + })()} </div> ) : ( /* Logs & Tracking Tab */