feat: chat izq/der en tickets, correos con ultimo mensaje, dashboard filtrable solo admin/supervisor

This commit is contained in:
Nurfog committed 2026-09-29 16:15:53 -03:00
1 parent 01173d973e
commit 8b59680588
7 files changed
+526 -41

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
// Utilidades para mostrar y enviar solo el mensaje actual de un correo,
// sin el historial citado (respuestas encadenadas, forwards, firmas).
//
// Se usa en:
// - Hilo del ticket: correos enviados/recibidos muestran solo el último
// mensaje con botón "Ver mensaje completo".
// - Respuesta del agente: antes de POST /comments se limpia el texto para
// enviar/guardar solo lo escrito (revisión futura de casos).
const QUOTE_SPLIT_PATTERNS: RegExp[] = [
// "On Mon, 1 Jan 2024 at 10:00, Juan <j@x.cl> wrote:"
/^\s*On\s.+wrote:\s*$/i,
// "El 1 ene 2024, Juan <j@x.cl> escribió:"
/^\s*El\s.+escribi[óo]:\s*$/i,
// Outlook / Gmail headers del mensaje original
/^\s*-{2,}\s*Mensaje original\s*-{2,}\s*$/i,
/^\s*-{2,}\s*Original Message\s*-{2,}\s*$/i,
/^\s*-{2,}\s*Forwarded message\s*-{2,}\s*$/i,
/^\s*-{2,}\s*Mensaje reenviado\s*-{2,}\s*$/i,
/^\s*_{2,}\s*$/,
// Cabeceras típicas citadas
/^\s*De:\s+.+$/i,
/^\s*From:\s+.+$/i,
/^\s*Enviado:\s+.+$/i,
/^\s*Sent:\s+.+$/i,
/^\s*Para:\s+.+$/i,
/^\s*To:\s+.+$/i,
/^\s*Asunto:\s+.+$/i,
/^\s*Subject:\s+.+$/i,
];
const MAX_PREVIEW_CHARS = 800;
export function stripHtmlToText(html: string): string {
if (typeof window !== "undefined" && typeof DOMParser !== "undefined") {
try {
const doc = new DOMParser().parseFromString(html, "text/html");
// Elimina bloques típicos de historial citado antes de extraer texto
doc
.querySelectorAll(
"blockquote, .gmail_quote, .gmail_attr, .moz-cite-prefix, #is_reply, .quoted, [data-quoted]"
)
.forEach((el) => el.remove());
// <br> y párrafos como saltos de línea
doc.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
doc.querySelectorAll("p, div, li, tr").forEach((el) => {
el.prepend("\n");
});
return (doc.body.textContent || "").replace(/\n{3,}/g, "\n\n").trim();
} catch {
// cae al regex de abajo
}
}
return html
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/\n{3,}/g, "\n\n")
.trim();
}
/** Quita firmas comunes tipo "-- \nJuan" al final. */
function stripSignature(lines: string[]): string[] {
const idx = lines.findIndex((l) => l.trim() === "--" || l.trim() === "—");
if (idx >= 0 && idx > Math.max(0, lines.length - 8)) {
return lines.slice(0, idx);
}
return lines;
}
/**
* Extrae el último mensaje (actual) de un cuerpo de correo con historial.
* Corta en la primera línea que parezca inicio de cita y descarta líneas
* que empiezan con ">". Si no hay cita detectable, recorta por longitud.
*/
export function extractLatestMessage(body: string): {
preview: string;
hasMore: boolean;
} {
const text = (body || "").replace(/\r\n/g, "\n");
if (!text.trim()) return { preview: "", hasMore: false };
const lines = stripSignature(text.split("\n"));
const kept: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Línea citada estilo ">"
if (/^&gt;/.test(trimmed) || /^>/.test(trimmed)) break;
// Inicio de bloque citado ("On ... wrote:", "De:", "----- Mensaje original -----", ...)
if (QUOTE_SPLIT_PATTERNS.some((re) => re.test(line))) break;
kept.push(line);
}
let preview = kept.join("\n").trim();
const hadQuoteCut = kept.length < lines.length;
// Sin corte por cita pero texto larguísimo: recorte por longitud
if (!hadQuoteCut && preview.length > MAX_PREVIEW_CHARS) {
const cut = preview.slice(0, MAX_PREVIEW_CHARS);
const lastSpace = cut.lastIndexOf(" ");
preview = (lastSpace > 400 ? cut.slice(0, lastSpace) : cut).trim() + "…";
return { preview, hasMore: true };
}
if (!preview) {
// Todo parecía cita: muestra un recorte del original
const fallback = text.trim().slice(0, MAX_PREVIEW_CHARS).trim();
return {
preview: fallback.length < text.trim().length ? fallback + "…" : fallback,
hasMore: fallback.length < text.trim().length,
};
}
return { preview, hasMore: hadQuoteCut || preview.length < text.trim().length };
}
/**
* Limpia HTML de correo enviado dejando solo el mensaje actual:
* elimina <blockquote> y contenedores de cita conocidos. Devuelve HTML.
*/
export function extractLatestHtml(html: string): { previewHtml: string; hasMore: boolean } {
if (typeof window === "undefined" || typeof DOMParser === "undefined") {
const text = stripHtmlToText(html);
const { preview, hasMore } = extractLatestMessage(text);
// Sin DOM no podemos recortar HTML con fidelidad: mostramos texto plano
return { previewHtml: "", hasMore: hasMore || preview.length < text.length };
}
try {
const doc = new DOMParser().parseFromString(html, "text/html");
const quoted = doc.querySelectorAll(
"blockquote, .gmail_quote, .gmail_attr, .moz-cite-prefix, #is_reply, .quoted, [data-quoted]"
);
const hasMore = quoted.length > 0;
quoted.forEach((el) => el.remove());
// También corta marcadores textuales dentro del HTML ("On ... wrote:", etc.)
const text = (doc.body.textContent || "").replace(/\r\n/g, "\n");
const { hasMore: textCut } = extractLatestMessage(text);
const previewHtml = doc.body.innerHTML.trim();
return { previewHtml, hasMore: hasMore || textCut };
} catch {
return { previewHtml: html, hasMore: false };
}
}
/**
* Limpia lo que el agente escribió antes de enviar/guardar:
* quita historial pegado y líneas citadas. Solo va el mensaje nuevo.
*/
export function cleanReplyForSend(text: string): string {
const { preview } = extractLatestMessage(text || "");
return preview.trim();
}