From a88c7cc133c8ece9c9a798f553dbaeb2b86d677a Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Mon, 4 May 2026 16:44:45 +0200 Subject: [PATCH] tod mich in den mail Co-authored-by: Copilot --- .../components/mailTemplatesManager.tsx | 723 +++++++++++++----- src/app/admin/news-management/page.tsx | 2 + .../manage/hooks/usePoolManageState.ts | 4 +- src/app/api/i18n/scan/route.ts | 114 ++- src/app/components/LanguageSwitcher.tsx | 10 +- src/app/components/dashboard/QuickActions.tsx | 4 + src/app/components/nav/Header.tsx | 6 +- src/app/components/pagination.tsx | 2 + src/app/components/sidebar-layout.tsx | 2 + src/app/components/stacked-layout.tsx | 2 + src/app/i18n/translations/de.ts | 24 +- src/app/i18n/translations/en.ts | 24 +- src/app/i18n/types.ts | 1 + src/app/i18n/useTranslation.tsx | 23 +- src/app/password-reset/page.tsx | 2 +- .../components/generateReferralLinkWidget.tsx | 32 +- 16 files changed, 747 insertions(+), 228 deletions(-) diff --git a/src/app/admin/contract-management/components/mailTemplatesManager.tsx b/src/app/admin/contract-management/components/mailTemplatesManager.tsx index e36fbb3..23e2938 100644 --- a/src/app/admin/contract-management/components/mailTemplatesManager.tsx +++ b/src/app/admin/contract-management/components/mailTemplatesManager.tsx @@ -1,246 +1,581 @@ 'use client'; -import { useState } from 'react'; +import { useTranslation } from '../../../i18n/useTranslation'; +import { useState, useEffect, useCallback } 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: string; + id: number; + template_type: string; name: string; - subject: string; - html: string; - updatedAt: string; + subject: string | null; + html_content: string; + is_active: boolean; + is_archived: boolean; + archived_at: string | null; + created_at: string; + updated_at: string; }; -const MOCK_MAIL_TEMPLATES: MailTemplate[] = [ - { - id: 'welcome', - name: 'Welcome Mail', - subject: 'Welcome to Profit Planet', - html: '

Welcome {{firstName}}

Thanks for joining Profit Planet. We are happy to have you onboard.

Best regards,
Profit Planet Team

', - updatedAt: '2026-05-04T09:00:00.000Z', - }, - { - id: 'invoice-reminder', - name: 'Invoice Reminder', - subject: 'Friendly reminder: invoice {{invoiceNumber}}', - html: '

Invoice Reminder

Hello {{companyName}},

your invoice {{invoiceNumber}} is due on {{dueDate}}.

Thank you!

', - updatedAt: '2026-05-03T14:30:00.000Z', - }, -]; +type EditorState = { + template_type: string; + name: string; + subject: string; + html_content: string; +}; + +const BLANK_EDITOR: EditorState = { + template_type: '', + name: '', + subject: '', + html_content: '

New Template

Edit this HTML content.

', +}; export default function MailTemplatesManager() { - const [mailTemplates, setMailTemplates] = useState(MOCK_MAIL_TEMPLATES); - const [selectedMailTemplateId, setSelectedMailTemplateId] = useState(MOCK_MAIL_TEMPLATES[0]?.id ?? ''); - const [isCreatingMailTemplate, setIsCreatingMailTemplate] = useState(false); - const [mailTemplateSavedMessage, setMailTemplateSavedMessage] = useState(null); - const [mailEditor, setMailEditor] = useState<{ name: string; subject: string; html: string }>({ - name: MOCK_MAIL_TEMPLATES[0]?.name ?? '', - subject: MOCK_MAIL_TEMPLATES[0]?.subject ?? '', - html: MOCK_MAIL_TEMPLATES[0]?.html ?? '', - }); + const { t } = useTranslation(); + const { showToast } = useToast(); - const selectedMailTemplate = mailTemplates.find((template) => template.id === selectedMailTemplateId) ?? null; + const [tab, setTab] = useState<'active' | 'archived'>('active'); + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [fetchError, setFetchError] = useState(null); - const startCreateMailTemplate = () => { - setIsCreatingMailTemplate(true); - setSelectedMailTemplateId(''); - setMailEditor({ - name: 'New Mail Template', - subject: '', - html: '

New Template

Edit this HTML content.

', - }); - }; + const [selectedId, setSelectedId] = useState(null); + const [isCreating, setIsCreating] = useState(false); + const [i18nSyncStatus, setI18nSyncStatus] = useState<'idle' | 'syncing' | 'synced' | 'error'>('idle'); + const [editor, setEditor] = useState(BLANK_EDITOR); + const [isSaving, setIsSaving] = useState(false); + const [actionLoadingId, setActionLoadingId] = useState(null); - const selectExistingMailTemplate = (id: string) => { - const template = mailTemplates.find((item) => item.id === id); - if (!template) return; - setIsCreatingMailTemplate(false); - setSelectedMailTemplateId(id); - setMailEditor({ - name: template.name, - subject: template.subject, - html: template.html, - }); - }; - - const saveMailTemplate = () => { - const nowIso = new Date().toISOString(); - - if (isCreatingMailTemplate) { - const generatedId = `mail-${Date.now()}`; - const nextTemplate: MailTemplate = { - id: generatedId, - name: mailEditor.name.trim() || 'Untitled Template', - subject: mailEditor.subject, - html: mailEditor.html, - updatedAt: nowIso, - }; - setMailTemplates((current) => [nextTemplate, ...current]); - setSelectedMailTemplateId(generatedId); - setIsCreatingMailTemplate(false); - setMailTemplateSavedMessage('Mail template created (mock).'); - return; + 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); } + }, []); - if (!selectedMailTemplateId) return; + useEffect(() => { + fetchTemplates(tab === 'archived'); + setSelectedId(null); + setIsCreating(false); + setEditor(BLANK_EDITOR); + }, [tab, fetchTemplates]); - setMailTemplates((current) => - current.map((template) => - template.id === selectedMailTemplateId - ? { - ...template, - name: mailEditor.name.trim() || template.name, - subject: mailEditor.subject, - html: mailEditor.html, - updatedAt: nowIso, - } - : template - ) - ); - setMailTemplateSavedMessage('Mail template updated (mock).'); - }; + const visibleTemplates = tab === 'archived' + ? templates.filter((tmpl) => tmpl.is_archived) + : templates.filter((tmpl) => !tmpl.is_archived); - const deleteSelectedMailTemplate = () => { - if (!selectedMailTemplateId) return; + const selectedTemplate = templates.find((tmpl) => tmpl.id === selectedId) ?? null; - setMailTemplates((current) => { - const next = current.filter((template) => template.id !== selectedMailTemplateId); - const nextSelected = next[0] ?? null; - setSelectedMailTemplateId(nextSelected?.id ?? ''); - setIsCreatingMailTemplate(false); - setMailEditor({ - name: nextSelected?.name ?? '', - subject: nextSelected?.subject ?? '', - html: nextSelected?.html ?? '', - }); - return next; + 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, }); - - setMailTemplateSavedMessage('Mail template removed (mock).'); }; + const startCreate = () => { + setIsCreating(true); + setSelectedId(null); + setI18nSyncStatus('idle'); + setEditor(BLANK_EDITOR); + }; + + 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({ type: 'success', message: 'Mail template created.' }); + 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({ type: 'success', message: 'Mail template updated.' }); + 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({ type: 'error', message: e?.message || 'Failed to save template.' }); + } 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({ type: 'success', message: 'Template activated.' }); + await fetchTemplates(tab === 'archived'); + } catch (e: any) { + showToast({ type: 'error', message: e?.message || 'Failed to activate template.' }); + } 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({ type: 'success', message: 'Template archived.' }); + if (selectedId === id) { + setSelectedId(null); + setEditor(BLANK_EDITOR); + } + await fetchTemplates(tab === 'archived'); + } catch (e: any) { + showToast({ type: 'error', message: e?.message || 'Failed to archive template.' }); + } finally { + setActionLoadingId(null); + } + }; + + const handleDelete = async (id: number) => { + if (!window.confirm('Delete this mail template? This cannot be undone.')) return; + 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({ type: 'success', message: 'Template deleted.' }); + if (selectedId === id) { + setSelectedId(null); + setEditor(BLANK_EDITOR); + } + await fetchTemplates(tab === 'archived'); + } catch (e: any) { + showToast({ type: 'error', message: e?.message || 'Failed to delete template.' }); + } finally { + setActionLoadingId(null); + } + }; + + const editorOpen = isCreating || selectedId !== null; + return (
+ {/* Header */}

- - Mail Templates + + + + {t('autofix.kd93a60af')}

+ {tab === 'active' && ( + + )} + {editorOpen && ( + + )} - -
-

- Frontend-only mock editor. Data is stored in local component state and resets on page reload. -

+ {/* Tabs */} +
+ + +
- {mailTemplateSavedMessage && ( -
- {mailTemplateSavedMessage} + {fetchError && ( +
+ {fetchError}
)} -
+
+ {/* Template List */}
-
-
-
- + {/* Editor + Preview */} + {editorOpen && ( +
+
+
+ - + -