- added UI for management of email templates;
This commit is contained in:
@@ -58,6 +58,13 @@ const navItems = [
|
|||||||
label: __('Invio PEC Massivo', 'gepafin'),
|
label: __('Invio PEC Massivo', 'gepafin'),
|
||||||
subtitle: __('Invia PEC a più destinatari tramite file CSV', 'gepafin'),
|
subtitle: __('Invia PEC a più destinatari tramite file CSV', 'gepafin'),
|
||||||
icon: 'pi pi-envelope'
|
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 AdminEliminaDomande from './pages/AdminEliminaDomande';
|
||||||
import AdminLog from './pages/AdminLog';
|
import AdminLog from './pages/AdminLog';
|
||||||
import AdminSendPec from './pages/AdminSendPec';
|
import AdminSendPec from './pages/AdminSendPec';
|
||||||
|
import AdminEmailTemplates from './pages/AdminEmailTemplates';
|
||||||
|
|
||||||
const routes = ({ role, chosenCompanyId }) => {
|
const routes = ({ role, chosenCompanyId }) => {
|
||||||
|
|
||||||
@@ -319,6 +320,9 @@ const routes = ({ role, chosenCompanyId }) => {
|
|||||||
<Route path="/admin/send-pec" element={<DefaultLayout>
|
<Route path="/admin/send-pec" element={<DefaultLayout>
|
||||||
<AdminSendPec/>
|
<AdminSendPec/>
|
||||||
</DefaultLayout>}/>
|
</DefaultLayout>}/>
|
||||||
|
<Route path="/admin/email-templates" element={<DefaultLayout>
|
||||||
|
<AdminEmailTemplates/>
|
||||||
|
</DefaultLayout>}/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/reset-password" element={<ResetPassword/>}/>
|
<Route exact path="/reset-password" element={<ResetPassword/>}/>
|
||||||
<Route exact path="/reset-password-admin" element={<ResetPasswordAdmin/>}/>
|
<Route exact path="/reset-password-admin" element={<ResetPasswordAdmin/>}/>
|
||||||
|
|||||||
Reference in New Issue
Block a user