Consolidate chat timeline events + auto-end inactive sessions
Timeline consolidation: - Replace multiple LIVE_CHAT_STARTED/ENDED events with single LIVE_CHAT_SUMMARY - Show total duration accumulated across all sessions - Display session count (e.g., "23min 15s total - 3 sessoes") - Show "Ativo" badge when session is active Auto-end inactive chat sessions: - Add cron job running every minute to check inactive sessions - Automatically end sessions after 5 minutes of client inactivity - Mark auto-ended sessions with "(encerrado por inatividade)" flag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
409da8afda
commit
115c5128a6
5 changed files with 224 additions and 2 deletions
|
|
@ -81,6 +81,123 @@ const VISIT_COMPLETED_STATUSES = new Set(["done", "no_show", "canceled"]);
|
|||
|
||||
type AnyCtx = QueryCtx | MutationCtx;
|
||||
|
||||
// Tipos para eventos de chat
|
||||
type ChatEventPayload = {
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
machineHostname?: string;
|
||||
durationMs?: number;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
autoEnded?: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
type TimelineEvent = {
|
||||
_id: Id<"ticketEvents">;
|
||||
type: string;
|
||||
payload?: unknown;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Consolida eventos de chat ao vivo na timeline.
|
||||
* Em vez de mostrar "Chat iniciado" e "Chat finalizado" separadamente,
|
||||
* mostra um unico evento "LIVE_CHAT_SUMMARY" com a duracao total.
|
||||
*/
|
||||
function consolidateChatEventsInTimeline(events: TimelineEvent[]): TimelineEvent[] {
|
||||
const chatStartEvents: TimelineEvent[] = [];
|
||||
const chatEndEvents: TimelineEvent[] = [];
|
||||
const otherEvents: TimelineEvent[] = [];
|
||||
|
||||
// Separar eventos de chat dos demais
|
||||
for (const event of events) {
|
||||
if (event.type === "LIVE_CHAT_STARTED") {
|
||||
chatStartEvents.push(event);
|
||||
} else if (event.type === "LIVE_CHAT_ENDED") {
|
||||
chatEndEvents.push(event);
|
||||
} else {
|
||||
otherEvents.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Se nao houver eventos de chat, retornar como esta
|
||||
if (chatStartEvents.length === 0 && chatEndEvents.length === 0) {
|
||||
return events;
|
||||
}
|
||||
|
||||
// Calcular duracao total de todas as sessoes encerradas
|
||||
let totalDurationMs = 0;
|
||||
const sessionIds = new Set<string>();
|
||||
let lastAgentName = "";
|
||||
let mostRecentActivity = 0;
|
||||
let hasActiveSession = false;
|
||||
|
||||
for (const endEvent of chatEndEvents) {
|
||||
const payload = endEvent.payload as ChatEventPayload | undefined;
|
||||
if (payload?.durationMs) {
|
||||
totalDurationMs += payload.durationMs;
|
||||
}
|
||||
if (payload?.sessionId) {
|
||||
sessionIds.add(payload.sessionId);
|
||||
}
|
||||
if (payload?.agentName) {
|
||||
lastAgentName = payload.agentName;
|
||||
}
|
||||
if (endEvent.createdAt > mostRecentActivity) {
|
||||
mostRecentActivity = endEvent.createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar sessoes iniciadas mas nao encerradas (ativas)
|
||||
const endedSessionIds = new Set(
|
||||
chatEndEvents
|
||||
.map((e) => (e.payload as ChatEventPayload | undefined)?.sessionId)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
for (const startEvent of chatStartEvents) {
|
||||
const payload = startEvent.payload as ChatEventPayload | undefined;
|
||||
const sessionId = payload?.sessionId;
|
||||
if (sessionId && !endedSessionIds.has(sessionId)) {
|
||||
hasActiveSession = true;
|
||||
sessionIds.add(sessionId);
|
||||
}
|
||||
if (payload?.agentName && !lastAgentName) {
|
||||
lastAgentName = payload.agentName;
|
||||
}
|
||||
if (startEvent.createdAt > mostRecentActivity) {
|
||||
mostRecentActivity = startEvent.createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Se so tem sessao iniciada sem encerrar, mostrar o evento de inicio
|
||||
if (chatEndEvents.length === 0 && chatStartEvents.length > 0) {
|
||||
// Retornar o evento de inicio mais recente
|
||||
const latestStart = chatStartEvents.reduce((a, b) =>
|
||||
a.createdAt > b.createdAt ? a : b
|
||||
);
|
||||
return [...otherEvents, latestStart];
|
||||
}
|
||||
|
||||
// Criar evento consolidado
|
||||
const sessionCount = sessionIds.size || chatEndEvents.length;
|
||||
const consolidatedEvent: TimelineEvent = {
|
||||
_id: chatEndEvents[0]._id, // Usar ID do primeiro evento para manter referencia
|
||||
type: "LIVE_CHAT_SUMMARY",
|
||||
payload: {
|
||||
sessionCount,
|
||||
totalDurationMs,
|
||||
agentName: lastAgentName || "Agente",
|
||||
hasActiveSession,
|
||||
},
|
||||
createdAt: mostRecentActivity,
|
||||
};
|
||||
|
||||
return [...otherEvents, consolidatedEvent];
|
||||
}
|
||||
|
||||
type TemplateSummary = {
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -2024,7 +2141,7 @@ export const getById = query({
|
|||
visitPerformedAt: t.visitPerformedAt ?? null,
|
||||
description: undefined,
|
||||
customFields: customFieldsRecord,
|
||||
timeline: timelineRecords.map((ev) => {
|
||||
timeline: consolidateChatEventsInTimeline(timelineRecords).map((ev) => {
|
||||
let payload = ev.payload;
|
||||
if (ev.type === "QUEUE_CHANGED" && payload && typeof payload === "object" && "queueName" in payload) {
|
||||
const normalized = renameQueueString((payload as { queueName?: string }).queueName ?? null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue