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[] {
|
||||
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;
|
||||
|
||||
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' },
|
||||
});
|
||||
|
||||
@ -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,33 +1421,147 @@ 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>
|
||||
{/* 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 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"
|
||||
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>بروزرسانی از ملی پیامک</span>
|
||||
<span className="hidden sm:inline">بروزرسانی از ملی پیامک</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white text-xs font-bold py-2.5 px-5 rounded-xl transition-all flex items-center gap-2 shadow-md shadow-purple-200"
|
||||
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" />
|
||||
درج الگوی جدید (Add Pattern)
|
||||
<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">
|
||||
@ -1462,14 +1578,16 @@ export default function SmsSettingsPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 font-medium">
|
||||
{patterns.length === 0 ? (
|
||||
{filteredPatterns.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="p-8 text-center text-gray-400 text-xs">
|
||||
هیچ الگویی یافت نشد. میتوانید با دکمه «درج الگوی جدید» اولین پترن خود را ثبت کنید.
|
||||
{patterns.length === 0
|
||||
? 'هیچ الگویی یافت نشد. میتوانید با دکمه «درج الگوی جدید» اولین پترن خود را ثبت کنید.'
|
||||
: 'هیچ الگویی با شرایط فیلتر جستجو شده مطابقت ندارد.'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
patterns.map((p) => (
|
||||
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">
|
||||
@ -1591,6 +1709,9 @@ export default function SmsSettingsPage() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
/* Logs & Tracking Tab */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user