- added UI for management of email templates;

This commit is contained in:
Vitalii Kiiko
2026-04-01 11:06:22 +02:00
parent 0b8be811f5
commit 0e258febe2
3 changed files with 354 additions and 0 deletions

View File

@@ -58,6 +58,13 @@ const navItems = [
label: __('Invio PEC Massivo', 'gepafin'),
subtitle: __('Invia PEC a più destinatari tramite file CSV', 'gepafin'),
icon: 'pi pi-envelope'
},
{
permission: ['ROOT_MANAGE_EMAIL_TEMPLATES'],
route: '/admin/email-templates',
label: __('Template Email', 'gepafin'),
subtitle: __('Gestione dei template per le email', 'gepafin'),
icon: 'pi pi-file-edit'
}
];

View File

@@ -0,0 +1,343 @@
import React, { useEffect, useRef, useState } from 'react';
import { __ } from '@wordpress/i18n';
import { intersection } from 'ramda';
import { useNavigate } from 'react-router-dom';
// store
import { useStoreValue } from '../../store';
// components
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Editor } from 'primereact/editor';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
const TEMPLATE_TYPES = [
{
value: 'WELCOME',
label: __('Benvenuto', 'gepafin'),
tags: ['companyName', 'userEmail', 'userName']
},
{
value: 'APPLICATION_APPROVED',
label: __('Domanda Approvata', 'gepafin'),
tags: ['companyName', 'callTitle', 'applicationId', 'approvalDate']
},
{
value: 'APPLICATION_REJECTED',
label: __('Domanda Rifiutata', 'gepafin'),
tags: ['companyName', 'callTitle', 'applicationId', 'rejectionReason']
},
{
value: 'AMENDMENT_REQUEST',
label: __('Richiesta Integrazione', 'gepafin'),
tags: ['companyName', 'callTitle', 'applicationId', 'amendmentDeadline', 'amendmentNotes']
},
{
value: 'PEC_NOTIFICATION',
label: __('Notifica PEC', 'gepafin'),
tags: ['companyName', 'companyPec', 'callTitle', 'applicationId']
}
];
const INITIAL_TEMPLATES = [
{
id: 1,
title: 'Email di Benvenuto',
type: 'WELCOME',
content: '<p>Benvenuto <strong>{{userName}}</strong>,</p><p>il tuo account è stato creato con successo. Azienda: {{companyName}}.</p>'
},
{
id: 2,
title: 'Approvazione Domanda',
type: 'APPLICATION_APPROVED',
content: '<p>Gentile {{companyName}},</p><p>la domanda <strong>{{applicationId}}</strong> per il bando <em>{{callTitle}}</em> è stata approvata in data {{approvalDate}}.</p>'
}
];
const getTypeLabel = (value) => {
const found = TEMPLATE_TYPES.find((t) => t.value === value);
return found ? found.label : value;
};
const getTypeTags = (value) => {
const found = TEMPLATE_TYPES.find((t) => t.value === value);
return found ? found.tags : [];
};
const editorHeader = (
<span className="ql-formats">
<button className="ql-bold" aria-label="Bold"></button>
<button className="ql-italic" aria-label="Italic"></button>
<button className="ql-underline" aria-label="Underline"></button>
<button className="ql-link" aria-label="Link"></button>
<button className="ql-list" value="ordered"></button>
<button className="ql-list" value="bullet"></button>
<button className="ql-header" value="2"></button>
<button className="ql-blockquote"></button>
</span>
);
const AdminEmailTemplates = () => {
const permissions = useStoreValue('getPermissions');
const navigate = useNavigate();
const toast = useRef(null);
const editorRef = useRef(null);
const hasPermission = intersection(permissions, ['ROOT_MANAGE_EMAIL_TEMPLATES']).length > 0;
const [templates, setTemplates] = useState(INITIAL_TEMPLATES);
const [dialogVisible, setDialogVisible] = useState(false);
const [editingTemplate, setEditingTemplate] = useState(null);
const [modalTitle, setModalTitle] = useState('');
const [modalType, setModalType] = useState(TEMPLATE_TYPES[0].value);
const [modalContent, setModalContent] = useState('');
useEffect(() => {
if (!hasPermission) navigate('/admin');
}, [hasPermission]);
const openAddDialog = () => {
setEditingTemplate({});
setModalTitle('');
setModalType(TEMPLATE_TYPES[0].value);
setModalContent('');
setDialogVisible(true);
};
const openEditDialog = (template) => {
setEditingTemplate(template);
setModalTitle(template.title);
setModalType(template.type);
setModalContent(template.content);
setDialogVisible(true);
};
const closeDialog = () => {
setDialogVisible(false);
setEditingTemplate(null);
};
const handleSave = () => {
if (!modalTitle.trim()) {
toast.current.show({
severity: 'warn',
summary: __('Attenzione', 'gepafin'),
detail: __('Il titolo è obbligatorio.', 'gepafin')
});
return;
}
if (editingTemplate && editingTemplate.id) {
setTemplates((prev) =>
prev.map((t) =>
t.id === editingTemplate.id
? { ...t, title: modalTitle, type: modalType, content: modalContent }
: t
)
);
} else {
setTemplates((prev) => [
...prev,
{ id: Date.now(), title: modalTitle, type: modalType, content: modalContent }
]);
}
closeDialog();
toast.current.show({
severity: 'success',
summary: __('Salvato', 'gepafin'),
detail: __('Template salvato con successo.', 'gepafin')
});
};
const handleRemove = (template) => {
confirmDialog({
message: __('Sei sicuro di voler eliminare questo template?', 'gepafin'),
header: __('Conferma eliminazione', 'gepafin'),
icon: 'pi pi-exclamation-triangle',
acceptLabel: __('Elimina', 'gepafin'),
rejectLabel: __('Annulla', 'gepafin'),
accept: () => {
setTemplates((prev) => prev.filter((t) => t.id !== template.id));
toast.current.show({
severity: 'info',
summary: __('Eliminato', 'gepafin'),
detail: __('Template eliminato.', 'gepafin')
});
}
});
};
const insertTag = (tag) => {
if (editorRef.current) {
const quill = editorRef.current.getQuill();
if (quill) {
const range = quill.getSelection(true);
const index = range ? range.index : quill.getLength();
quill.insertText(index, `{{${tag}}}`);
quill.setSelection(index + tag.length + 4);
return;
}
}
setModalContent((prev) => (prev || '') + `{{${tag}}}`);
};
const typeBodyTemplate = (row) => getTypeLabel(row.type);
const actionsBodyTemplate = (row) => (
<div style={{ display: 'flex', gap: '0.5rem' }}>
<Button
size="small"
icon="pi pi-pencil"
label={__('Modifica', 'gepafin')}
outlined
onClick={() => openEditDialog(row)}
/>
<Button
size="small"
icon="pi pi-trash"
label={__('Elimina', 'gepafin')}
severity="danger"
outlined
onClick={() => handleRemove(row)}
/>
</div>
);
const currentTags = getTypeTags(modalType);
const isEditing = editingTemplate && editingTemplate.id;
const dialogFooter = (
<div>
<Button
label={__('Annulla', 'gepafin')}
icon="pi pi-times"
outlined
onClick={closeDialog}
/>
<Button
label={__('Salva', 'gepafin')}
icon="pi pi-check"
onClick={handleSave}
/>
</div>
);
return <div className="appPage">
<Toast ref={toast}/>
<ConfirmDialog/>
<div className="appPage__pageHeader">
<h1>{__('Template Email', 'gepafin')}</h1>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection__actions">
<Button
type="button"
outlined
label={__('Indietro', 'gepafin')}
icon="pi pi-arrow-left"
onClick={() => navigate('/admin')}
/>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection__actions">
<Button
label={__('Aggiungi nuovo', 'gepafin')}
icon="pi pi-plus"
onClick={openAddDialog}
/>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection__table">
<DataTable
value={templates}
emptyMessage={__('Nessun template trovato.', 'gepafin')}
stripedRows
showGridlines
>
<Column field="id" header={__('ID', 'gepafin')} style={{ width: '80px' }}/>
<Column field="title" header={__('Titolo', 'gepafin')}/>
<Column header={__('Tipo / Azione', 'gepafin')} body={typeBodyTemplate}/>
<Column header={__('Azioni', 'gepafin')} body={actionsBodyTemplate}/>
</DataTable>
</div>
<Dialog
visible={dialogVisible}
modal
header={isEditing ? __('Modifica Template', 'gepafin') : __('Nuovo Template', 'gepafin')}
footer={dialogFooter}
style={{ width: '700px', maxWidth: '100%' }}
onHide={closeDialog}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
<div className="formLayout__col">
<label htmlFor="modalTitle">{__('Titolo', 'gepafin')} <span className="appForm__field--required">*</span></label>
<InputText
id="modalTitle"
value={modalTitle}
onChange={(e) => setModalTitle(e.target.value)}
style={{ width: '100%' }}
placeholder={__('Inserisci il titolo del template', 'gepafin')}
/>
</div>
<div className="formLayout__col">
<label htmlFor="modalType">{__('Tipo / Azione', 'gepafin')}</label>
<Dropdown
id="modalType"
value={modalType}
options={TEMPLATE_TYPES}
optionLabel="label"
optionValue="value"
onChange={(e) => setModalType(e.value)}
style={{ width: '100%' }}
/>
</div>
<div className="formLayout__col">
<label>{__('Contenuto', 'gepafin')}</label>
<div translate="no">
<Editor
ref={editorRef}
value={modalContent}
onTextChange={(e) => setModalContent(e.htmlValue)}
headerTemplate={editorHeader}
style={{ height: '240px' }}
placeholder={__('Scrivi il contenuto del template...', 'gepafin')}
/>
</div>
</div>
{currentTags.length > 0 && (
<div className="formLayout__col">
<label>{__('Tag dinamici disponibili', 'gepafin')}</label>
<p style={{ fontSize: '0.875rem', color: 'var(--text-color-secondary)', marginBottom: '0.5rem' }}>
{__('Clicca su un tag per inserirlo nel testo', 'gepafin')}
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{currentTags.map((tag) => (
<Tag
key={tag}
value={`{{${tag}}}`}
style={{ cursor: 'pointer' }}
onClick={() => insertTag(tag)}
/>
))}
</div>
</div>
)}
</div>
</Dialog>
</div>;
};
export default AdminEmailTemplates;

View File

@@ -64,6 +64,7 @@ import AdminAmendmentExtend from './pages/AdminAmendmentExtend';
import AdminEliminaDomande from './pages/AdminEliminaDomande';
import AdminLog from './pages/AdminLog';
import AdminSendPec from './pages/AdminSendPec';
import AdminEmailTemplates from './pages/AdminEmailTemplates';
const routes = ({ role, chosenCompanyId }) => {
@@ -319,6 +320,9 @@ const routes = ({ role, chosenCompanyId }) => {
<Route path="/admin/send-pec" element={<DefaultLayout>
<AdminSendPec/>
</DefaultLayout>}/>
<Route path="/admin/email-templates" element={<DefaultLayout>
<AdminEmailTemplates/>
</DefaultLayout>}/>
</Route>
<Route exact path="/reset-password" element={<ResetPassword/>}/>
<Route exact path="/reset-password-admin" element={<ResetPasswordAdmin/>}/>