fix(sms): parse melipayamak xml pattern tags and add search filter in admin panel
This commit is contained in:
parent
99e726b08b
commit
2f41d4afe7
@ -494,26 +494,30 @@ export class SmsService {
|
|||||||
*/
|
*/
|
||||||
private parsePatternsXml(xml: string): MeliPayamakPattern[] {
|
private parsePatternsXml(xml: string): MeliPayamakPattern[] {
|
||||||
const patterns: MeliPayamakPattern[] = [];
|
const patterns: MeliPayamakPattern[] = [];
|
||||||
const itemRegex = /<SharedServiceBody>([\s\S]*?)<\/SharedServiceBody>/gi;
|
// MeliPayamak uses <ShareServiceBody> and child tags <BodyID>, <Title>, <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;
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
while ((match = itemRegex.exec(xml)) !== null) {
|
while ((match = itemRegex.exec(xml)) !== null) {
|
||||||
const block = match[1];
|
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 titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
|
||||||
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/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) {
|
if (idMatch) {
|
||||||
const id = parseInt(idMatch[1], 10);
|
const id = parseInt(idMatch[1], 10);
|
||||||
const title = (titleMatch ? titleMatch[1] : '')
|
const title = (titleMatch ? titleMatch[1] : '')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/&/g, '&');
|
.replace(/&/g, '&')
|
||||||
|
.trim();
|
||||||
const body = (bodyMatch ? bodyMatch[1] : '')
|
const body = (bodyMatch ? bodyMatch[1] : '')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/&/g, '&');
|
.replace(/&/g, '&')
|
||||||
|
.trim();
|
||||||
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
||||||
|
|
||||||
let statusText = 'در انتظار تایید';
|
let statusText = 'در انتظار تایید';
|
||||||
@ -611,6 +615,22 @@ export class SmsService {
|
|||||||
password: config.password,
|
password: config.password,
|
||||||
});
|
});
|
||||||
remotePatterns = this.parsePatternsXml(rawXml);
|
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) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
this.logger.warn(
|
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({
|
const storedSetting = await this.prisma.setting.findFirst({
|
||||||
where: { category: 'sms_patterns' },
|
where: { category: 'sms_patterns' },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -170,6 +170,8 @@ export default function SmsSettingsPage() {
|
|||||||
const [isSubmittingPattern, setIsSubmittingPattern] = useState(false);
|
const [isSubmittingPattern, setIsSubmittingPattern] = useState(false);
|
||||||
const [editingPattern, setEditingPattern] = useState<PatternItem | null>(null);
|
const [editingPattern, setEditingPattern] = useState<PatternItem | null>(null);
|
||||||
const [editPatternBody, setEditPatternBody] = useState('');
|
const [editPatternBody, setEditPatternBody] = useState('');
|
||||||
|
const [patternFilter, setPatternFilter] = useState('');
|
||||||
|
const [patternStatusFilter, setPatternStatusFilter] = useState<'ALL' | 'APPROVED' | 'PENDING' | 'REJECTED'>('ALL');
|
||||||
|
|
||||||
// Logs State
|
// Logs State
|
||||||
const [logs, setLogs] = useState<SmsLogItem[]>([]);
|
const [logs, setLogs] = useState<SmsLogItem[]>([]);
|
||||||
@ -1419,58 +1421,174 @@ export default function SmsSettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Bar */}
|
{/* Action & Filter 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">
|
const trimmedFilter = patternFilter.trim().toLowerCase();
|
||||||
<span className="text-sm font-bold text-gray-700">
|
const filteredPatterns = patterns.filter((p) => {
|
||||||
مجموع الگوهای ثبت شده: <span className="text-purple-600 font-mono">{patterns.length}</span>
|
if (patternStatusFilter === 'APPROVED' && p.status !== 1) return false;
|
||||||
</span>
|
if (patternStatusFilter === 'PENDING' && p.status !== 0) return false;
|
||||||
<button
|
if (patternStatusFilter === 'REJECTED' && p.status !== 2 && p.status !== -1) return false;
|
||||||
type="button"
|
|
||||||
onClick={refreshPatterns}
|
|
||||||
disabled={isLoadingPatterns}
|
|
||||||
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-all border border-gray-200 flex items-center gap-1.5 text-xs font-bold"
|
|
||||||
title="بازخوانی از وبسرویس ملی پیامک"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-3.5 h-3.5 ${isLoadingPatterns ? 'animate-spin text-purple-600' : ''}`} />
|
|
||||||
<span>بروزرسانی از ملی پیامک</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
if (!trimmedFilter) return true;
|
||||||
type="button"
|
const matchesId = String(p.id).includes(trimmedFilter);
|
||||||
onClick={() => setIsAddModalOpen(true)}
|
const matchesTitle = (p.title || '').toLowerCase().includes(trimmedFilter);
|
||||||
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"
|
const matchesBody = (p.body || '').toLowerCase().includes(trimmedFilter);
|
||||||
>
|
const matchesAssigned = (p.assignedTo || []).some((a) => a.toLowerCase().includes(trimmedFilter));
|
||||||
<Plus className="w-4 h-4" />
|
return matchesId || matchesTitle || matchesBody || matchesAssigned;
|
||||||
درج الگوی جدید (Add Pattern)
|
});
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Patterns Table */}
|
return (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<>
|
||||||
<div className="overflow-x-auto">
|
<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">
|
||||||
<table className="w-full text-right text-sm">
|
{/* Search and Filters */}
|
||||||
<thead className="bg-gray-50 text-gray-600 font-bold border-b border-gray-200 text-xs">
|
<div className="flex flex-1 flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
||||||
<tr>
|
{/* Search Input */}
|
||||||
<th className="p-4">شناسه الگو (BodyId)</th>
|
<div className="relative flex-1 max-w-md">
|
||||||
<th className="p-4">عنوان الگو</th>
|
<div className="absolute inset-y-0 right-0 pr-3.5 flex items-center pointer-events-none text-gray-400">
|
||||||
<th className="p-4">متن الگو و متغیرها</th>
|
<Search className="w-4 h-4" />
|
||||||
<th className="p-4">وضعیت تایید</th>
|
</div>
|
||||||
<th className="p-4">اتصال به سیستم</th>
|
<input
|
||||||
<th className="p-4 text-center">عملیات</th>
|
type="text"
|
||||||
</tr>
|
value={patternFilter}
|
||||||
</thead>
|
onChange={(e) => setPatternFilter(e.target.value)}
|
||||||
<tbody className="divide-y divide-gray-100 font-medium">
|
placeholder="جستجو در متن، عنوان یا کد الگو..."
|
||||||
{patterns.length === 0 ? (
|
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"
|
||||||
<tr>
|
/>
|
||||||
<td colSpan={6} className="p-8 text-center text-gray-400 text-xs">
|
{patternFilter && (
|
||||||
هیچ الگویی یافت نشد. میتوانید با دکمه «درج الگوی جدید» اولین پترن خود را ثبت کنید.
|
<button
|
||||||
</td>
|
type="button"
|
||||||
</tr>
|
onClick={() => setPatternFilter('')}
|
||||||
) : (
|
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
patterns.map((p) => (
|
>
|
||||||
<tr key={p.id} className="hover:bg-purple-50/40 transition-colors">
|
<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">
|
<td className="p-4 font-mono font-bold text-gray-900">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded-lg text-xs">
|
<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>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Logs & Tracking Tab */
|
/* Logs & Tracking Tab */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user