feat(visits): concluir/reabrir visita sem poluir agenda

This commit is contained in:
Esdras Renan 2025-11-26 14:21:31 -03:00
parent 8f2c00a75a
commit 66559eafbf
9 changed files with 264 additions and 31 deletions

View file

@ -273,6 +273,8 @@ export default defineSchema({
slaPausedBy: v.optional(v.string()),
slaPausedMs: v.optional(v.number()),
dueAt: v.optional(v.number()), // ms since epoch
visitStatus: v.optional(v.string()),
visitPerformedAt: v.optional(v.number()),
firstResponseAt: v.optional(v.number()),
resolvedAt: v.optional(v.number()),
closedAt: v.optional(v.number()),

View file

@ -76,6 +76,8 @@ const MAX_COMMENT_CHARS = 20000;
const DEFAULT_REOPEN_DAYS = 7;
const MAX_REOPEN_DAYS = 14;
const VISIT_QUEUE_KEYWORDS = ["visita", "visitas", "in loco", "laboratório", "laboratorio", "lab"];
const VISIT_STATUSES = new Set(["scheduled", "en_route", "in_service", "done", "no_show", "canceled"]);
const VISIT_COMPLETED_STATUSES = new Set(["done", "no_show", "canceled"]);
type AnyCtx = QueryCtx | MutationCtx;
@ -2003,6 +2005,8 @@ export const getById = query({
resolvedWithTicketId: t.resolvedWithTicketId ? String(t.resolvedWithTicketId) : null,
reopenDeadline: t.reopenDeadline ?? null,
reopenedAt: t.reopenedAt ?? null,
visitStatus: t.visitStatus ?? null,
visitPerformedAt: t.visitPerformedAt ?? null,
description: undefined,
customFields: customFieldsRecord,
timeline: timelineRecords.map((ev) => {
@ -2247,6 +2251,8 @@ export const create = mutation({
slaPolicyId: undefined,
dueAt: visitDueAt && isVisitQueue ? visitDueAt : undefined,
customFields: normalizedCustomFields.length ? normalizedCustomFields : undefined,
visitStatus: isVisitQueue ? "scheduled" : undefined,
visitPerformedAt: undefined,
...slaFields,
});
await ctx.db.insert("ticketEvents", {
@ -3546,6 +3552,8 @@ export const updateVisitSchedule = mutation({
const actor = viewer.user
await ctx.db.patch(ticketId, {
dueAt: visitDate,
visitStatus: "scheduled",
visitPerformedAt: undefined,
updatedAt: now,
})
await ctx.db.insert("ticketEvents", {
@ -3564,6 +3572,69 @@ export const updateVisitSchedule = mutation({
},
})
export const updateVisitStatus = mutation({
args: {
ticketId: v.id("tickets"),
actorId: v.id("users"),
status: v.string(),
performedAt: v.optional(v.number()),
},
handler: async (ctx, { ticketId, actorId, status, performedAt }) => {
const ticket = await ctx.db.get(ticketId)
if (!ticket) {
throw new ConvexError("Ticket não encontrado")
}
const ticketDoc = ticket as Doc<"tickets">
const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
if (viewer.role === "MANAGER") {
throw new ConvexError("Gestores não podem alterar o status da visita")
}
const normalizedStatus = (status ?? "").toLowerCase()
if (!VISIT_STATUSES.has(normalizedStatus)) {
throw new ConvexError("Status da visita inválido")
}
if (!ticketDoc.queueId) {
throw new ConvexError("Este ticket não possui fila configurada")
}
const queue = (await ctx.db.get(ticketDoc.queueId)) as Doc<"queues"> | null
if (!queue) {
throw new ConvexError("Fila não encontrada para este ticket")
}
const queueLabel = (normalizeQueueName(queue) ?? queue.name ?? "").toLowerCase()
const isVisitQueue = VISIT_QUEUE_KEYWORDS.some((keyword) => queueLabel.includes(keyword))
if (!isVisitQueue) {
throw new ConvexError("Somente tickets da fila de visitas possuem status de visita")
}
const now = Date.now()
const completed = VISIT_COMPLETED_STATUSES.has(normalizedStatus)
const resolvedPerformedAt =
completed && typeof performedAt === "number" && Number.isFinite(performedAt) ? performedAt : completed ? now : undefined
await ctx.db.patch(ticketId, {
visitStatus: normalizedStatus,
visitPerformedAt: completed ? resolvedPerformedAt : undefined,
// Mantemos dueAt para não perder o agendamento original.
dueAt: ticketDoc.dueAt,
updatedAt: now,
})
await ctx.db.insert("ticketEvents", {
ticketId,
type: "VISIT_STATUS_CHANGED",
payload: {
status: normalizedStatus,
performedAt: resolvedPerformedAt,
actorId,
actorName: viewer.user.name,
actorAvatar: viewer.user.avatarUrl ?? undefined,
},
createdAt: now,
})
return { status: normalizedStatus }
},
})
export const changeQueue = mutation({
args: { ticketId: v.id("tickets"), queueId: v.id("queues"), actorId: v.id("users") },
handler: async (ctx, { ticketId, queueId, actorId }) => {