feat(sms): display full rendered SMS body in logs, streamline pattern triggers tab and remove legacy assignment dropdown
This commit is contained in:
parent
2f41d4afe7
commit
09c8588401
@ -601,6 +601,35 @@ export class SmsService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render interpolated message text using pattern body template and arguments
|
||||
*/
|
||||
async renderPatternMessage(
|
||||
patternId?: number | null,
|
||||
args: string[] = [],
|
||||
): Promise<string> {
|
||||
if (!patternId) {
|
||||
return args.join(' ');
|
||||
}
|
||||
|
||||
try {
|
||||
const patterns = await this.getPatterns();
|
||||
const target = patterns.find((p) => p.id === patternId);
|
||||
if (!target || !target.body) {
|
||||
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`;
|
||||
}
|
||||
|
||||
let rendered = target.body;
|
||||
args.forEach((val, idx) => {
|
||||
const regex = new RegExp(`\\{${idx}\\}`, 'g');
|
||||
rendered = rendered.replace(regex, val || '');
|
||||
});
|
||||
return rendered;
|
||||
} catch {
|
||||
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all registered patterns from MeliPayamak (with local cache sync)
|
||||
*/
|
||||
@ -639,90 +668,90 @@ export class SmsService {
|
||||
}
|
||||
}
|
||||
|
||||
// Load local stored patterns from settings table (fallback if offline or newly added locally)
|
||||
// Load cached patterns from settings table
|
||||
const storedSetting = await this.prisma.setting.findFirst({
|
||||
where: { category: 'sms_patterns' },
|
||||
where: {
|
||||
category: 'sms_patterns',
|
||||
key: { in: ['sms_patterns_cache', 'sms_patterns_config'] },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
const localPatterns: MeliPayamakPattern[] = Array.isArray(
|
||||
const cachedPatterns: MeliPayamakPattern[] = Array.isArray(
|
||||
storedSetting?.value,
|
||||
)
|
||||
? (storedSetting.value as unknown as MeliPayamakPattern[])
|
||||
: [];
|
||||
|
||||
// Merge remote and local (remote takes precedence on ID match)
|
||||
// Prioritize remote patterns, then cached patterns, then fallback defaults only if completely empty
|
||||
const map = new Map<number, MeliPayamakPattern>();
|
||||
|
||||
// Add configured system patterns by default if map is empty
|
||||
const defaultConfigs: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
assigned: string;
|
||||
}> = [
|
||||
{
|
||||
id: config.otpBodyId,
|
||||
title: 'کد تایید ورود و ثبتنام (OTP)',
|
||||
body: 'کد ورود به کنینا: {0}',
|
||||
assigned: 'otp',
|
||||
},
|
||||
{
|
||||
id: config.orderBodyId,
|
||||
title: 'تایید و ثبت فاکتور سفارش',
|
||||
body: '{0} عزیز ، سفارش شما به شماره {1} به مبلغ {2} با موفقیت ثبت شد.\nکنینا ایران',
|
||||
assigned: 'order',
|
||||
},
|
||||
{
|
||||
id: config.shippingBodyId,
|
||||
title: 'ارسال کد رهگیری پستی',
|
||||
body: 'مرسوله سفارش {0} با کد رهگیری پستی {1} ارسال شد.',
|
||||
assigned: 'shipping',
|
||||
},
|
||||
{
|
||||
id: config.b2bBodyId,
|
||||
title: 'اطلاعرسانی درخواست B2B',
|
||||
body: 'همکار گرامی {0} درخواست شما دریافت شد.',
|
||||
assigned: 'b2b',
|
||||
},
|
||||
];
|
||||
|
||||
for (const d of defaultConfigs) {
|
||||
if (d.id > 0) {
|
||||
map.set(d.id, {
|
||||
id: d.id,
|
||||
title: d.title,
|
||||
body: d.body,
|
||||
status: 1,
|
||||
statusText: 'تایید شده',
|
||||
assignedTo: [d.assigned],
|
||||
});
|
||||
// 1. If we have remote patterns, use them
|
||||
for (const rp of remotePatterns) {
|
||||
if (rp && rp.id) {
|
||||
map.set(rp.id, { ...rp });
|
||||
}
|
||||
}
|
||||
|
||||
for (const lp of localPatterns) {
|
||||
if (lp && lp.id) map.set(lp.id, { ...lp });
|
||||
// 2. If remote returned empty or offline, use cached patterns
|
||||
if (map.size === 0) {
|
||||
for (const cp of cachedPatterns) {
|
||||
if (cp && cp.id) {
|
||||
map.set(cp.id, { ...cp });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const rp of remotePatterns) {
|
||||
const existing = map.get(rp.id);
|
||||
map.set(rp.id, {
|
||||
...rp,
|
||||
assignedTo: existing?.assignedTo || [],
|
||||
});
|
||||
// 3. Only if map is still completely empty (brand new install / unconfigured account), provide fallback defaults
|
||||
if (map.size === 0) {
|
||||
const defaultConfigs: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
}> = [
|
||||
{
|
||||
id: config.otpBodyId,
|
||||
title: 'کد تایید ورود و ثبتنام (OTP)',
|
||||
body: 'کد ورود به کنینا: {0}',
|
||||
},
|
||||
{
|
||||
id: config.orderBodyId,
|
||||
title: 'تایید و ثبت فاکتور سفارش',
|
||||
body: '{0} عزیز ، سفارش شما به شماره {1} به مبلغ {2} با موفقیت ثبت شد.\nکنینا ایران',
|
||||
},
|
||||
{
|
||||
id: config.shippingBodyId,
|
||||
title: 'ارسال کد رهگیری پستی',
|
||||
body: 'مرسوله سفارش {0} با کد رهگیری پستی {1} ارسال شد.',
|
||||
},
|
||||
{
|
||||
id: config.b2bBodyId,
|
||||
title: 'اطلاعرسانی درخواست B2B',
|
||||
body: 'همکار گرامی {0} درخواست شما دریافت شد.',
|
||||
},
|
||||
];
|
||||
|
||||
for (const d of defaultConfigs) {
|
||||
if (d.id > 0) {
|
||||
map.set(d.id, {
|
||||
id: d.id,
|
||||
title: d.title,
|
||||
body: d.body,
|
||||
status: 1,
|
||||
statusText: 'تایید شده',
|
||||
assignedTo: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark current bindings (both legacy bodyIds and dynamic scenario rules)
|
||||
// Bind current scenario triggers and system assignments
|
||||
const result = Array.from(map.values()).map((p) => {
|
||||
const assigned: string[] = [];
|
||||
if (p.id === config.otpBodyId) assigned.push('کد تایید OTP');
|
||||
if (p.id === config.orderBodyId) assigned.push('پترن پیشفرض سفارش');
|
||||
if (p.id === config.shippingBodyId) assigned.push('پترن پیشفرض پست');
|
||||
if (p.id === config.b2bBodyId) assigned.push('پترن پیشفرض B2B');
|
||||
if (p.id === config.petCareBodyId) assigned.push('پترن پیشفرض یادآور پت');
|
||||
|
||||
// Check dynamic scenario rules
|
||||
// Check dynamic scenario rules (Primary Assignment)
|
||||
if (Array.isArray(config.rules)) {
|
||||
for (const r of config.rules) {
|
||||
if (Number(r.patternId) === p.id) {
|
||||
if (Number(r.patternId) === p.id && r.enabled) {
|
||||
const ruleTag = `سناریو: ${r.title}`;
|
||||
if (!assigned.includes(ruleTag)) {
|
||||
assigned.push(ruleTag);
|
||||
@ -731,6 +760,23 @@ export class SmsService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check legacy default bindings
|
||||
if (p.id === config.otpBodyId && !assigned.some((a) => a.includes('OTP'))) {
|
||||
assigned.push('کد تایید OTP');
|
||||
}
|
||||
if (p.id === config.orderBodyId && !assigned.some((a) => a.includes('سفارش'))) {
|
||||
assigned.push('پترن پیشفرض سفارش');
|
||||
}
|
||||
if (p.id === config.shippingBodyId && !assigned.some((a) => a.includes('پست'))) {
|
||||
assigned.push('پترن پیشفرض پست');
|
||||
}
|
||||
if (p.id === config.b2bBodyId && !assigned.some((a) => a.includes('B2B'))) {
|
||||
assigned.push('پترن پیشفرض B2B');
|
||||
}
|
||||
if (p.id === config.petCareBodyId && !assigned.some((a) => a.includes('پت'))) {
|
||||
assigned.push('پترن پیشفرض پرونده پت');
|
||||
}
|
||||
|
||||
return { ...p, assignedTo: assigned };
|
||||
});
|
||||
|
||||
@ -982,88 +1028,95 @@ export class SmsService {
|
||||
bodyId: options.bodyId,
|
||||
});
|
||||
|
||||
const req = https.request(
|
||||
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
// Pre-render message text for logging
|
||||
void this.renderPatternMessage(options.bodyId, options.args).then((renderedText) => {
|
||||
const req = https.request(
|
||||
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
},
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
||||
res.on('end', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||
const val = json.Value ?? 0;
|
||||
if (json && (val > 15 || json.RetStatus === 1)) {
|
||||
this.logger.log(
|
||||
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
|
||||
);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
status: 'SUCCESS',
|
||||
recId: String(val),
|
||||
});
|
||||
resolve(true);
|
||||
} else {
|
||||
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
|
||||
this.logger.error(
|
||||
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
|
||||
);
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
||||
res.on('end', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||
const val = json.Value ?? 0;
|
||||
if (json && (val > 15 || json.RetStatus === 1)) {
|
||||
this.logger.log(
|
||||
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
|
||||
);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
messageText: renderedText,
|
||||
status: 'SUCCESS',
|
||||
recId: String(val),
|
||||
});
|
||||
resolve(true);
|
||||
} else {
|
||||
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
|
||||
this.logger.error(
|
||||
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
|
||||
);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
recId: String(val),
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
recId: String(val),
|
||||
errorMessage: errMsg,
|
||||
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
status: 'FAILED',
|
||||
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
})();
|
||||
});
|
||||
},
|
||||
);
|
||||
})();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on('error', (err: Error) => {
|
||||
void (async () => {
|
||||
this.logger.error(
|
||||
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
|
||||
);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
status: 'FAILED',
|
||||
errorMessage: `خطای شبکه: ${err.message}`,
|
||||
});
|
||||
resolve(false);
|
||||
})();
|
||||
req.on('error', (err: Error) => {
|
||||
void (async () => {
|
||||
this.logger.error(
|
||||
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
|
||||
);
|
||||
await this.recordLog({
|
||||
receptor: options.to,
|
||||
type: msgType,
|
||||
patternId: options.bodyId,
|
||||
args: options.args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
errorMessage: `خطای شبکه: ${err.message}`,
|
||||
});
|
||||
resolve(false);
|
||||
})();
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1106,89 +1159,95 @@ export class SmsService {
|
||||
bodyId: bodyId,
|
||||
});
|
||||
|
||||
const req = https.request(
|
||||
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
void this.renderPatternMessage(bodyId, args).then((renderedText) => {
|
||||
const req = https.request(
|
||||
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
},
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
||||
res.on('end', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||
const val = json.Value ?? 0;
|
||||
if (json && (val > 15 || json.RetStatus === 1)) {
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
status: 'SUCCESS',
|
||||
recId: String(val),
|
||||
});
|
||||
resolve({
|
||||
success: true,
|
||||
message: `پیامک تستی پترن با موفقیت ارسال شد (شناسه پیگیری ملی پیامک: ${val})`,
|
||||
rawResponse: json,
|
||||
});
|
||||
} else {
|
||||
const errMsg = `خطای درگاه ملی پیامک: کد پاسخ بازگشتی ${val}`;
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
|
||||
res.on('end', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const json = JSON.parse(data) as MeliPayamakResponse;
|
||||
const val = json.Value ?? 0;
|
||||
if (json && (val > 15 || json.RetStatus === 1)) {
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
messageText: renderedText,
|
||||
status: 'SUCCESS',
|
||||
recId: String(val),
|
||||
});
|
||||
resolve({
|
||||
success: true,
|
||||
message: `پیامک تستی پترن با موفقیت ارسال شد (شناسه پیگیری ملی پیامک: ${val})`,
|
||||
rawResponse: json,
|
||||
});
|
||||
} else {
|
||||
const errMsg = `خطای درگاه ملی پیامک: کد پاسخ بازگشتی ${val}`;
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
recId: String(val),
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve({
|
||||
success: false,
|
||||
message: errMsg,
|
||||
rawResponse: json,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const errMsg = `پاسخ نامعتبر از سرور ملی پیامک: ${data || msg}`;
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
recId: String(val),
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve({
|
||||
success: false,
|
||||
message: errMsg,
|
||||
rawResponse: json,
|
||||
});
|
||||
resolve({ success: false, message: errMsg });
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const errMsg = `پاسخ نامعتبر از سرور ملی پیامک: ${data || msg}`;
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
status: 'FAILED',
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve({ success: false, message: errMsg });
|
||||
}
|
||||
})();
|
||||
});
|
||||
},
|
||||
);
|
||||
})();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on('error', (err: Error) => {
|
||||
void (async () => {
|
||||
const errMsg = `خطای برقراری ارتباط با وبسرویس ملی پیامک: ${err.message}`;
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
status: 'FAILED',
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve({ success: false, message: errMsg });
|
||||
})();
|
||||
req.on('error', (err: Error) => {
|
||||
void (async () => {
|
||||
const errMsg = `خطای برقراری ارتباط با وبسرویس ملی پیامک: ${err.message}`;
|
||||
await this.recordLog({
|
||||
receptor: targetPhone,
|
||||
type: 'TEST',
|
||||
patternId: bodyId,
|
||||
args,
|
||||
messageText: renderedText,
|
||||
status: 'FAILED',
|
||||
errorMessage: errMsg,
|
||||
});
|
||||
resolve({ success: false, message: errMsg });
|
||||
})();
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ const toPersianDigits = (n: string | number | null | undefined): string => {
|
||||
};
|
||||
|
||||
export default function SmsSettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState<'settings' | 'triggers' | 'patterns' | 'logs'>('triggers');
|
||||
const [activeTab, setActiveTab] = useState<'settings' | 'patterns' | 'triggers' | 'logs'>('patterns');
|
||||
|
||||
// Config State
|
||||
const [config, setConfig] = useState<SmsConfigState>({
|
||||
@ -729,24 +729,6 @@ export default function SmsSettingsPage() {
|
||||
پیکربندی حساب
|
||||
</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('patterns')}
|
||||
@ -765,6 +747,24 @@ export default function SmsSettingsPage() {
|
||||
)}
|
||||
</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')}
|
||||
@ -1573,7 +1573,7 @@ export default function SmsSettingsPage() {
|
||||
<th className="p-4">عنوان الگو</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>
|
||||
@ -1638,56 +1638,36 @@ export default function SmsSettingsPage() {
|
||||
|
||||
<td className="p-4 text-xs">
|
||||
{p.assignedTo && p.assignedTo.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1.5 max-w-xs">
|
||||
{p.assignedTo.map((a, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-purple-100 text-purple-800 text-[10px] font-bold px-2 py-0.5 rounded-md"
|
||||
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"
|
||||
>
|
||||
{a}
|
||||
<Zap className="w-2.5 h-2.5 text-purple-600" />
|
||||
<span>{a}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 text-[11px]">آزاد (بدون اتصال)</span>
|
||||
<span className="text-gray-400 text-[11px] italic">آزاد (بدون سناریو)</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td className="p-4 text-center">
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleOpenAddRuleModal(p.id, 'ORDER_PAID', `ارسال الگو ${p.id} - ${p.title}`);
|
||||
handleOpenAddRuleModal(p.id, 'ORDER_PAID', `ارسال با الگوی ${p.id} - ${p.title}`);
|
||||
}}
|
||||
className="px-2.5 py-1.5 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-lg text-xs font-bold transition-colors border border-purple-200 flex items-center gap-1"
|
||||
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="اتصال این الگو به یک سناریوی ارسال پیامک (مانند ارسال به مشتری، ادمین، انبار یا شخص خاص)"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>افزودن به سناریوها</span>
|
||||
<Zap className="w-3.5 h-3.5 text-amber-600" />
|
||||
<span>اتصال به سناریو</span>
|
||||
</button>
|
||||
|
||||
<select
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
handleAssignPattern(e.target.value as keyof SmsConfigState, p.id);
|
||||
e.target.value = '';
|
||||
}
|
||||
}}
|
||||
defaultValue=""
|
||||
className="text-[11px] font-bold bg-gray-50 border border-gray-200 rounded-lg p-1.5 text-gray-700 outline-none hover:border-purple-300"
|
||||
title="تنظیم به عنوان الگوی پیشفرض سیستمی"
|
||||
>
|
||||
<option value="" disabled>
|
||||
الگوی پیشفرض...
|
||||
</option>
|
||||
<option value="otpBodyId">پیشفرض: کد تایید OTP</option>
|
||||
<option value="orderBodyId">پیشفرض: تایید ثبت سفارش</option>
|
||||
<option value="shippingBodyId">پیشفرض: کد رهگیری پست</option>
|
||||
<option value="b2bBodyId">پیشفرض: درخواست همکار B2B</option>
|
||||
<option value="petCareBodyId">پیشفرض: یادآور پرونده سلامت پت</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@ -1695,7 +1675,7 @@ export default function SmsSettingsPage() {
|
||||
setEditPatternBody(p.body);
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
className="p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
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" />
|
||||
@ -1902,21 +1882,44 @@ export default function SmsSettingsPage() {
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="p-4 text-xs font-mono">
|
||||
{log.patternId ? (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded font-bold">
|
||||
#{log.patternId}
|
||||
</span>
|
||||
{log.args && log.args.length > 0 && (
|
||||
<span className="text-gray-500 font-sans text-[11px]">
|
||||
[{log.args.join(' , ')}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 font-sans">متنی آزاد</span>
|
||||
)}
|
||||
<td 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">
|
||||
@ -2539,6 +2542,33 @@ export default function SmsSettingsPage() {
|
||||
</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">
|
||||
<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-3.5 rounded-xl border border-purple-200/70 font-sans text-xs text-gray-800 leading-relaxed whitespace-pre-wrap select-text">
|
||||
{fullText || (selectedLog.args && selectedLog.args.length > 0 ? selectedLog.args.join(' ') : 'متن ثبت نشده است.')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{selectedLog.args && selectedLog.args.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="font-bold text-gray-700">آرگومانها و مقادیر ارسالی به پترن:</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user