diff --git a/frontend/src/app/(app)/settings/email/page.tsx b/frontend/src/app/(app)/settings/email/page.tsx index d41a1c3..584e079 100644 --- a/frontend/src/app/(app)/settings/email/page.tsx +++ b/frontend/src/app/(app)/settings/email/page.tsx @@ -12,6 +12,7 @@ import { type EmailLogEntry, } from "@/lib/api"; import { useAuth } from "@/lib/auth"; +import { formatDateTime } from "@/lib/format"; import Modal from "@/components/Modal"; const PLACEHOLDERS = [ @@ -415,7 +416,7 @@ export default function EmailPage() { log.map((e) => ( - {new Date(e.createdAt).toLocaleString("es-CL")} + {formatDateTime(e.createdAt)} {e.template} diff --git a/frontend/src/app/(app)/tickets/[id]/page.tsx b/frontend/src/app/(app)/tickets/[id]/page.tsx index fdc381c..6073429 100644 --- a/frontend/src/app/(app)/tickets/[id]/page.tsx +++ b/frontend/src/app/(app)/tickets/[id]/page.tsx @@ -6,6 +6,7 @@ import { useAuth } from "@/lib/auth"; import { getTicket, getComments, + getTicketEmails, addComment, updateTicketStatus, assignTicket, @@ -15,9 +16,12 @@ import { type Comment, type Group, type TeamMember, + type TicketSentEmail, + type TicketReceivedEmail, } from "@/lib/api"; import BadgeStatus from "@/components/BadgeStatus"; import BadgePriority from "@/components/BadgePriority"; +import { formatDateTime } from "@/lib/format"; import Link from "next/link"; const STATUS_OPTIONS = [ @@ -37,6 +41,8 @@ export default function TicketDetailPage() { const [ticket, setTicket] = useState(null); const [comments, setComments] = useState([]); + const [sentEmails, setSentEmails] = useState([]); + const [receivedEmails, setReceivedEmails] = useState([]); const [groups, setGroups] = useState([]); const [team, setTeam] = useState([]); const [loading, setLoading] = useState(true); @@ -52,12 +58,15 @@ export default function TicketDetailPage() { Promise.all([ getTicket(id).catch(() => null), getComments(id).catch(() => []), + getTicketEmails(id).catch(() => ({ sent: [], received: [] })), getGroups().catch(() => []), getTeam().catch(() => []), ]) - .then(([ticketData, commentsData, groupsData, teamData]) => { + .then(([ticketData, commentsData, emailsData, groupsData, teamData]) => { setTicket(ticketData); setComments(commentsData); + setSentEmails(emailsData.sent ?? []); + setReceivedEmails(emailsData.received ?? []); setGroups(groupsData); setTeam(teamData); }) @@ -303,7 +312,7 @@ export default function TicketDetailPage() {
{c.content}
- {new Date(c.createdAt).toLocaleString("es-CL")} + {formatDateTime(c.createdAt)}
@@ -317,6 +326,74 @@ export default function TicketDetailPage() { + {/* Historial de correos: enviados desde soporte + respuestas + recibidas, integrados al ticket (hilo email <-> ticket) */} +
+
+ + Historial de correos ({sentEmails.length + receivedEmails.length}) +
+
+ {sentEmails.length === 0 && receivedEmails.length === 0 ? ( +
+ + Sin correos asociados a este ticket +
+ ) : ( + [...sentEmails.map((e) => ({ + key: `sent-${e.id}`, + dir: "sent" as const, + who: e.recipient, + subject: e.subject, + extra: e.template, + status: e.status, + error: e.error, + createdAt: e.createdAt, + })), ...receivedEmails.map((e, i) => ({ + key: `recv-${i}-${e.createdAt}`, + dir: "received" as const, + who: e.authorEmail, + subject: e.subject, + extra: e.status === "inserted" ? "Agregado como comentario" : (e.error || e.status), + status: e.status, + error: e.error, + createdAt: e.createdAt, + }))] + .sort((a, b) => +new Date(b.createdAt) - +new Date(a.createdAt)) + .map((m) => ( +
+ + + +
+
+ {m.dir === "sent" ? "Para: " : "De: "}{m.who} +
+
{m.subject}
+
+ {formatDateTime(m.createdAt)} + · + {m.extra} + {m.status !== "sent" && m.status !== "inserted" && ( + + {m.status} + + )} +
+
+
+ )) + )} + + Las respuestas con etiqueta HD-{String(id).padStart(6, "0")} en el asunto + se agregan solas como comentarios (ingesta cada 5 min). + +
+
+ {/* Comment Form */}
@@ -384,11 +461,11 @@ export default function TicketDetailPage() {
Creado - {new Date(ticket.createdAt).toLocaleString("es-CL")} + {formatDateTime(ticket.createdAt)}
Actualizado - {new Date(ticket.updatedAt).toLocaleString("es-CL")} + {formatDateTime(ticket.updatedAt)}
diff --git a/frontend/src/app/(app)/tickets/page.tsx b/frontend/src/app/(app)/tickets/page.tsx index d1be2da..f1df11d 100644 --- a/frontend/src/app/(app)/tickets/page.tsx +++ b/frontend/src/app/(app)/tickets/page.tsx @@ -7,6 +7,7 @@ import { useAuth } from "@/lib/auth"; import BadgeStatus from "@/components/BadgeStatus"; import BadgePriority from "@/components/BadgePriority"; import PaginationComponent from "@/components/Pagination"; +import { formatDate } from "@/lib/format"; import Link from "next/link"; export default function TicketsPage() { @@ -204,7 +205,7 @@ export default function TicketsPage() { {ticket.assigneeName || Sin asignar} - {new Date(ticket.updatedAt).toLocaleDateString("es-CL")} + {formatDate(ticket.updatedAt)} )) diff --git a/frontend/src/components/NotificationBell.tsx b/frontend/src/components/NotificationBell.tsx index 812fe09..faac699 100644 --- a/frontend/src/components/NotificationBell.tsx +++ b/frontend/src/components/NotificationBell.tsx @@ -9,6 +9,7 @@ import { markAllNotificationsRead, type AppNotification, } from "@/lib/api"; +import { formatDate } from "@/lib/format"; const POLL_MS = 60000; @@ -38,7 +39,7 @@ function timeAgo(iso: string): string { if (hours < 24) return `hace ${hours} h`; const days = Math.floor(hours / 24); if (days < 7) return `hace ${days} d`; - return new Date(iso).toLocaleDateString("es-CL"); + return formatDate(iso); } export default function NotificationBell() { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 385ba79..2327d02 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -129,6 +129,35 @@ export async function addComment( }); } +// ==================== Historial de correos del ticket ==================== +// Integración email <-> ticket: enviados desde soporte (mail.log) + +// respuestas recibidas (helpdesk.email_inbound). Visible para quien +// puede ver el ticket (equipo + solicitante). +export interface TicketSentEmail { + id: number; + template: string; + recipient: string; + subject: string; + status: string; + error?: string; + createdAt: string; +} + +export interface TicketReceivedEmail { + authorId?: string; + authorEmail: string; + subject: string; + status: string; + error?: string; + createdAt: string; +} + +export async function getTicketEmails(ticketId: number) { + return fetchApi<{ sent: TicketSentEmail[]; received: TicketReceivedEmail[] }>( + `/api/helpdesk/ticket/${ticketId}/emails` + ); +} + // ==================== Knowledge Base ==================== export async function getKBArticles() { return fetchApi("/api/helpdesk/kb/published"); diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 0000000..3859e9d --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,16 @@ +// Formato de fechas siempre en America/Santiago, independiente de la +// zona horaria del equipo que abre el portal. La API devuelve UTC con +// sufijo Z (timestamptz) y el navegador convierte a esta zona explícita. +export const DISPLAY_TZ = "America/Santiago"; + +export function formatDateTime(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleString("es-CL", { timeZone: DISPLAY_TZ }); +} + +export function formatDate(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleDateString("es-CL", { timeZone: DISPLAY_TZ }); +}