'use client'; import { useTranslation } from '../../../i18n/useTranslation'; import { useState, useEffect, useCallback, useMemo } from 'react'; import { authFetch } from '../../../utils/authFetch'; import { useToast } from '../../../components/toast/toastComponent'; const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, ''); const MAIL_TEMPLATES_BASE = `${API_BASE}/api/admin/mail-templates`; /** Convert kebab-case to camelCase for use as i18n key segment */ function toCamelCase(str: string): string { return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); } /** Derive the i18n translation key for a mail template's subject */ function subjectI18nKey(templateType: string): string { return `mailTemplates.${toCamelCase(templateType)}.subject`; } /** Derive the i18n translation key for a mail template's HTML content */ function htmlI18nKey(templateType: string): string { return `mailTemplates.${toCamelCase(templateType)}.htmlContent`; } /** * Sync mail template content to the i18n translation files so it appears * in Language Management and can be translated to other languages. */ async function syncToI18n(templateType: string, subject: string, htmlContent: string): Promise { const res = await fetch('/api/i18n/translations', { cache: 'no-store' }); const data = await res.json(); if (!res.ok || !data?.ok) throw new Error(data?.message || 'Failed to load translations'); const translations: Record> = data.translations ?? {}; const sKey = subjectI18nKey(templateType); const hKey = htmlI18nKey(templateType); const updated = { ...translations, en: { ...(translations.en ?? {}), [sKey]: subject, [hKey]: htmlContent }, }; const putRes = await fetch('/api/i18n/translations', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ translations: updated }), }); const putData = await putRes.json(); if (!putRes.ok || !putData?.ok) throw new Error(putData?.message || 'Failed to sync to Language Management'); } type MailTemplate = { id: number; template_type: string; name: string; subject: string | null; html_content: string; is_active: boolean; is_archived: boolean; archived_at: string | null; created_at: string; updated_at: string; }; type EditorState = { template_type: string; name: string; subject: string; html_content: string; }; function createBlankEditor(t: (key: string) => string): EditorState { return { template_type: '', name: '', subject: '', html_content: `

${t('autofix.k88f0d12a')}

${t('autofix.k4f530782')}

`, }; } export default function MailTemplatesManager() { const { t } = useTranslation(); const { showToast } = useToast(); const blankEditor = useMemo(() => createBlankEditor(t), [t]); const [tab, setTab] = useState<'active' | 'archived'>('active'); const [templates, setTemplates] = useState([]); const [isLoading, setIsLoading] = useState(false); const [fetchError, setFetchError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [isCreating, setIsCreating] = useState(false); const [i18nSyncStatus, setI18nSyncStatus] = useState<'idle' | 'syncing' | 'synced' | 'error'>('idle'); const [editor, setEditor] = useState(blankEditor); const [isSaving, setIsSaving] = useState(false); const [actionLoadingId, setActionLoadingId] = useState(null); const fetchTemplates = useCallback(async (includeArchived: boolean) => { setIsLoading(true); setFetchError(null); try { const url = `${MAIL_TEMPLATES_BASE}${includeArchived ? '?includeArchived=true' : ''}`; const res = await authFetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } const raw = await res.json(); // API may return a bare array or a wrapper object { data: [...] } / { templates: [...] } const data: MailTemplate[] = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.templates) ? raw.templates : []; setTemplates(data); } catch (e: any) { setFetchError(e?.message || 'Failed to load templates'); } finally { setIsLoading(false); } }, []); useEffect(() => { fetchTemplates(tab === 'archived'); setSelectedId(null); setIsCreating(false); setEditor(blankEditor); }, [tab, fetchTemplates, blankEditor]); const visibleTemplates = tab === 'archived' ? templates.filter((tmpl) => tmpl.is_archived) : templates.filter((tmpl) => !tmpl.is_archived); const selectedTemplate = templates.find((tmpl) => tmpl.id === selectedId) ?? null; const openEditor = (template: MailTemplate) => { setIsCreating(false); setSelectedId(template.id); setI18nSyncStatus('idle'); setEditor({ template_type: template.template_type, name: template.name, subject: template.subject ?? '', html_content: template.html_content, }); }; const startCreate = () => { setIsCreating(true); setSelectedId(null); setI18nSyncStatus('idle'); setEditor(blankEditor); }; const handleSave = async () => { if (isSaving) return; setIsSaving(true); setI18nSyncStatus('idle'); let savedTemplateType = editor.template_type; let savedSubject = editor.subject; let savedHtmlContent = editor.html_content; try { if (isCreating) { const res = await authFetch(MAIL_TEMPLATES_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template_type: editor.template_type, name: editor.name, subject: editor.subject || undefined, html_content: editor.html_content, }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } const created: MailTemplate = await res.json(); savedTemplateType = created.template_type; savedSubject = created.subject ?? editor.subject; savedHtmlContent = created.html_content; showToast({ variant: 'success', message: t('autofix.ke8a3bd92') }); await fetchTemplates(false); setTab('active'); setIsCreating(false); setSelectedId(created.id); setEditor({ template_type: created.template_type, name: created.name, subject: created.subject ?? '', html_content: created.html_content, }); } else if (selectedId !== null) { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${selectedId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template_type: editor.template_type, name: editor.name, subject: editor.subject || undefined, html_content: editor.html_content, }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } showToast({ variant: 'success', message: t('autofix.k9535ed27') }); await fetchTemplates(tab === 'archived'); } // Sync content to i18n system so it's translatable in Language Management if (savedTemplateType?.trim()) { setI18nSyncStatus('syncing'); syncToI18n(savedTemplateType, savedSubject, savedHtmlContent) .then(() => setI18nSyncStatus('synced')) .catch(() => setI18nSyncStatus('error')); } } catch (e: any) { showToast({ variant: 'error', message: e?.message || t('autofix.kb743b7c2') }); } finally { setIsSaving(false); } }; const handleActivate = async (id: number) => { setActionLoadingId(id); try { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}/activate`, { method: 'PATCH' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } showToast({ variant: 'success', message: t('autofix.ke1a18ce6') }); await fetchTemplates(tab === 'archived'); } catch (e: any) { showToast({ variant: 'error', message: e?.message || t('autofix.k1c5f641a') }); } finally { setActionLoadingId(null); } }; const handleArchive = async (id: number) => { setActionLoadingId(id); try { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}/archive`, { method: 'PATCH' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } showToast({ variant: 'success', message: t('autofix.k2f162f5d') }); if (selectedId === id) { setSelectedId(null); setEditor(blankEditor); } await fetchTemplates(tab === 'archived'); } catch (e: any) { showToast({ variant: 'error', message: e?.message || t('autofix.kff7b3b21') }); } finally { setActionLoadingId(null); } }; const handleUnarchive = async (id: number) => { setActionLoadingId(id); try { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}/unarchive`, { method: 'PATCH' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || (err as any)?.error || `Error ${res.status}`); } showToast({ variant: 'success', message: t('autofix.kc3a71e92') }); setSelectedId(null); setEditor(blankEditor); setTab('active'); await fetchTemplates(false); } catch (e: any) { showToast({ variant: 'error', message: e?.message || t('autofix.ka91f3c05') }); } finally { setActionLoadingId(null); } }; const removeI18nKeys = async (templateType: string) => { try { const res = await fetch('/api/i18n/translations', { cache: 'no-store' }); const data = await res.json(); if (!res.ok || !data?.ok) return; const translations: Record> = data.translations ?? {}; const sKey = subjectI18nKey(templateType); const hKey = htmlI18nKey(templateType); const updated: Record> = {}; for (const lang of Object.keys(translations)) { const copy = { ...translations[lang] }; delete copy[sKey]; delete copy[hKey]; updated[lang] = copy; } await fetch('/api/i18n/translations', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ translations: updated }), }); } catch { // best-effort — don't block the delete flow } }; const handleDelete = async (id: number) => { if (!window.confirm(t('autofix.ka63bb731'))) return; const templateType = templates.find((tmpl) => tmpl.id === id)?.template_type ?? ''; setActionLoadingId(id); try { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } showToast({ variant: 'success', message: t('autofix.kf6b83106') }); if (selectedId === id) { setSelectedId(null); setEditor(blankEditor); } await fetchTemplates(tab === 'archived'); if (templateType.trim()) { await removeI18nKeys(templateType); } } catch (e: any) { showToast({ variant: 'error', message: e?.message || t('autofix.kccf6593a') }); } finally { setActionLoadingId(null); } }; const editorOpen = isCreating || selectedId !== null; return (
{/* Header */}

{t('autofix.kd93a60af')}

{tab === 'active' && ( )} {editorOpen && ( )}
{/* Tabs */}
{fetchError && (
{fetchError}
)}
{/* Template List */} {/* Editor + Preview */} {editorOpen && (
{/* i18n key info + sync status */} {editor.template_type?.trim() && (
{t('autofix.k48b366e4')}
{i18nSyncStatus === 'syncing' && ( {t('autofix.ke4326584')} )} {i18nSyncStatus === 'synced' && ( {t('autofix.kf5fec72a')} )} {i18nSyncStatus === 'error' && ( {t('autofix.k5321f8f0')} )}
{t('autofix.k8aea9103')} {subjectI18nKey(editor.template_type)} {t('autofix.k0aa53382')} {htmlI18nKey(editor.template_type)}
)}