fix: reconcile ticket timer with server start

This commit is contained in:
Esdras Renan 2025-10-19 19:52:42 -03:00
parent 1df7e13c8f
commit 3b5676ed35
3 changed files with 96 additions and 5 deletions

View file

@ -31,6 +31,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { reconcileLocalSessionStart, type SessionStartOrigin } from "./ticket-timer.utils"
interface TicketHeaderProps {
ticket: TicketWithDetails
@ -390,6 +391,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
// Guarda um marcador local do início da sessão atual para evitar inflar tempo com
// timestamps defasados vindos da rede. Escolhemos o MAIOR entre (remoto, local).
const localStartAtRef = useRef<number>(0)
const localStartOriginRef = useRef<SessionStartOrigin>("unknown")
useEffect(() => {
if (!workSummary?.activeSession) return
@ -403,12 +405,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
useEffect(() => {
if (!workSummary?.activeSession) {
localStartAtRef.current = 0
localStartOriginRef.current = "unknown"
return
}
const remoteStart = Number(workSummary.activeSession.startedAt) || 0
if (remoteStart > localStartAtRef.current) {
localStartAtRef.current = remoteStart
}
const { localStart, origin } = reconcileLocalSessionStart({
remoteStart: Number(workSummary.activeSession.startedAt) || 0,
localStart: localStartAtRef.current,
origin: localStartOriginRef.current,
})
localStartAtRef.current = localStart
localStartOriginRef.current = origin
}, [workSummary?.activeSession?.id, workSummary?.activeSession?.startedAt])
useEffect(() => {
@ -446,13 +452,21 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
toast.loading("Iniciando atendimento...", { id: "work" })
try {
const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType })
if (result?.status === "already_started") {
const status = (result as { status?: string } | null)?.status ?? "started"
if (status === "already_started") {
toast.info("O atendimento já estava em andamento", { id: "work" })
} else {
toast.success("Atendimento iniciado", { id: "work" })
}
// Otimização local: garantir startedAt correto imediatamente
const startedAtMs = typeof result?.startedAt === "number" ? result.startedAt : Date.now()
if (typeof result?.startedAt === "number") {
localStartOriginRef.current = "remote"
} else if (status === "already_started") {
localStartOriginRef.current = "already-running-fallback"
} else {
localStartOriginRef.current = "fresh-local"
}
localStartAtRef.current = startedAtMs
const sessionId = (result as { sessionId?: unknown })?.sessionId as Id<"ticketWorkSessions"> | undefined
setWorkSummary((prev) => {
@ -500,6 +514,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
// Otimização local: aplicar duração retornada no total e limpar sessão ativa
const delta = typeof (result as { durationMs?: unknown })?.durationMs === "number" ? (result as { durationMs?: number }).durationMs! : 0
localStartAtRef.current = 0
localStartOriginRef.current = "unknown"
setWorkSummary((prev) => {
if (!prev) return prev
const workType = prev.activeSession?.workType ?? "INTERNAL"

View file

@ -0,0 +1,40 @@
export type SessionStartOrigin = "unknown" | "remote" | "fresh-local" | "already-running-fallback"
interface ReconcileArgs {
remoteStart: number
localStart: number
origin: SessionStartOrigin
}
interface ReconcileResult {
localStart: number
origin: SessionStartOrigin
}
/**
* Reconcilia o início da sessão local com o timestamp vindo do servidor.
* Mantém a proteção contra timestamps remotos defasados (que geravam tempo inflado),
* mas permite substituir o fallback local quando sabemos que existia uma sessão em andamento.
*/
export function reconcileLocalSessionStart({ remoteStart, localStart, origin }: ReconcileArgs): ReconcileResult {
const sanitizedRemote = Number(remoteStart) || 0
const sanitizedLocal = Number(localStart) || 0
if (sanitizedRemote <= 0) {
return { localStart: sanitizedLocal, origin }
}
if (sanitizedLocal <= 0) {
return { localStart: sanitizedRemote, origin: "remote" }
}
if (sanitizedRemote >= sanitizedLocal) {
return { localStart: sanitizedRemote, origin: "remote" }
}
if (origin === "already-running-fallback") {
return { localStart: sanitizedRemote, origin: "remote" }
}
return { localStart: sanitizedLocal, origin }
}