'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { coerceToString } from '@/helpers' import { addToast } from '@/lib/toast' import { renderNotificationActionUrlPreview, renderNotificationTemplate } from '@/lib/notificationTemplate' import { countSmsSegments } from '@/lib/smsSegments' import Button from '@/components/formElements/Button' import Input from '@/components/formElements/Input' import AdminState from '@/components/feedback/AdminState' import axiosInstance from '@/config/axios' import { unwrapApiData, type ApiSuccessBody } from '@/services/apiResponse' import { API_ROUTES } from '@/services/config' import { extractServerErrorDetail } from '@/services/errorHandler' type Channel = 'in_app' | 'sms' | 'both' interface Rule { eventKey: string displayName: string description: string enabled: boolean channel: Channel titleTemplate: string bodyTemplate: string actionUrlTemplate: string | null allowedVariables: string[] sampleVariables: Record smsAllowed: boolean updatedAt: string } type RuleDraft = Pick & { actionUrlTemplate: string } const CHANNELS: { code: Channel; name: string }[] = [ { code: 'in_app', name: 'درون‌برنامه‌ای + پوش' }, { code: 'sms', name: 'پیامک' }, { code: 'both', name: 'درون‌برنامه‌ای + پوش + پیامک' }, ] const toDraft = (rule: Rule): RuleDraft => ({ enabled: rule.enabled, channel: rule.channel, titleTemplate: rule.titleTemplate, bodyTemplate: rule.bodyTemplate, actionUrlTemplate: rule.actionUrlTemplate ?? '', }) const isDirtyDraft = (current: RuleDraft, saved: RuleDraft | undefined) => Boolean(saved) && JSON.stringify(current) !== JSON.stringify(saved) const NotificationRulesPanel = () => { const [rules, setRules] = useState([]) const [savedDrafts, setSavedDrafts] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [saving, setSaving] = useState(null) const load = useCallback(async () => { setLoading(true) setError(null) try { const response = await axiosInstance.get(API_ROUTES.NOTIFICATION_RULES.ADMIN_LIST) const payload = unwrapApiData(response.data as ApiSuccessBody) const nextRules = Array.isArray(payload) ? payload : [] setRules(nextRules) setSavedDrafts(Object.fromEntries(nextRules.map((rule) => [rule.eventKey, toDraft(rule)]))) } catch { setError('دریافت تنظیمات اعلان‌ها ناموفق بود.') } finally { setLoading(false) } }, []) useEffect(() => { void load() }, [load]) const change = (eventKey: string, patch: Partial) => { setRules((current) => current.map((rule) => (rule.eventKey === eventKey ? { ...rule, ...patch } : rule))) } const save = async (rule: Rule) => { setSaving(rule.eventKey) try { const response = await axiosInstance.patch(API_ROUTES.NOTIFICATION_RULES.ADMIN_UPDATE(rule.eventKey), { enabled: rule.enabled, channel: rule.channel, titleTemplate: rule.titleTemplate, bodyTemplate: rule.bodyTemplate, actionUrlTemplate: rule.actionUrlTemplate ?? '', }) const updated = unwrapApiData(response.data as ApiSuccessBody) if (updated) { setRules((current) => current.map((item) => (item.eventKey === rule.eventKey ? { ...item, ...updated } : item))) setSavedDrafts((current) => ({ ...current, [rule.eventKey]: toDraft({ ...rule, ...updated }) })) } addToast({ title: 'تنظیمات اعلان ذخیره شد', color: 'success' }) } catch (saveError) { addToast({ title: extractServerErrorDetail((saveError as { response?: { data?: unknown } })?.response?.data) ?? 'ذخیره تنظیمات اعلان ناموفق بود', color: 'danger', }) } finally { setSaving(null) } } return (
متغیرهای داخل دو آکولاد مثل {'{{eventTitle}}'} هنگام ارسال با اطلاعات واقعی جایگزین می‌شوند. فقط متغیرهای همان رویداد مجاز است. اعلان درون‌برنامه‌ای در صورت فعال‌بودن اشتراک مرورگر، Web Push هم ارسال می‌کند.
{loading ?

در حال دریافت تنظیمات…

: null} {!loading && error ? ( void load()} /> ) : null} {!loading && !error && rules.length === 0 ? ( ) : null} {rules.map((rule) => ( void save(rule)} /> ))}
) } const RuleCard = ({ dirty, rule, saving, onChange, onSave, }: { dirty: boolean rule: Rule saving: boolean onChange: (eventKey: string, patch: Partial) => void onSave: () => void }) => { const channelOptions = useMemo(() => { const allowed = CHANNELS.filter((channel) => channel.code === 'in_app' || rule.smsAllowed) if (allowed.some((channel) => channel.code === rule.channel)) return allowed const current = CHANNELS.find((channel) => channel.code === rule.channel) return current ? [...allowed, current] : allowed }, [rule.channel, rule.smsAllowed]) const includesSms = rule.channel === 'sms' || rule.channel === 'both' const smsCount = includesSms ? countSmsSegments(rule.bodyTemplate) : null const previewTitle = renderNotificationTemplate(rule.titleTemplate, rule.sampleVariables) const previewBody = renderNotificationTemplate(rule.bodyTemplate, rule.sampleVariables) const previewActionUrl = rule.actionUrlTemplate ? renderNotificationActionUrlPreview(rule.actionUrlTemplate, rule.sampleVariables) : '' return (

{rule.displayName}

{dirty ? ( ذخیره نشده ) : null}

{rule.description}

{rule.eventKey}
{ onChange(rule.eventKey, { enabled: Boolean(enabled) }) }} />
{rule.allowedVariables.length === 0 ? (

این رویداد متغیر قابل‌جایگزینی ندارد.

) : (

متغیرهای مجاز

{rule.allowedVariables.map((variable) => ( {`{{${variable}}}`} ))}
)} { if (next) onChange(rule.eventKey, { channel: coerceToString(next) as Channel }) }} /> {rule.channel === 'sms' ? (

با انتخاب فقط پیامک، اعلان درون‌برنامه‌ای و پوش ارسال نمی‌شود.

) : null} {!rule.smsAllowed && rule.channel !== 'in_app' ? (

پیامک برای این رویداد مجاز نیست. کانال را به درون‌برنامه‌ای برگردانید.

) : null} { onChange(rule.eventKey, { titleTemplate: coerceToString(titleTemplate) }) }} /> 1 ? ' — متن را کوتاه نگه دارید' : '' }` : undefined } generalType="textarea" label="متن اعلان" name={`body-${rule.eventKey}`} value={rule.bodyTemplate} onValueChange={(bodyTemplate) => { onChange(rule.eventKey, { bodyTemplate: coerceToString(bodyTemplate) }) }} /> { onChange(rule.eventKey, { actionUrlTemplate: coerceToString(actionUrlTemplate) }) }} />

پیش‌نمایش با دادهٔ نمونه

{previewTitle || '—'}

{previewBody || '—'}

{previewActionUrl ? ( {previewActionUrl} ) : null}
) } export default NotificationRulesPanel