feat: portal reopen, reports, templates and remote access

This commit is contained in:
Esdras Renan 2025-11-13 23:22:17 -03:00
parent 6a75a0a9ed
commit 52c03ff1cf
16 changed files with 1387 additions and 16 deletions

View file

@ -1317,6 +1317,150 @@ export const ticketsByChannel = query({
handler: ticketsByChannelHandler,
});
type MachineCategoryDailyEntry = {
date: string
machineId: Id<"machines"> | null
machineHostname: string | null
companyId: Id<"companies"> | null
companyName: string | null
categoryId: Id<"ticketCategories"> | null
categoryName: string
total: number
}
export async function ticketsByMachineAndCategoryHandler(
ctx: QueryCtx,
{
tenantId,
viewerId,
range,
companyId,
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
) {
const viewer = await requireStaff(ctx, viewerId, tenantId)
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90
const end = new Date()
end.setUTCHours(0, 0, 0, 0)
const endMs = end.getTime() + ONE_DAY_MS
const startMs = endMs - days * ONE_DAY_MS
const tickets = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId)
const categoriesMap = await fetchCategoryMap(ctx, tenantId)
const companyIds = new Set<Id<"companies">>()
for (const ticket of tickets) {
if (ticket.companyId) {
companyIds.add(ticket.companyId)
}
}
const companiesById = new Map<string, Doc<"companies"> | null>()
await Promise.all(
Array.from(companyIds).map(async (id) => {
const doc = await ctx.db.get(id)
companiesById.set(String(id), doc ?? null)
})
)
const aggregated = new Map<string, MachineCategoryDailyEntry>()
for (const ticket of tickets) {
const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null
if (createdAt === null || createdAt < startMs || createdAt >= endMs) continue
const hasMachine = Boolean(ticket.machineId) || Boolean(ticket.machineSnapshot)
if (!hasMachine) continue
const date = formatDateKey(createdAt)
const machineId = (ticket.machineId ?? null) as Id<"machines"> | null
const machineSnapshot = (ticket.machineSnapshot ?? null) as
| {
hostname?: string | null
}
| null
const rawHostname =
typeof machineSnapshot?.hostname === "string" && machineSnapshot.hostname.trim().length > 0
? machineSnapshot.hostname.trim()
: null
const machineHostname = rawHostname ?? null
const snapshot = (ticket.slaSnapshot ?? null) as { categoryId?: Id<"ticketCategories">; categoryName?: string } | null
const rawCategoryId =
ticket.categoryId && typeof ticket.categoryId === "string"
? String(ticket.categoryId)
: snapshot?.categoryId
? String(snapshot.categoryId)
: null
const categoryName = resolveCategoryName(rawCategoryId, snapshot, categoriesMap)
const companyIdValue = (ticket.companyId ?? null) as Id<"companies"> | null
let companyName: string | null = null
if (companyIdValue) {
const company = companiesById.get(String(companyIdValue))
if (company?.name && company.name.trim().length > 0) {
companyName = company.name.trim()
}
}
if (!companyName) {
const companySnapshot = (ticket.companySnapshot ?? null) as { name?: string | null } | null
if (companySnapshot?.name && companySnapshot.name.trim().length > 0) {
companyName = companySnapshot.name.trim()
}
}
if (!companyName) {
companyName = "Sem empresa"
}
const key = [
date,
machineId ? String(machineId) : "null",
machineHostname ?? "",
rawCategoryId ?? "uncategorized",
companyIdValue ? String(companyIdValue) : "null",
].join("|")
const existing = aggregated.get(key)
if (existing) {
existing.total += 1
} else {
aggregated.set(key, {
date,
machineId,
machineHostname,
companyId: companyIdValue,
companyName,
categoryId: (rawCategoryId as Id<"ticketCategories"> | null) ?? null,
categoryName,
total: 1,
})
}
}
const items = Array.from(aggregated.values()).sort((a, b) => {
if (a.date !== b.date) return a.date.localeCompare(b.date)
const machineA = (a.machineHostname ?? "").toLowerCase()
const machineB = (b.machineHostname ?? "").toLowerCase()
if (machineA !== machineB) return machineA.localeCompare(machineB)
return a.categoryName.localeCompare(b.categoryName, "pt-BR")
})
return {
rangeDays: days,
items,
}
}
export const ticketsByMachineAndCategory = query({
args: {
tenantId: v.string(),
viewerId: v.id("users"),
range: v.optional(v.string()),
companyId: v.optional(v.id("companies")),
},
handler: ticketsByMachineAndCategoryHandler,
})
export async function hoursByClientHandler(
ctx: QueryCtx,
{ tenantId, viewerId, range }: { tenantId: string; viewerId: Id<"users">; range?: string }

View file

@ -75,6 +75,7 @@ const MAX_SUMMARY_CHARS = 600;
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"];
type AnyCtx = QueryCtx | MutationCtx;
@ -2013,6 +2014,7 @@ export const create = mutation({
),
formTemplate: v.optional(v.string()),
chatEnabled: v.optional(v.boolean()),
visitDate: v.optional(v.number()),
},
handler: async (ctx, args) => {
const { user: actorUser, role } = await requireUser(ctx, args.actorId, args.tenantId)
@ -2151,6 +2153,23 @@ export const create = mutation({
}
const slaFields = applySlaSnapshot(slaSnapshot, now)
let resolvedQueueDoc: Doc<"queues"> | null = null
if (resolvedQueueId) {
const queueDoc = await ctx.db.get(resolvedQueueId)
if (queueDoc && queueDoc.tenantId === args.tenantId) {
resolvedQueueDoc = queueDoc as Doc<"queues">
}
}
const queueLabel = (resolvedQueueDoc?.slug ?? resolvedQueueDoc?.name ?? "").toLowerCase()
const isVisitQueue = VISIT_QUEUE_KEYWORDS.some((keyword) => queueLabel.includes(keyword))
const visitDueAt =
typeof args.visitDate === "number" && Number.isFinite(args.visitDate) ? args.visitDate : null
if (isVisitQueue && !visitDueAt) {
throw new ConvexError("Informe a data da visita para tickets da fila de visitas")
}
const id = await ctx.db.insert("tickets", {
tenantId: args.tenantId,
reference: nextRef,
@ -2191,7 +2210,7 @@ export const create = mutation({
closedAt: undefined,
tags: [],
slaPolicyId: undefined,
dueAt: undefined,
dueAt: visitDueAt && isVisitQueue ? visitDueAt : undefined,
customFields: normalizedCustomFields.length ? normalizedCustomFields : undefined,
...slaFields,
});