Update TemplateEditor.tsx

This commit is contained in:
fcarraUniSa
2025-12-23 12:23:30 +01:00
committed by GitHub
parent 394b87dc25
commit f38ad1b6e7

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { EmailTemplate } from '../types'; import { EmailTemplate } from '../types';
import { saveTemplate, generateSQL, generateSelectSQL, getTemplates, generateTemplateKey, generateN8nCode } from '../services/storage'; import { saveTemplate, generateSQL, generateSelectSQL, getTemplates, generateTemplateKey, generateN8nCode, generateUUID } from '../services/storage';
import { generateEmailContent } from '../services/geminiService'; import { generateEmailContent } from '../services/geminiService';
import RichTextEditor from './RichTextEditor'; import RichTextEditor from './RichTextEditor';
import { import {
@@ -15,18 +15,6 @@ interface Props {
onSave: () => void; onSave: () => void;
} }
// Fallback for crypto.randomUUID in non-secure (HTTP) contexts
const generateUUID = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
const DEFAULT_HEADER = `<div style="background-color: #f8fafc; padding: 20px; text-align: center;"> const DEFAULT_HEADER = `<div style="background-color: #f8fafc; padding: 20px; text-align: center;">
<h1 style="color: #334155; margin: 0;">La Mia Azienda</h1> <h1 style="color: #334155; margin: 0;">La Mia Azienda</h1>
</div>`; </div>`;
@@ -78,21 +66,20 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
const newKey = generateTemplateKey(name); const newKey = generateTemplateKey(name);
if (!newKey) { if (!newKey) {
setNameError('Il nome del template non può essere vuoto o contenere solo simboli.'); setNameError('Il nome del template non può essere vuoto.');
return; return;
} }
setIsSaving(true); setIsSaving(true);
try { console.log("Saving process started for:", name);
console.log("Inizio processo di salvataggio per:", name);
// Check for duplicates try {
// 1. Get current templates to check duplicates
let allTemplates: EmailTemplate[] = []; let allTemplates: EmailTemplate[] = [];
try { try {
allTemplates = await getTemplates(); allTemplates = await getTemplates();
} catch (err) { } catch (err) {
console.error("Errore durante il recupero dei template esistenti:", err); console.warn("Could not fetch templates for duplicate check, proceeding anyway...", err);
// Non blocchiamo necessariamente, ma avvisiamo
} }
const isDuplicate = allTemplates.some(t => { const isDuplicate = allTemplates.some(t => {
@@ -118,13 +105,13 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString()
}; };
console.log("Invio dati al server...", newTemplate); console.log("Sending POST to server with data:", newTemplate);
await saveTemplate(newTemplate); await saveTemplate(newTemplate);
console.log("Salvataggio completato con successo"); console.log("Save successful!");
onSave(); onSave();
} catch (e: any) { } catch (e: any) {
console.error("ERRORE SALVATAGGIO:", e); console.error("SAVE ERROR DETAILS:", e);
alert(`Errore: ${e.message || 'Impossibile salvare il template'}`); alert(`Errore di salvataggio: ${e.message}`);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@@ -231,7 +218,7 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
placeholder="es. Email di Benvenuto" placeholder="es. Email di Benvenuto"
/> />
{nameError && <p className="text-red-500 text-xs mt-1">{nameError}</p>} {nameError && <p className="text-red-500 text-xs mt-1">{nameError}</p>}
<p className="text-xs text-slate-400 mt-1">Deve essere univoco. Usato per generare la chiave DB: <span className="font-mono bg-slate-100 px-1">{generateTemplateKey(name) || '...'}</span></p> <p className="text-xs text-slate-400 mt-1">Deve essere univoco. Chiave DB: <span className="font-mono bg-slate-100 px-1">{generateTemplateKey(name) || '...'}</span></p>
</div> </div>
<div> <div>
@@ -240,7 +227,7 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none bg-white resize-none text-sm" className="w-full px-3 py-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none bg-white resize-none text-sm"
placeholder="Note interne (es. Usato per i nuovi iscritti)" placeholder="Note interne..."
rows={2} rows={2}
/> />
</div> </div>
@@ -252,18 +239,17 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
value={subject} value={subject}
onChange={e => setSubject(e.target.value)} onChange={e => setSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none bg-white" className="w-full px-3 py-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none bg-white"
placeholder="Oggetto... (supporta {{placeholder}})" placeholder="Oggetto..."
/> />
</div> </div>
</div> </div>
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Variabili Attive</span> <span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Variabili</span>
<span className="text-xs text-slate-400">Rilevate automaticamente dal testo</span>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{detectedVars.length === 0 && <span className="text-sm text-slate-400 italic">Nessuna variabile rilevata. Scrivi {'{{nome}}'} per aggiungerne una.</span>} {detectedVars.length === 0 && <span className="text-sm text-slate-400 italic">Nessuna variabile. Usa {'{{nome}}'}.</span>}
{detectedVars.map(v => ( {detectedVars.map(v => (
<span key={v} className="px-2 py-1 bg-brand-100 text-brand-700 text-sm rounded border border-brand-200 font-mono"> <span key={v} className="px-2 py-1 bg-brand-100 text-brand-700 text-sm rounded border border-brand-200 font-mono">
{`{{${v}}}`} {`{{${v}}}`}
@@ -290,51 +276,41 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
<div className="relative"> <div className="relative">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<label className="text-sm font-semibold text-slate-700">Editor Contenuti</label> <label className="text-sm font-semibold text-slate-700">Editor</label>
<div className="flex gap-2">
<button <button
onClick={() => setShowAiModal(true)} onClick={() => setShowAiModal(true)}
className="text-xs flex items-center gap-1 text-purple-600 hover:text-purple-700 font-medium bg-purple-50 px-2 py-1 rounded" className="text-xs flex items-center gap-1 text-purple-600 hover:text-purple-700 font-medium bg-purple-50 px-2 py-1 rounded"
> >
<Wand2 size={14} /> <Wand2 size={14} />
Genera con IA IA
</button> </button>
</div> </div>
</div>
<div className="h-[400px]"> <div className="h-[400px]">
<RichTextEditor <RichTextEditor
key={activeTab} key={activeTab}
value={getActiveContent()} value={getActiveContent()}
onChange={setActiveContent} onChange={setActiveContent}
placeholder={`Crea qui la sezione ${tabNames[activeTab]}...`} placeholder={`Scrivi qui...`}
className="h-full shadow-sm" className="h-full shadow-sm"
/> />
</div> </div>
<p className="mt-2 text-xs text-slate-500">
Usa il pulsante "Variabile" nella toolbar per inserire placeholder come {'{{nome}}'}.
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="w-1/2 bg-slate-100 flex flex-col overflow-hidden"> <div className="w-1/2 bg-slate-100 flex flex-col overflow-hidden">
<div className="p-3 bg-white border-b border-slate-200 flex justify-between items-center shadow-sm z-10 shrink-0"> <div className="p-3 bg-white border-b border-slate-200 flex justify-between items-center shadow-sm shrink-0">
<span className="font-semibold text-slate-600 flex items-center gap-2"> <span className="font-semibold text-slate-600 flex items-center gap-2">
<Eye size={18} /> Anteprima Live <Eye size={18} /> Anteprima
</span> </span>
<div className="text-xs text-slate-400">
Renderizzato come HTML standard
</div>
</div> </div>
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar"> <div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
<div className="w-full max-w-2xl bg-white shadow-xl rounded-lg overflow-hidden min-h-[600px] flex flex-col mx-auto"> <div className="w-full max-w-2xl bg-white shadow-xl rounded-lg overflow-hidden min-h-[600px] flex flex-col mx-auto">
<div className="bg-slate-50 border-b border-slate-100 p-4"> <div className="bg-slate-50 border-b border-slate-100 p-4">
<span className="text-xs font-bold text-slate-400 uppercase">Oggetto:</span> <span className="text-xs font-bold text-slate-400 uppercase">Oggetto:</span>
<p className="text-sm font-medium text-slate-800">{subject.replace(/\{\{([\w\d_-]+)\}\}/g, (match, p1) => `<span class="bg-yellow-100 text-yellow-800 px-1 rounded">${match}</span>`)}</p> <p className="text-sm font-medium text-slate-800">{subject}</p>
</div> </div>
<div dangerouslySetInnerHTML={{ __html: header }} /> <div dangerouslySetInnerHTML={{ __html: header }} />
<div dangerouslySetInnerHTML={{ __html: body }} className="flex-1" /> <div dangerouslySetInnerHTML={{ __html: body }} className="flex-1" />
<div dangerouslySetInnerHTML={{ __html: footer }} /> <div dangerouslySetInnerHTML={{ __html: footer }} />
@@ -349,68 +325,18 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2"> <h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Database size={20} className="text-brand-600"/> <Database size={20} className="text-brand-600"/>
Dettagli Integrazione Integrazione
</h3> </h3>
<button onClick={() => setShowSqlModal(false)} className="text-slate-400 hover:text-slate-600"> <button onClick={() => setShowSqlModal(false)} className="text-slate-400 hover:text-slate-600 text-2xl">&times;</button>
<span className="text-2xl">&times;</span>
</button>
</div> </div>
<div className="space-y-6 overflow-y-auto">
<div className="space-y-8 overflow-y-auto px-1 pb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2 block">1. Setup (Esegui una volta nel DB)</label> <label className="text-xs font-bold text-slate-500 uppercase block mb-1">SQL INSERT/UPDATE</label>
<div className="relative"> <pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm font-mono overflow-x-auto">{currentInsertSql}</pre>
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm font-mono custom-scrollbar h-40">
{currentInsertSql}
</pre>
<button
onClick={() => navigator.clipboard.writeText(currentInsertSql)}
className="absolute top-2 right-2 p-2 bg-slate-700 text-white rounded hover:bg-slate-600"
title="Copia SQL INSERT"
>
<Copy size={16} />
</button>
</div> </div>
</div>
<div> <div>
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2 block">2. Recupero (Nodo SQL n8n)</label> <label className="text-xs font-bold text-slate-500 uppercase block mb-1">JS n8n</label>
<div className="relative"> <pre className="bg-slate-50 border border-slate-200 p-4 rounded-lg text-sm font-mono overflow-x-auto">{currentN8nCode}</pre>
<pre className="bg-slate-800 text-slate-50 p-4 rounded-lg overflow-x-auto text-sm font-mono custom-scrollbar h-40">
{currentSelectSql}
</pre>
<button
onClick={() => navigator.clipboard.writeText(currentSelectSql)}
className="absolute top-2 right-2 p-2 bg-slate-600 text-white rounded hover:bg-slate-500"
title="Copia SQL SELECT"
>
<Copy size={16} />
</button>
</div>
</div>
</div>
<div>
<label className="text-xs font-bold text-purple-600 uppercase tracking-wider mb-2 flex items-center gap-2">
<Code size={16} />
3. Popolamento (Nodo Codice n8n)
</label>
<div className="relative">
<pre className="bg-slate-50 border border-slate-200 text-slate-700 p-4 rounded-lg overflow-x-auto text-sm font-mono custom-scrollbar max-h-60">
{currentN8nCode}
</pre>
<button
onClick={() => navigator.clipboard.writeText(currentN8nCode)}
className="absolute top-2 right-2 p-2 bg-white border border-slate-200 text-slate-600 rounded hover:bg-slate-50 shadow-sm"
title="Copia Codice JS"
>
<Copy size={16} />
</button>
</div>
<p className="text-xs text-slate-500 mt-2">
Incolla questo codice in un <strong>Nodo Code</strong> collegato dopo il tuo nodo SQL. Sostituisci <code>REPLACE_WITH_VALUE</code> con le variabili reali (es. <code>$('NodeName').item.json.name</code>).
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -420,34 +346,21 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
{showAiModal && ( {showAiModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg p-6"> <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg p-6">
<h3 className="text-lg font-bold text-slate-800 mb-2 flex items-center gap-2"> <h3 className="text-lg font-bold text-slate-800 mb-2">Generatore IA</h3>
<Wand2 className="text-purple-600"/>
Generatore Contenuti IA
</h3>
<p className="text-sm text-slate-600 mb-4">
Descrivi cosa vuoi per la sezione <strong>{tabNames[activeTab]}</strong>.
L'IA genererà il codice HTML con i placeholder necessari.
</p>
<textarea <textarea
className="w-full h-32 border border-slate-300 rounded p-3 text-sm focus:ring-2 focus:ring-purple-500 outline-none mb-4" className="w-full h-32 border border-slate-300 rounded p-3 text-sm focus:ring-2 focus:ring-purple-500 outline-none mb-4"
placeholder="es. Scrivi una notifica gentile che avvisi l'utente del cambio password avvenuto con successo. Includi un placeholder per il nome utente." placeholder="Descrivi cosa generare..."
value={aiPrompt} value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)} onChange={(e) => setAiPrompt(e.target.value)}
/> />
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button onClick={() => setShowAiModal(false)} className="px-4 py-2 text-slate-600">Annulla</button>
onClick={() => setShowAiModal(false)}
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded"
>
Annulla
</button>
<button <button
onClick={handleAiGenerate} onClick={handleAiGenerate}
disabled={isGenerating || !aiPrompt} disabled={isGenerating || !aiPrompt}
className={`px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 flex items-center gap-2 ${isGenerating ? 'opacity-70' : ''}`} className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 flex items-center gap-2"
> >
{isGenerating ? 'Generazione...' : 'Genera HTML'} {isGenerating ? 'Generazione...' : 'Genera HTML'}
{!isGenerating && <Wand2 size={16} />}
</button> </button>
</div> </div>
</div> </div>