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;
}