feat: cadastro manual de acesso remoto e ajustes de horas
This commit is contained in:
parent
8e3cbc7a9a
commit
f3a7045691
16 changed files with 1549 additions and 207 deletions
|
|
@ -182,6 +182,29 @@ function normalizeStatus(status: string | null | undefined): TicketStatusNormali
|
|||
return normalized ?? "PENDING";
|
||||
}
|
||||
|
||||
function formatWorkDuration(ms: number): string {
|
||||
if (!Number.isFinite(ms) || ms <= 0) {
|
||||
return "0m";
|
||||
}
|
||||
const totalMinutes = Math.round(ms / 60000);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = Math.abs(totalMinutes % 60);
|
||||
const parts: string[] = [];
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0) parts.push(`${minutes}m`);
|
||||
if (parts.length === 0) {
|
||||
return "0m";
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function formatWorkDelta(deltaMs: number): string {
|
||||
if (deltaMs === 0) return "0m";
|
||||
const sign = deltaMs > 0 ? "+" : "-";
|
||||
const absolute = formatWorkDuration(Math.abs(deltaMs));
|
||||
return `${sign}${absolute}`;
|
||||
}
|
||||
|
||||
type AgentWorkTotals = {
|
||||
agentId: Id<"users">;
|
||||
agentName: string | null;
|
||||
|
|
@ -2048,6 +2071,126 @@ export const pauseWork = mutation({
|
|||
},
|
||||
})
|
||||
|
||||
export const adjustWorkSummary = mutation({
|
||||
args: {
|
||||
ticketId: v.id("tickets"),
|
||||
actorId: v.id("users"),
|
||||
internalWorkedMs: v.number(),
|
||||
externalWorkedMs: v.number(),
|
||||
reason: v.string(),
|
||||
},
|
||||
handler: async (ctx, { ticketId, actorId, internalWorkedMs, externalWorkedMs, reason }) => {
|
||||
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)
|
||||
const normalizedRole = (viewer.role ?? "").toUpperCase()
|
||||
if (normalizedRole !== "ADMIN" && normalizedRole !== "AGENT") {
|
||||
throw new ConvexError("Somente administradores e agentes podem ajustar as horas de um chamado.")
|
||||
}
|
||||
if (ticketDoc.activeSessionId) {
|
||||
throw new ConvexError("Pause o atendimento antes de ajustar as horas do chamado.")
|
||||
}
|
||||
|
||||
const trimmedReason = reason.trim()
|
||||
if (trimmedReason.length < 5) {
|
||||
throw new ConvexError("Informe um motivo com pelo menos 5 caracteres.")
|
||||
}
|
||||
if (trimmedReason.length > 1000) {
|
||||
throw new ConvexError("Motivo muito longo (máx. 1000 caracteres).")
|
||||
}
|
||||
|
||||
const previousInternal = Math.max(0, ticketDoc.internalWorkedMs ?? 0)
|
||||
const previousExternal = Math.max(0, ticketDoc.externalWorkedMs ?? 0)
|
||||
const previousTotal = Math.max(0, ticketDoc.totalWorkedMs ?? previousInternal + previousExternal)
|
||||
|
||||
const nextInternal = Math.max(0, Math.round(internalWorkedMs))
|
||||
const nextExternal = Math.max(0, Math.round(externalWorkedMs))
|
||||
const nextTotal = nextInternal + nextExternal
|
||||
|
||||
const deltaInternal = nextInternal - previousInternal
|
||||
const deltaExternal = nextExternal - previousExternal
|
||||
const deltaTotal = nextTotal - previousTotal
|
||||
|
||||
const now = Date.now()
|
||||
await ctx.db.patch(ticketId, {
|
||||
internalWorkedMs: nextInternal,
|
||||
externalWorkedMs: nextExternal,
|
||||
totalWorkedMs: nextTotal,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId,
|
||||
type: "WORK_ADJUSTED",
|
||||
payload: {
|
||||
actorId,
|
||||
actorName: viewer.user.name,
|
||||
actorAvatar: viewer.user.avatarUrl,
|
||||
previousInternalMs: previousInternal,
|
||||
previousExternalMs: previousExternal,
|
||||
previousTotalMs: previousTotal,
|
||||
nextInternalMs: nextInternal,
|
||||
nextExternalMs: nextExternal,
|
||||
nextTotalMs: nextTotal,
|
||||
deltaInternalMs: deltaInternal,
|
||||
deltaExternalMs: deltaExternal,
|
||||
deltaTotalMs: deltaTotal,
|
||||
},
|
||||
createdAt: now,
|
||||
})
|
||||
|
||||
const bodyHtml = [
|
||||
"<p><strong>Ajuste manual de horas</strong></p>",
|
||||
"<ul>",
|
||||
`<li>Horas internas: ${escapeHtml(formatWorkDuration(previousInternal))} → ${escapeHtml(formatWorkDuration(nextInternal))} (${escapeHtml(formatWorkDelta(deltaInternal))})</li>`,
|
||||
`<li>Horas externas: ${escapeHtml(formatWorkDuration(previousExternal))} → ${escapeHtml(formatWorkDuration(nextExternal))} (${escapeHtml(formatWorkDelta(deltaExternal))})</li>`,
|
||||
`<li>Total: ${escapeHtml(formatWorkDuration(previousTotal))} → ${escapeHtml(formatWorkDuration(nextTotal))} (${escapeHtml(formatWorkDelta(deltaTotal))})</li>`,
|
||||
"</ul>",
|
||||
`<p><strong>Motivo:</strong> ${escapeHtml(trimmedReason)}</p>`,
|
||||
].join("")
|
||||
|
||||
const authorSnapshot: CommentAuthorSnapshot = {
|
||||
name: viewer.user.name,
|
||||
email: viewer.user.email,
|
||||
avatarUrl: viewer.user.avatarUrl ?? undefined,
|
||||
teams: viewer.user.teams ?? undefined,
|
||||
}
|
||||
|
||||
await ctx.db.insert("ticketComments", {
|
||||
ticketId,
|
||||
authorId: actorId,
|
||||
visibility: "INTERNAL",
|
||||
body: bodyHtml,
|
||||
authorSnapshot,
|
||||
attachments: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
const perAgentTotals = await computeAgentWorkTotals(ctx, ticketId, now)
|
||||
|
||||
return {
|
||||
ticketId,
|
||||
totalWorkedMs: nextTotal,
|
||||
internalWorkedMs: nextInternal,
|
||||
externalWorkedMs: nextExternal,
|
||||
serverNow: now,
|
||||
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,
|
||||
})),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const updateSubject = mutation({
|
||||
args: { ticketId: v.id("tickets"), subject: v.string(), actorId: v.id("users") },
|
||||
handler: async (ctx, { ticketId, subject, actorId }) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue