feat: add Kavenegar template guide and update PreRegistrationController to use appUrl

This commit is contained in:
hamed
2026-07-05 11:27:10 +03:30
parent 969dc9651f
commit 87d94801f8
3 changed files with 77 additions and 2 deletions
+71 -1
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CheckIcon, XMarkIcon, TrashIcon, PlusIcon, PencilIcon } from '@heroicons/react/24/outline';
import { CheckIcon, XMarkIcon, TrashIcon, PlusIcon, PencilIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -45,6 +45,75 @@ const TAG_FILTER_OPTIONS = [
...Object.entries(TAG_LABELS).map(([value, label]) => ({ value, label })),
];
// الگوی تمپلت کاوه‌نگار را از body (با {var}) و token_map (var→slot) می‌سازد:
// هر {var} با %slot جایگزین می‌شود.
function buildKavenegarPattern(body: string, tokenMap: Record<string, string>): string {
return body.replace(/\{([a-zA-Z0-9_]+)\}/g, (whole, key) =>
tokenMap[key] ? `%${tokenMap[key]}` : whole,
);
}
function KavenegarGuide({ msg }: { msg: SmsMessageText }) {
const tokenMap = msg.token_map ?? {};
const pattern = buildKavenegarPattern(msg.body, tokenMap);
const templateName = msg.kavenegar_template ?? '';
const copy = (text: string, label: string) => {
navigator.clipboard.writeText(text).then(
() => toast.success(`${label} کپی شد`),
() => toast.error('کپی ناموفق بود'),
);
};
return (
<details style={{ marginTop: 4 }}>
<summary style={{ cursor: 'pointer', fontSize: 12, color: 'var(--primary)', userSelect: 'none' }}>
نحوه تعریف این تمپلت در پنل کاوهنگار
</summary>
<div style={{ marginTop: 8, padding: 12, background: 'var(--surface-2)', borderRadius: 'var(--r-sm)', display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ fontSize: 11, color: 'var(--text-3)', lineHeight: 1.8 }}>
در پنل کاوهنگار بخش «الگوها» یک الگوی جدید با نام و متن زیر بسازید و تأیید بگیرید.
متن واقعی پیامک از همین الگو ارسال میشود.
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 12, color: 'var(--text-2)', minWidth: 90 }}>نام الگو:</span>
<code dir="ltr" style={{ fontSize: 12, flex: 1, direction: 'ltr' }}>{templateName || '—'}</code>
<button type="button" className="btn ghost sm" disabled={!templateName} onClick={() => copy(templateName, 'نام الگو')}>
<ClipboardDocumentIcon style={{ width: 13, height: 13 }} /> کپی
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 12, color: 'var(--text-2)' }}>متن الگو (با %token):</span>
<button type="button" className="btn ghost sm" onClick={() => copy(pattern, 'متن الگو')}>
<ClipboardDocumentIcon style={{ width: 13, height: 13 }} /> کپی متن
</button>
</div>
<pre dir="rtl" style={{ margin: 0, padding: 10, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', fontSize: 13, lineHeight: 1.9, whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
{pattern}
</pre>
</div>
{Object.keys(tokenMap).length > 0 && (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>نگاشت متغیرها:</span>
{Object.entries(tokenMap).map(([k, slot]) => (
<span key={k} className="chip" dir="ltr">{`{${k}} → %${slot}`}</span>
))}
</div>
)}
<div style={{ fontSize: 11, color: 'var(--text-3)', lineHeight: 1.8 }}>
توجه: <code dir="ltr">token</code>، <code dir="ltr">token2</code>، <code dir="ltr">token3</code> فاصله نمیپذیرند؛
مقادیر دارای فاصله در <code dir="ltr">token10</code>/<code dir="ltr">token20</code> قرار میگیرند.
</div>
</div>
</details>
);
}
export default function SmsPage() {
const qc = useQueryClient();
const [activeTab, setActiveTab] = useState<Tab>('samples');
@@ -497,6 +566,7 @@ export default function SmsPage() {
{m.kavenegar_template || 'تعریف‌نشده'}
</span>
</div>
<KavenegarGuide msg={m} />
{m.variables.length > 0 && (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{m.variables.map((v) => <span key={v} className="chip" dir="ltr">{`{${v}}`}</span>)}
+4
View File
@@ -112,6 +112,10 @@ services:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Auth\Controller\PreRegistrationController:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Sms\Controller\SmsWalletController:
arguments:
$appBaseUrl: '%env(APP_BASE_URL)%'
@@ -34,6 +34,7 @@ class PreRegistrationController extends BaseController
private readonly UserPasswordHasherInterface $hasher,
private readonly SmsService $sms,
private readonly LoggerInterface $logger,
private readonly string $appUrl,
) {}
#[Route('/api/v1/pre-registration', methods: ['POST'])]
@@ -160,7 +161,7 @@ class PreRegistrationController extends BaseController
[
'username' => $preReg->getMobile(),
'password' => $password,
'link' => 'https://clinic-pro.ddev.site/admin',
'link' => rtrim($this->appUrl, '/') . '/admin',
],
);
} catch (\Throwable $e) {