- added UI for management of email templates;
This commit is contained in:
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
343
src/pages/AdminEmailTemplates/index.js
Normal file
343
src/pages/AdminEmailTemplates/index.js
Normal 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;
|
||||
@@ -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/>}/>
|
||||
|
||||
Reference in New Issue
Block a user