feat: enhance tickets portal and admin flows
This commit is contained in:
parent
9cdd8763b4
commit
c15f0a5b09
67 changed files with 1101 additions and 338 deletions
|
|
@ -7,6 +7,38 @@ import { requireCustomer, requireStaff, requireUser } from "./rbac";
|
|||
|
||||
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]);
|
||||
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT", "COLLABORATOR"]);
|
||||
const PAUSE_REASON_LABELS: Record<string, string> = {
|
||||
NO_CONTACT: "Falta de contato",
|
||||
WAITING_THIRD_PARTY: "Aguardando terceiro",
|
||||
IN_PROCEDURE: "Em procedimento",
|
||||
};
|
||||
|
||||
type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED" | "CLOSED";
|
||||
|
||||
const STATUS_LABELS: Record<TicketStatusNormalized, string> = {
|
||||
PENDING: "Pendente",
|
||||
AWAITING_ATTENDANCE: "Aguardando atendimento",
|
||||
PAUSED: "Pausado",
|
||||
RESOLVED: "Resolvido",
|
||||
CLOSED: "Fechado",
|
||||
};
|
||||
|
||||
const LEGACY_STATUS_MAP: Record<string, TicketStatusNormalized> = {
|
||||
NEW: "PENDING",
|
||||
PENDING: "PENDING",
|
||||
OPEN: "AWAITING_ATTENDANCE",
|
||||
AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE",
|
||||
ON_HOLD: "PAUSED",
|
||||
PAUSED: "PAUSED",
|
||||
RESOLVED: "RESOLVED",
|
||||
CLOSED: "CLOSED",
|
||||
};
|
||||
|
||||
function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
|
||||
if (!status) return "PENDING";
|
||||
const normalized = LEGACY_STATUS_MAP[status.toUpperCase()];
|
||||
return normalized ?? "PENDING";
|
||||
}
|
||||
|
||||
async function ensureManagerTicketAccess(
|
||||
ctx: MutationCtx | QueryCtx,
|
||||
|
|
@ -232,11 +264,6 @@ export const list = query({
|
|||
.query("tickets")
|
||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", args.tenantId).eq("companyId", user.companyId!))
|
||||
.collect();
|
||||
} else if (args.status) {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_status", (q) => q.eq("tenantId", args.tenantId).eq("status", args.status!))
|
||||
.collect();
|
||||
} else if (args.queueId) {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
|
|
@ -258,9 +285,13 @@ export const list = query({
|
|||
}
|
||||
filtered = filtered.filter((t) => t.companyId === user.companyId)
|
||||
}
|
||||
|
||||
const normalizedStatusFilter = args.status ? normalizeStatus(args.status) : null;
|
||||
|
||||
if (args.priority) filtered = filtered.filter((t) => t.priority === args.priority);
|
||||
if (args.channel) filtered = filtered.filter((t) => t.channel === args.channel);
|
||||
if (normalizedStatusFilter) {
|
||||
filtered = filtered.filter((t) => normalizeStatus(t.status) === normalizedStatusFilter);
|
||||
}
|
||||
if (args.search) {
|
||||
const term = args.search.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
|
|
@ -307,7 +338,7 @@ export const list = query({
|
|||
tenantId: t.tenantId,
|
||||
subject: t.subject,
|
||||
summary: t.summary,
|
||||
status: t.status,
|
||||
status: normalizeStatus(t.status),
|
||||
priority: t.priority,
|
||||
channel: t.channel,
|
||||
queue: queueName,
|
||||
|
|
@ -428,7 +459,7 @@ export const getById = query({
|
|||
tenantId: t.tenantId,
|
||||
subject: t.subject,
|
||||
summary: t.summary,
|
||||
status: t.status,
|
||||
status: normalizeStatus(t.status),
|
||||
priority: t.priority,
|
||||
channel: t.channel,
|
||||
queue: queueName,
|
||||
|
|
@ -588,12 +619,13 @@ export const create = mutation({
|
|||
.take(1);
|
||||
const nextRef = existing[0]?.reference ? existing[0].reference + 1 : 41000;
|
||||
const now = Date.now();
|
||||
const initialStatus: TicketStatusNormalized = initialAssigneeId ? "AWAITING_ATTENDANCE" : "PENDING";
|
||||
const id = await ctx.db.insert("tickets", {
|
||||
tenantId: args.tenantId,
|
||||
reference: nextRef,
|
||||
subject,
|
||||
summary: args.summary?.trim() || undefined,
|
||||
status: "NEW",
|
||||
status: initialStatus,
|
||||
priority: args.priority,
|
||||
channel: args.channel,
|
||||
queueId: args.queueId,
|
||||
|
|
@ -847,20 +879,13 @@ export const updateStatus = mutation({
|
|||
}
|
||||
const ticketDoc = ticket as Doc<"tickets">
|
||||
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||
const normalizedStatus = normalizeStatus(status)
|
||||
const now = Date.now();
|
||||
await ctx.db.patch(ticketId, { status, updatedAt: now });
|
||||
const statusPt: Record<string, string> = {
|
||||
NEW: "Novo",
|
||||
OPEN: "Aberto",
|
||||
PENDING: "Pendente",
|
||||
ON_HOLD: "Em espera",
|
||||
RESOLVED: "Resolvido",
|
||||
CLOSED: "Fechado",
|
||||
} as const;
|
||||
await ctx.db.patch(ticketId, { status: normalizedStatus, updatedAt: now });
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId,
|
||||
type: "STATUS_CHANGED",
|
||||
payload: { to: status, toLabel: statusPt[status] ?? status, actorId },
|
||||
payload: { to: normalizedStatus, toLabel: STATUS_LABELS[normalizedStatus], actorId },
|
||||
createdAt: now,
|
||||
});
|
||||
},
|
||||
|
|
@ -1095,8 +1120,13 @@ export const startWork = mutation({
|
|||
})
|
||||
|
||||
export const pauseWork = mutation({
|
||||
args: { ticketId: v.id("tickets"), actorId: v.id("users") },
|
||||
handler: async (ctx, { ticketId, actorId }) => {
|
||||
args: {
|
||||
ticketId: v.id("tickets"),
|
||||
actorId: v.id("users"),
|
||||
reason: v.string(),
|
||||
note: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { ticketId, actorId, reason, note }) => {
|
||||
const ticket = await ctx.db.get(ticketId)
|
||||
if (!ticket) {
|
||||
throw new ConvexError("Ticket não encontrado")
|
||||
|
|
@ -1106,6 +1136,10 @@ export const pauseWork = mutation({
|
|||
return { status: "already_paused" }
|
||||
}
|
||||
|
||||
if (!PAUSE_REASON_LABELS[reason]) {
|
||||
throw new ConvexError("Motivo de pausa inválido")
|
||||
}
|
||||
|
||||
const session = await ctx.db.get(ticket.activeSessionId)
|
||||
if (!session) {
|
||||
await ctx.db.patch(ticketId, { activeSessionId: undefined, working: false })
|
||||
|
|
@ -1118,6 +1152,8 @@ export const pauseWork = mutation({
|
|||
await ctx.db.patch(ticket.activeSessionId, {
|
||||
stoppedAt: now,
|
||||
durationMs,
|
||||
pauseReason: reason,
|
||||
pauseNote: note ?? "",
|
||||
})
|
||||
|
||||
await ctx.db.patch(ticketId, {
|
||||
|
|
@ -1137,11 +1173,19 @@ export const pauseWork = mutation({
|
|||
actorAvatar: actor?.avatarUrl,
|
||||
sessionId: session._id,
|
||||
sessionDurationMs: durationMs,
|
||||
pauseReason: reason,
|
||||
pauseReasonLabel: PAUSE_REASON_LABELS[reason],
|
||||
pauseNote: note ?? "",
|
||||
},
|
||||
createdAt: now,
|
||||
})
|
||||
|
||||
return { status: "paused", durationMs }
|
||||
return {
|
||||
status: "paused",
|
||||
durationMs,
|
||||
pauseReason: reason,
|
||||
pauseNote: note ?? "",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -1228,7 +1272,10 @@ export const playNext = mutation({
|
|||
|
||||
const chosen = candidates[0];
|
||||
const now = Date.now();
|
||||
await ctx.db.patch(chosen._id, { assigneeId: agentId, status: chosen.status === "NEW" ? "OPEN" : chosen.status, updatedAt: now });
|
||||
const currentStatus = normalizeStatus(chosen.status);
|
||||
const nextStatus: TicketStatusNormalized =
|
||||
currentStatus === "PENDING" ? "AWAITING_ATTENDANCE" : currentStatus;
|
||||
await ctx.db.patch(chosen._id, { assigneeId: agentId, status: nextStatus, updatedAt: now });
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId: chosen._id,
|
||||
type: "ASSIGNEE_CHANGED",
|
||||
|
|
@ -1247,7 +1294,7 @@ export const playNext = mutation({
|
|||
tenantId: chosen.tenantId,
|
||||
subject: chosen.subject,
|
||||
summary: chosen.summary,
|
||||
status: chosen.status,
|
||||
status: nextStatus,
|
||||
priority: chosen.priority,
|
||||
channel: chosen.channel,
|
||||
queue: queueName,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue