178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
import { ConvexError, v } from "convex/values"
|
|
|
|
import { mutation, query } from "./_generated/server"
|
|
import type { Id } from "./_generated/dataModel"
|
|
import { requireStaff } from "./rbac"
|
|
|
|
const DEFAULT_STATUS = "investigating"
|
|
|
|
function timelineId() {
|
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
return crypto.randomUUID()
|
|
}
|
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
}
|
|
|
|
export const list = query({
|
|
args: { tenantId: v.string(), viewerId: v.id("users") },
|
|
handler: async (ctx, { tenantId, viewerId }) => {
|
|
await requireStaff(ctx, viewerId, tenantId)
|
|
const incidents = await ctx.db
|
|
.query("incidents")
|
|
.withIndex("by_tenant_updated", (q) => q.eq("tenantId", tenantId))
|
|
.order("desc")
|
|
.collect()
|
|
return incidents
|
|
},
|
|
})
|
|
|
|
export const createIncident = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
title: v.string(),
|
|
severity: v.string(),
|
|
impactSummary: v.optional(v.string()),
|
|
affectedQueues: v.optional(v.array(v.string())),
|
|
initialUpdate: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, title, severity, impactSummary, affectedQueues, initialUpdate }) => {
|
|
const viewer = await requireStaff(ctx, actorId, tenantId)
|
|
const normalizedTitle = title.trim()
|
|
if (normalizedTitle.length < 3) {
|
|
throw new ConvexError("Informe um título válido para o incidente")
|
|
}
|
|
const now = Date.now()
|
|
const timelineEntry = {
|
|
id: timelineId(),
|
|
authorId: actorId,
|
|
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
|
|
message: initialUpdate?.trim().length ? initialUpdate.trim() : "Incidente registrado.",
|
|
type: "created",
|
|
createdAt: now,
|
|
}
|
|
const id = await ctx.db.insert("incidents", {
|
|
tenantId,
|
|
title: normalizedTitle,
|
|
status: DEFAULT_STATUS,
|
|
severity,
|
|
impactSummary: impactSummary?.trim() || undefined,
|
|
affectedQueues: affectedQueues ?? [],
|
|
ownerId: actorId,
|
|
ownerName: viewer.user.name ?? undefined,
|
|
ownerEmail: viewer.user.email ?? undefined,
|
|
startedAt: now,
|
|
updatedAt: now,
|
|
resolvedAt: undefined,
|
|
timeline: [timelineEntry],
|
|
})
|
|
return id
|
|
},
|
|
})
|
|
|
|
export const updateIncidentStatus = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
incidentId: v.id("incidents"),
|
|
status: v.string(),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, incidentId, status }) => {
|
|
const viewer = await requireStaff(ctx, actorId, tenantId)
|
|
const incident = await ctx.db.get(incidentId)
|
|
if (!incident || incident.tenantId !== tenantId) {
|
|
throw new ConvexError("Incidente não encontrado")
|
|
}
|
|
const now = Date.now()
|
|
const timeline = [
|
|
...(incident.timeline ?? []),
|
|
{
|
|
id: timelineId(),
|
|
authorId: actorId,
|
|
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
|
|
message: `Status atualizado para ${status}`,
|
|
type: "status",
|
|
createdAt: now,
|
|
},
|
|
]
|
|
await ctx.db.patch(incidentId, {
|
|
status,
|
|
updatedAt: now,
|
|
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
|
|
timeline,
|
|
})
|
|
},
|
|
})
|
|
|
|
export const bulkUpdateIncidentStatus = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
incidentIds: v.array(v.id("incidents")),
|
|
status: v.string(),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, incidentIds, status }) => {
|
|
const viewer = await requireStaff(ctx, actorId, tenantId)
|
|
const now = Date.now()
|
|
for (const incidentId of incidentIds) {
|
|
const incident = await ctx.db.get(incidentId)
|
|
if (!incident || incident.tenantId !== tenantId) continue
|
|
const timeline = [
|
|
...(incident.timeline ?? []),
|
|
{
|
|
id: timelineId(),
|
|
authorId: actorId,
|
|
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
|
|
message: `Status atualizado em massa para ${status}`,
|
|
type: "status",
|
|
createdAt: now,
|
|
},
|
|
]
|
|
await ctx.db.patch(incidentId, {
|
|
status,
|
|
updatedAt: now,
|
|
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
|
|
timeline,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
|
|
export const addIncidentUpdate = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
incidentId: v.id("incidents"),
|
|
message: v.string(),
|
|
status: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, incidentId, message, status }) => {
|
|
const viewer = await requireStaff(ctx, actorId, tenantId)
|
|
const incident = await ctx.db.get(incidentId)
|
|
if (!incident || incident.tenantId !== tenantId) {
|
|
throw new ConvexError("Incidente não encontrado")
|
|
}
|
|
const trimmed = message.trim()
|
|
if (trimmed.length < 3) {
|
|
throw new ConvexError("Descreva a atualização do incidente")
|
|
}
|
|
const now = Date.now()
|
|
const timeline = [
|
|
...(incident.timeline ?? []),
|
|
{
|
|
id: timelineId(),
|
|
authorId: actorId,
|
|
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
|
|
message: trimmed,
|
|
type: "update",
|
|
createdAt: now,
|
|
},
|
|
]
|
|
await ctx.db.patch(incidentId, {
|
|
timeline,
|
|
status: status ?? incident.status,
|
|
updatedAt: now,
|
|
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
|
|
})
|
|
},
|
|
})
|