From 01173d973e43c0d819d44253bc8dbc1ddbabcaf9 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Fri, 25 Sep 2026 12:40:13 -0300 Subject: [PATCH] feat: conversacion unificada estilo Freshdesk en ticket (comentarios + correos con cuerpo) --- frontend/src/app/(app)/tickets/[id]/page.tsx | 210 +++++++++++-------- frontend/src/lib/api.ts | 3 + 2 files changed, 121 insertions(+), 92 deletions(-) diff --git a/frontend/src/app/(app)/tickets/[id]/page.tsx b/frontend/src/app/(app)/tickets/[id]/page.tsx index 6073429..6086b8c 100644 --- a/frontend/src/app/(app)/tickets/[id]/page.tsx +++ b/frontend/src/app/(app)/tickets/[id]/page.tsx @@ -32,6 +32,57 @@ const STATUS_OPTIONS = [ { value: "closed", label: "Cerrado" }, ]; +// Hilo único estilo Freshdesk: comentarios + correos enviados desde +// soporte + respuestas recibidas, ordenados cronológicamente. Las +// respuestas por email que ya generaron comentario se muestran una +// sola vez (versión email, con asunto y cuerpo). +type ThreadItem = + | { kind: "comment"; key: string; createdAt: string; userName?: string; content: string; isInternal: boolean } + | { kind: "sent"; key: string; createdAt: string; who: string; subject: string; body?: string; fallback: string } + | { kind: "received"; key: string; createdAt: string; who: string; subject: string; body?: string; status: string; error?: string }; + +function buildThread( + comments: Comment[], + sent: TicketSentEmail[], + received: TicketReceivedEmail[] +): ThreadItem[] { + const linked = new Set( + received.map((r) => r.commentId).filter((v): v is number => typeof v === "number") + ); + const items: ThreadItem[] = [ + ...comments + .filter((c) => !linked.has(c.id)) + .map((c): ThreadItem => ({ + kind: "comment", + key: `comment-${c.id}`, + createdAt: c.createdAt, + userName: c.userName, + content: c.content, + isInternal: c.isInternal, + })), + ...sent.map((e): ThreadItem => ({ + kind: "sent", + key: `sent-${e.id}`, + createdAt: e.createdAt, + who: e.recipient, + subject: e.subject, + body: e.body || undefined, + fallback: e.template, + })), + ...received.map((e, i): ThreadItem => ({ + kind: "received", + key: `recv-${i}-${e.createdAt}`, + createdAt: e.createdAt, + who: e.authorEmail, + subject: e.subject, + body: e.body || undefined, + status: e.status, + error: e.error, + })), + ]; + return items.sort((a, b) => +new Date(a.createdAt) - +new Date(b.createdAt)); +} + export default function TicketDetailPage() { const params = useParams(); const id = Number(params.id); @@ -287,109 +338,84 @@ export default function TicketDetailPage() { )}
- {/* Comments Timeline */} + {/* Conversación estilo Freshdesk: comentarios + hilo de correos */}
- Comentarios ({comments.length}) + Conversación ({buildThread(comments, sentEmails, receivedEmails).length})
-
- {comments.length > 0 ? ( - comments.map((c) => ( -
-
-
- {c.userName || "Usuario"} - {c.isInternal && ( - - Nota Interna +
+ {(() => { + const thread = buildThread(comments, sentEmails, receivedEmails); + if (thread.length === 0) { + return ( +
+ + Sin conversación aún +
+ ); + } + return thread.map((m) => { + if (m.kind === "comment") { + return ( +
+
+
+ {m.userName || "Usuario"} + {m.isInternal && ( + + Nota Interna + + )} +
+
{m.content}
+
{formatDateTime(m.createdAt)}
+
+
+ ); + } + const isSent = m.kind === "sent"; + return ( +
+
+
+ + + {isSent ? `Soporte → ${m.who}` : `De: ${m.who}`} +
+
{m.subject}
+ {isSent ? ( + m.body ? ( +
+ ) : ( +
Plantilla: {m.fallback}
+ ) + ) : m.body ? ( +
{m.body}
+ ) : ( +
+ {m.status === "inserted" + ? "Agregado como comentario" + : `Omitido: ${m.error || m.status}`} +
)} -
-
{c.content}
-
- {formatDateTime(c.createdAt)} +
{formatDateTime(m.createdAt)}
-
- )) - ) : ( -
- - No hay comentarios aún -
- )} -
-
- - {/* 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). + llegan a este hilo (ingesta cada 5 min).
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2327d02..82bb96e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -140,6 +140,7 @@ export interface TicketSentEmail { subject: string; status: string; error?: string; + body?: string; createdAt: string; } @@ -149,6 +150,8 @@ export interface TicketReceivedEmail { subject: string; status: string; error?: string; + body?: string; + commentId?: number; createdAt: string; }