From ce008105c97093055f7629727022247a33d908cd Mon Sep 17 00:00:00 2001 From: Nurfog Date: Thu, 10 Sep 2026 15:51:03 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20p=C3=A1gina=20de=20plantillas=20de=20co?= =?UTF-8?q?rreo=20+=20prueba=20de=20env=C3=ADo=20+=20bit=C3=A1cora?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(app)/settings/email/page.tsx | 356 ++++++++++++++++++ frontend/src/components/Sidebar.tsx | 1 + frontend/src/lib/api.ts | 45 +++ 3 files changed, 402 insertions(+) create mode 100644 frontend/src/app/(app)/settings/email/page.tsx diff --git a/frontend/src/app/(app)/settings/email/page.tsx b/frontend/src/app/(app)/settings/email/page.tsx new file mode 100644 index 0000000..0b5bed3 --- /dev/null +++ b/frontend/src/app/(app)/settings/email/page.tsx @@ -0,0 +1,356 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { + getEmailTemplates, + updateEmailTemplate, + getEmailLog, + sendTestEmail, + type EmailTemplate, + type EmailLogEntry, +} from "@/lib/api"; +import { useAuth } from "@/lib/auth"; +import Modal from "@/components/Modal"; + +const PLACEHOLDERS = [ + "TicketNumber", + "Title", + "Description", + "Status", + "Priority", + "RequesterName", + "AssigneeName", + "Department", + "Link", + "Comment", + "CommentAuthor", +]; + +export default function EmailPage() { + const { user, isLoading: authLoading } = useAuth(); + const [templates, setTemplates] = useState([]); + const [log, setLog] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [showModal, setShowModal] = useState(false); + const [editing, setEditing] = useState(null); + const [subject, setSubject] = useState(""); + const [bodyHtml, setBodyHtml] = useState(""); + const [isActive, setIsActive] = useState(true); + const [saving, setSaving] = useState(false); + const [modalError, setModalError] = useState(""); + const [testTo, setTestTo] = useState(""); + const [testResult, setTestResult] = useState<{ ok: boolean; text: string } | null>(null); + const [testing, setTesting] = useState(false); + + const fetchData = useCallback(async () => { + setLoading(true); + setError(""); + try { + const [t, l] = await Promise.all([getEmailTemplates(), getEmailLog(50)]); + setTemplates(t); + setLog(l); + } catch (err) { + setError(err instanceof Error ? err.message : "Error al cargar"); + setTemplates([]); + setLog([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (authLoading) return; + if (user?.role !== "admin") { + setLoading(false); + return; + } + fetchData(); + }, [fetchData, authLoading, user?.role]); + + const openEdit = (t: EmailTemplate) => { + setEditing(t); + setSubject(t.subject); + setBodyHtml(t.bodyHtml); + setIsActive(t.isActive); + setModalError(""); + setShowModal(true); + }; + + const handleSubmit = async () => { + if (!editing) return; + if (!subject.trim() || !bodyHtml.trim()) { + setModalError("Asunto y cuerpo son requeridos"); + return; + } + setSaving(true); + setModalError(""); + try { + await updateEmailTemplate(editing.key, { + subject: subject.trim(), + bodyHtml: bodyHtml.trim(), + isActive, + }); + setShowModal(false); + fetchData(); + } catch (err) { + setModalError(err instanceof Error ? err.message : "Error al guardar"); + } finally { + setSaving(false); + } + }; + + const handleTest = async () => { + if (!testTo.trim() || !testTo.includes("@")) { + setTestResult({ ok: false, text: "Destino inválido" }); + return; + } + setTesting(true); + setTestResult(null); + try { + await sendTestEmail(testTo.trim()); + setTestResult({ ok: true, text: `Correo de prueba enviado a ${testTo.trim()}` }); + fetchData(); + } catch (err) { + setTestResult({ ok: false, text: err instanceof Error ? err.message : "Error al enviar" }); + } finally { + setTesting(false); + } + }; + + if (!authLoading && user?.role !== "admin") { + return ( +
+
+

Correo

+
+
+ + Solo los administradores pueden gestionar las plantillas de correo. +
+
+ ); + } + + return ( +
+
+

Plantillas de Correo

+
+ + {error && ( +
+ + {error} +
+ )} + +
+
+
+ + + + + + + + + + + {loading ? ( + + + + ) : templates.length === 0 ? ( + + + + ) : ( + templates.map((t) => ( + + + + + + + )) + )} + +
PlantillaAsuntoEstadoAcciones
+
+
+ +
Sin plantillas
+
+
{t.name}
+ + {t.key} + +
{t.subject} + {t.isActive ? ( + Activa + ) : ( + Inactiva + )} + + +
+
+
+
+ +
+
+
+ + Probar envío +
+ {testResult && ( +
+ {testResult.text} +
+ )} +
+ setTestTo(e.target.value)} + style={{ maxWidth: 320 }} + /> + +
+ + Si falla, revisa la bitácora y la configuración SMTP del backend. + +
+
+ +
+ + Últimos envíos +
+
+
+
+ + + + + + + + + + + + {log.length === 0 ? ( + + + + ) : ( + log.map((e) => ( + + + + + + + + )) + )} + +
FechaPlantillaTicketDestinatarioEstado
+ Sin envíos registrados +
+ {new Date(e.createdAt).toLocaleString("es-CL")} + + {e.template} + {e.ticketId ? `#${e.ticketId}` : "—"}{e.recipient} + {e.status === "sent" ? ( + Enviado + ) : e.status === "skipped" ? ( + + Omitido + + ) : ( + + Error + + )} +
+
+
+
+ + setShowModal(false)} + onSubmit={handleSubmit} + submitting={saving} + > + {modalError && ( +
+ {modalError} +
+ )} +
+ + setSubject(e.target.value)} + autoFocus + /> +
+
+ +