Auto-pause internal work during lunch
This commit is contained in:
parent
c6a7e0dd0b
commit
ff41a8bd4e
4 changed files with 115 additions and 0 deletions
|
|
@ -10,4 +10,11 @@ crons.interval(
|
|||
{}
|
||||
)
|
||||
|
||||
crons.daily(
|
||||
"auto-pause-internal-lunch",
|
||||
{ hourUTC: 15, minuteUTC: 0 },
|
||||
api.tickets.pauseInternalSessionsForLunch,
|
||||
{}
|
||||
)
|
||||
|
||||
export default crons
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -77,6 +77,19 @@ Fluxo ideal:
|
|||
}
|
||||
}
|
||||
```
|
||||
- **Checklist pós-preparo**: todo ciclo bem-sucedido sempre termina com este bloco no `rustdesk.log` (nessa ordem):
|
||||
1. `Senha padrão definida com sucesso`
|
||||
2. `Aplicando senha nos perfis do RustDesk`
|
||||
3. Quatro linhas `Senha escrita via fallback ...` / `verification-method ...` / `approve-mode ...` cobrindo `%APPDATA%`, `ProgramData`, `LocalService`, `SystemProfile`
|
||||
4. `Senha e flags de segurança gravadas ...` seguido por `Senha confirmada em ... (FM***8P)` para cada `RustDesk.toml`
|
||||
5. Logs de propagação (`RustDesk.toml propagado para ...`, `RustDesk_local.toml propagado para ...`, `RustDesk2.toml propagado ...`)
|
||||
6. `Serviço RustDesk reiniciado/run ativo` e `remote_id atualizado para ...`
|
||||
|
||||
> Observação: as mensagens `Aviso: chave 'password' não encontrada em ...RustDesk_local.toml` são esperadas — esse arquivo nunca armazena a senha, apenas `verification-method`/`approve-mode`.
|
||||
- **Quando algo der errado**:
|
||||
- Se o log parar em `Senha padrão definida...` e nada mais aparecer, algum erro interrompeu o módulo antes de gravar os arquivos. Rode o Raven com DevTools (`Ctrl+Shift`) e capture o stack trace.
|
||||
- Se aparecer “senha divergente” para algum diretório, execute o script PowerShell acima para confirmar e, se necessário, rode “Preparar” novamente (ele sempre derruba `rustdesk.exe`, limpa os perfis e reaplica o PIN).
|
||||
- Se `Serviço RustDesk reiniciado...` não surgir, pare/inicie manualmente (`Stop-Service RustDesk; Start-Service RustDesk`) após verificar que os arquivos já trazem a senha nova.
|
||||
- **Reforço contínuo**: toda nova execução do botão “Preparar” repete o ciclo (kill → copiar → gravar flags → `sc start`). Se alguém voltar manualmente para “Use both”, o Raven mata o processo, reescreve os TOML e reinicia o serviço — o RustDesk volta travado na senha permanente com o PIN registrado nos logs (`rustdesk.log`).
|
||||
- **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
|
||||
```ts
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ const PAUSE_REASONS = [
|
|||
{ value: "NO_CONTACT", label: "Falta de contato" },
|
||||
{ value: "WAITING_THIRD_PARTY", label: "Aguardando terceiro" },
|
||||
{ value: "IN_PROCEDURE", label: "Em procedimento" },
|
||||
{ value: "LUNCH_BREAK", label: "Intervalo de almoço" },
|
||||
]
|
||||
|
||||
type CustomerOption = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue