Auto-pause internal work during lunch

This commit is contained in:
Esdras Renan 2025-11-12 17:48:12 -03:00
parent c6a7e0dd0b
commit ff41a8bd4e
4 changed files with 115 additions and 0 deletions

View file

@ -20,10 +20,17 @@ import {
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]);
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
const LUNCH_BREAK_REASON = "LUNCH_BREAK";
const LUNCH_BREAK_NOTE = "Pausa automática do intervalo de almoço";
const LUNCH_BREAK_PAUSE_LABEL = "Intervalo de almoço";
const LUNCH_BREAK_TIMEZONE = "America/Sao_Paulo";
const LUNCH_BREAK_HOUR = 12;
const PAUSE_REASON_LABELS: Record<string, string> = {
NO_CONTACT: "Falta de contato",
WAITING_THIRD_PARTY: "Aguardando terceiro",
IN_PROCEDURE: "Em procedimento",
[LUNCH_BREAK_REASON]: LUNCH_BREAK_PAUSE_LABEL,
};
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
@ -293,6 +300,19 @@ function buildSlaStatusPatch(ticketDoc: Doc<"tickets">, nextStatus: TicketStatus
return {};
}
function getHourMinuteInTimezone(date: number, timeZone: string): { hour: number; minute: number } {
const formatter = new Intl.DateTimeFormat("en-CA", {
timeZone,
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
const parts = formatter.formatToParts(new Date(date))
const hour = Number(parts.find((p) => p.type === "hour")?.value ?? "0")
const minute = Number(parts.find((p) => p.type === "minute")?.value ?? "0")
return { hour, minute }
}
function mergeTicketState(ticketDoc: Doc<"tickets">, patch: Record<string, unknown>): Doc<"tickets"> {
const merged = { ...ticketDoc } as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
@ -305,6 +325,47 @@ function mergeTicketState(ticketDoc: Doc<"tickets">, patch: Record<string, unkno
return merged as Doc<"tickets">;
}
async function pauseSessionForLunch(ctx: MutationCtx, ticket: Doc<"tickets">, session: Doc<"ticketWorkSessions">) {
const now = Date.now()
const durationMs = Math.max(0, now - session.startedAt)
const slaPatch = buildSlaStatusPatch(ticket, "PAUSED", now)
await ctx.db.patch(session._id, {
stoppedAt: now,
durationMs,
pauseReason: LUNCH_BREAK_REASON,
pauseNote: LUNCH_BREAK_NOTE,
})
await ctx.db.patch(ticket._id, {
working: false,
activeSessionId: undefined,
status: "PAUSED",
totalWorkedMs: (ticket.totalWorkedMs ?? 0) + durationMs,
internalWorkedMs: (ticket.internalWorkedMs ?? 0) + durationMs,
updatedAt: now,
...slaPatch,
})
await ctx.db.insert("ticketEvents", {
ticketId: ticket._id,
type: "WORK_PAUSED",
payload: {
actorId: null,
actorName: "Sistema",
actorAvatar: null,
sessionId: session._id,
sessionDurationMs: durationMs,
workType: (session.workType ?? "INTERNAL").toUpperCase(),
pauseReason: LUNCH_BREAK_REASON,
pauseReasonLabel: PAUSE_REASON_LABELS[LUNCH_BREAK_REASON],
pauseNote: LUNCH_BREAK_NOTE,
source: "lunch-break-cron",
},
createdAt: now,
})
}
function buildResponseCompletionPatch(ticketDoc: Doc<"tickets">, now: number) {
if (ticketDoc.firstResponseAt) {
return {};
@ -3683,6 +3744,39 @@ export const pauseWork = mutation({
},
})
export const pauseInternalSessionsForLunch = mutation({
args: {},
handler: async (ctx) => {
const now = Date.now()
const { hour } = getHourMinuteInTimezone(now, LUNCH_BREAK_TIMEZONE)
if (hour !== LUNCH_BREAK_HOUR) {
return { skipped: true, reason: "outside_lunch_window" as const }
}
const activeSessions = await ctx.db
.query("ticketWorkSessions")
.filter((q) => q.eq(q.field("stoppedAt"), undefined))
.collect()
let paused = 0
for (const sessionDoc of activeSessions) {
const session = sessionDoc as Doc<"ticketWorkSessions">
const workType = (session.workType ?? "INTERNAL").toUpperCase()
if (workType !== "INTERNAL") continue
const ticket = (await ctx.db.get(session.ticketId)) as Doc<"tickets"> | null
if (!ticket || ticket.activeSessionId !== session._id) {
continue
}
await pauseSessionForLunch(ctx, ticket, session)
paused += 1
}
return { skipped: false, paused }
},
})
export const adjustWorkSummary = mutation({
args: {
ticketId: v.id("tickets"),