feat: enforce ticket ownership during work sessions
This commit is contained in:
parent
81657e52d8
commit
3972f66c92
6 changed files with 397 additions and 43 deletions
|
|
@ -59,6 +59,83 @@ function normalizeStatus(status: string | null | undefined): TicketStatusNormali
|
|||
return normalized ?? "PENDING";
|
||||
}
|
||||
|
||||
type AgentWorkTotals = {
|
||||
agentId: Id<"users">;
|
||||
agentName: string | null;
|
||||
agentEmail: string | null;
|
||||
avatarUrl: string | null;
|
||||
totalWorkedMs: number;
|
||||
internalWorkedMs: number;
|
||||
externalWorkedMs: number;
|
||||
};
|
||||
|
||||
async function computeAgentWorkTotals(
|
||||
ctx: MutationCtx | QueryCtx,
|
||||
ticketId: Id<"tickets">,
|
||||
referenceNow: number,
|
||||
): Promise<AgentWorkTotals[]> {
|
||||
const sessions = await ctx.db
|
||||
.query("ticketWorkSessions")
|
||||
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
|
||||
.collect();
|
||||
|
||||
if (!sessions.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const totals = new Map<
|
||||
string,
|
||||
{ totalWorkedMs: number; internalWorkedMs: number; externalWorkedMs: number }
|
||||
>();
|
||||
|
||||
for (const session of sessions) {
|
||||
const baseDuration = typeof session.durationMs === "number"
|
||||
? session.durationMs
|
||||
: typeof session.stoppedAt === "number"
|
||||
? session.stoppedAt - session.startedAt
|
||||
: referenceNow - session.startedAt;
|
||||
const durationMs = Math.max(0, baseDuration);
|
||||
if (durationMs <= 0) continue;
|
||||
const key = session.agentId as string;
|
||||
const bucket = totals.get(key) ?? {
|
||||
totalWorkedMs: 0,
|
||||
internalWorkedMs: 0,
|
||||
externalWorkedMs: 0,
|
||||
};
|
||||
bucket.totalWorkedMs += durationMs;
|
||||
const workType = (session.workType ?? "INTERNAL").toUpperCase();
|
||||
if (workType === "EXTERNAL") {
|
||||
bucket.externalWorkedMs += durationMs;
|
||||
} else {
|
||||
bucket.internalWorkedMs += durationMs;
|
||||
}
|
||||
totals.set(key, bucket);
|
||||
}
|
||||
|
||||
if (totals.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const agentIds = Array.from(totals.keys());
|
||||
const agents = await Promise.all(agentIds.map((agentId) => ctx.db.get(agentId as Id<"users">)));
|
||||
|
||||
return agentIds
|
||||
.map((agentId, index) => {
|
||||
const bucket = totals.get(agentId)!;
|
||||
const agentDoc = agents[index] as Doc<"users"> | null;
|
||||
return {
|
||||
agentId: agentId as Id<"users">,
|
||||
agentName: agentDoc?.name ?? null,
|
||||
agentEmail: agentDoc?.email ?? null,
|
||||
avatarUrl: agentDoc?.avatarUrl ?? null,
|
||||
totalWorkedMs: bucket.totalWorkedMs,
|
||||
internalWorkedMs: bucket.internalWorkedMs,
|
||||
externalWorkedMs: bucket.externalWorkedMs,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalWorkedMs - a.totalWorkedMs);
|
||||
}
|
||||
|
||||
async function ensureManagerTicketAccess(
|
||||
ctx: MutationCtx | QueryCtx,
|
||||
manager: Doc<"users">,
|
||||
|
|
@ -744,6 +821,7 @@ export const getById = query({
|
|||
);
|
||||
|
||||
const activeSession = t.activeSessionId ? await ctx.db.get(t.activeSessionId) : null;
|
||||
const perAgentTotals = await computeAgentWorkTotals(ctx, id, serverNow);
|
||||
|
||||
return {
|
||||
id: t._id,
|
||||
|
|
@ -813,6 +891,15 @@ export const getById = query({
|
|||
workType: activeSession.workType ?? "INTERNAL",
|
||||
}
|
||||
: null,
|
||||
perAgentTotals: perAgentTotals.map((item) => ({
|
||||
agentId: item.agentId,
|
||||
agentName: item.agentName,
|
||||
agentEmail: item.agentEmail,
|
||||
avatarUrl: item.avatarUrl,
|
||||
totalWorkedMs: item.totalWorkedMs,
|
||||
internalWorkedMs: item.internalWorkedMs,
|
||||
externalWorkedMs: item.externalWorkedMs,
|
||||
})),
|
||||
},
|
||||
description: undefined,
|
||||
customFields: customFieldsRecord,
|
||||
|
|
@ -1285,6 +1372,10 @@ export const changeAssignee = mutation({
|
|||
if (viewer.role === "MANAGER") {
|
||||
throw new ConvexError("Gestores não podem reatribuir chamados")
|
||||
}
|
||||
const normalizedStatus = normalizeStatus(ticketDoc.status)
|
||||
if (normalizedStatus === "AWAITING_ATTENDANCE" || ticketDoc.activeSessionId) {
|
||||
throw new ConvexError("Pause o atendimento antes de reatribuir o chamado")
|
||||
}
|
||||
const currentAssigneeId = ticketDoc.assigneeId ?? null
|
||||
if (currentAssigneeId && currentAssigneeId !== actorId && !isAdmin) {
|
||||
throw new ConvexError("Somente o responsável atual pode reatribuir este chamado")
|
||||
|
|
@ -1438,6 +1529,7 @@ export const workSummary = query({
|
|||
|
||||
const activeSession = ticket.activeSessionId ? await ctx.db.get(ticket.activeSessionId) : null
|
||||
const serverNow = Date.now()
|
||||
const perAgentTotals = await computeAgentWorkTotals(ctx, ticketId, serverNow)
|
||||
return {
|
||||
ticketId,
|
||||
totalWorkedMs: ticket.totalWorkedMs ?? 0,
|
||||
|
|
@ -1452,6 +1544,15 @@ export const workSummary = query({
|
|||
workType: activeSession.workType ?? "INTERNAL",
|
||||
}
|
||||
: null,
|
||||
perAgentTotals: perAgentTotals.map((item) => ({
|
||||
agentId: item.agentId,
|
||||
agentName: item.agentName,
|
||||
agentEmail: item.agentEmail,
|
||||
avatarUrl: item.avatarUrl,
|
||||
totalWorkedMs: item.totalWorkedMs,
|
||||
internalWorkedMs: item.internalWorkedMs,
|
||||
externalWorkedMs: item.externalWorkedMs,
|
||||
})),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue