From ff9d95746ef902f3c65175d3db473afed2a01aff Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Wed, 19 Nov 2025 09:24:30 -0300 Subject: [PATCH] Align report filters and update work session flows --- convex/tickets.ts | 90 ++++++++++--------- src/components/date-range-button.tsx | 12 ++- src/components/reports/hours-report.tsx | 25 ++++-- .../tickets/ticket-summary-header.tsx | 54 +++++------ src/components/tickets/tickets-filters.tsx | 2 + src/components/ui/searchable-combobox.tsx | 4 +- src/lib/ticket-matchers.ts | 2 +- 7 files changed, 106 insertions(+), 83 deletions(-) diff --git a/convex/tickets.ts b/convex/tickets.ts index ab6c4e4..5feb4d5 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -2806,7 +2806,8 @@ export const changeAssignee = mutation({ throw new ConvexError("Gestores não podem reatribuir chamados") } const normalizedStatus = normalizeStatus(ticketDoc.status) - if (normalizedStatus === "AWAITING_ATTENDANCE" || ticketDoc.activeSessionId) { + const hasActiveSession = Boolean(ticketDoc.activeSessionId) + if (normalizedStatus === "AWAITING_ATTENDANCE" && !hasActiveSession) { throw new ConvexError("Pause o atendimento antes de reatribuir o chamado") } const currentAssigneeId = ticketDoc.assigneeId ?? null @@ -2833,7 +2834,51 @@ export const changeAssignee = mutation({ avatarUrl: assignee.avatarUrl ?? undefined, teams: assignee.teams ?? undefined, } - await ctx.db.patch(ticketId, { assigneeId, assigneeSnapshot, updatedAt: now }); + const ticketPatch: Partial> = { + assigneeId, + assigneeSnapshot, + updatedAt: now, + } + + if (hasActiveSession) { + const session = await ctx.db.get(ticketDoc.activeSessionId as Id<"ticketWorkSessions">) + if (session) { + const durationMs = Math.max(0, now - session.startedAt) + const sessionType = (session.workType ?? "INTERNAL").toUpperCase() + const deltaInternal = sessionType === "INTERNAL" ? durationMs : 0 + const deltaExternal = sessionType === "EXTERNAL" ? durationMs : 0 + + await ctx.db.patch(session._id, { + stoppedAt: now, + durationMs, + }) + + ticketPatch.totalWorkedMs = (ticketDoc.totalWorkedMs ?? 0) + durationMs + ticketPatch.internalWorkedMs = (ticketDoc.internalWorkedMs ?? 0) + deltaInternal + ticketPatch.externalWorkedMs = (ticketDoc.externalWorkedMs ?? 0) + deltaExternal + + const newSessionId = await ctx.db.insert("ticketWorkSessions", { + ticketId, + agentId: assigneeId, + workType: sessionType, + startedAt: now, + }) + + ticketPatch.activeSessionId = newSessionId + ticketPatch.working = true + ticketPatch.status = "AWAITING_ATTENDANCE" + + ticketDoc.totalWorkedMs = ticketPatch.totalWorkedMs as number + ticketDoc.internalWorkedMs = ticketPatch.internalWorkedMs as number + ticketDoc.externalWorkedMs = ticketPatch.externalWorkedMs as number + ticketDoc.activeSessionId = newSessionId + } else { + ticketPatch.activeSessionId = undefined + ticketPatch.working = false + } + } + + await ctx.db.patch(ticketId, ticketPatch); await ctx.db.insert("ticketEvents", { ticketId, type: "ASSIGNEE_CHANGED", @@ -3717,12 +3762,11 @@ export const startWork = mutation({ } const ticketDoc = ticket as Doc<"tickets"> const viewer = await requireTicketStaff(ctx, actorId, ticketDoc) - const isAdmin = viewer.role === "ADMIN" const currentAssigneeId = ticketDoc.assigneeId ?? null const now = Date.now() - if (currentAssigneeId && currentAssigneeId !== actorId && !isAdmin) { - throw new ConvexError("Somente o responsável atual pode iniciar este chamado") + if (!currentAssigneeId) { + throw new ConvexError("Defina um responsável antes de iniciar o atendimento") } if (ticketDoc.activeSessionId) { @@ -3735,26 +3779,9 @@ export const startWork = mutation({ } } - let assigneePatched = false - const previousAssigneeIdForStart = currentAssigneeId - const previousAssigneeNameForStart = - ((ticketDoc.assigneeSnapshot as { name?: string } | null)?.name as string | undefined) ?? "Não atribuído" - - if (!currentAssigneeId) { - const assigneeSnapshot = { - name: viewer.user.name, - email: viewer.user.email, - avatarUrl: viewer.user.avatarUrl ?? undefined, - teams: viewer.user.teams ?? undefined, - } - await ctx.db.patch(ticketId, { assigneeId: actorId, assigneeSnapshot, updatedAt: now }) - ticketDoc.assigneeId = actorId - assigneePatched = true - } - const sessionId = await ctx.db.insert("ticketWorkSessions", { ticketId, - agentId: actorId, + agentId: currentAssigneeId, workType: (workType ?? "INTERNAL").toUpperCase(), startedAt: now, }) @@ -3768,23 +3795,6 @@ export const startWork = mutation({ ...slaStartPatch, }) - if (assigneePatched) { - await ctx.db.insert("ticketEvents", { - ticketId, - type: "ASSIGNEE_CHANGED", - payload: { - assigneeId: actorId, - assigneeName: viewer.user.name, - actorId, - actorName: viewer.user.name, - actorAvatar: viewer.user.avatarUrl ?? undefined, - previousAssigneeId: previousAssigneeIdForStart, - previousAssigneeName: previousAssigneeNameForStart, - }, - createdAt: now, - }) - } - await ctx.db.insert("ticketEvents", { ticketId, type: "WORK_STARTED", diff --git a/src/components/date-range-button.tsx b/src/components/date-range-button.tsx index fbe11fe..5e72799 100644 --- a/src/components/date-range-button.tsx +++ b/src/components/date-range-button.tsx @@ -20,6 +20,7 @@ type DateRangeButtonProps = { onChange: (next: DateRangeValue) => void className?: string clearLabel?: string + align?: "left" | "center" } function strToDate(value?: string | null): Date | undefined { @@ -41,7 +42,14 @@ function formatPtBR(value?: Date): string { return value ? value.toLocaleDateString("pt-BR") : "" } -export function DateRangeButton({ from, to, onChange, className, clearLabel = "Limpar período" }: DateRangeButtonProps) { +export function DateRangeButton({ + from, + to, + onChange, + className, + clearLabel = "Limpar período", + align = "left", +}: DateRangeButtonProps) { const [open, setOpen] = useState(false) const range: DateRange | undefined = useMemo( () => ({ @@ -123,7 +131,7 @@ export function DateRangeButton({ from, to, onChange, className, clearLabel = "L