feat: add Kavenegar template guide and update PreRegistrationController to use appUrl
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
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 { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -45,6 +45,75 @@ const TAG_FILTER_OPTIONS = [
|
|||||||
...Object.entries(TAG_LABELS).map(([value, label]) => ({ value, label })),
|
...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() {
|
export default function SmsPage() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('samples');
|
const [activeTab, setActiveTab] = useState<Tab>('samples');
|
||||||
@@ -497,6 +566,7 @@ export default function SmsPage() {
|
|||||||
{m.kavenegar_template || 'تعریفنشده'}
|
{m.kavenegar_template || 'تعریفنشده'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<KavenegarGuide msg={m} />
|
||||||
{m.variables.length > 0 && (
|
{m.variables.length > 0 && (
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
{m.variables.map((v) => <span key={v} className="chip" dir="ltr">{`{${v}}`}</span>)}
|
{m.variables.map((v) => <span key={v} className="chip" dir="ltr">{`{${v}}`}</span>)}
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$appUrl: '%env(APP_BASE_URL)%'
|
$appUrl: '%env(APP_BASE_URL)%'
|
||||||
|
|
||||||
|
App\Auth\Controller\PreRegistrationController:
|
||||||
|
arguments:
|
||||||
|
$appUrl: '%env(APP_BASE_URL)%'
|
||||||
|
|
||||||
App\Sms\Controller\SmsWalletController:
|
App\Sms\Controller\SmsWalletController:
|
||||||
arguments:
|
arguments:
|
||||||
$appBaseUrl: '%env(APP_BASE_URL)%'
|
$appBaseUrl: '%env(APP_BASE_URL)%'
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class PreRegistrationController extends BaseController
|
|||||||
private readonly UserPasswordHasherInterface $hasher,
|
private readonly UserPasswordHasherInterface $hasher,
|
||||||
private readonly SmsService $sms,
|
private readonly SmsService $sms,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly string $appUrl,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/v1/pre-registration', methods: ['POST'])]
|
#[Route('/api/v1/pre-registration', methods: ['POST'])]
|
||||||
@@ -160,7 +161,7 @@ class PreRegistrationController extends BaseController
|
|||||||
[
|
[
|
||||||
'username' => $preReg->getMobile(),
|
'username' => $preReg->getMobile(),
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
'link' => 'https://clinic-pro.ddev.site/admin',
|
'link' => rtrim($this->appUrl, '/') . '/admin',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user