Auto-pause internal work during lunch
This commit is contained in:
parent
c6a7e0dd0b
commit
ff41a8bd4e
4 changed files with 115 additions and 0 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue