feat: portal reopen, reports, templates and remote access
This commit is contained in:
parent
6a75a0a9ed
commit
52c03ff1cf
16 changed files with 1387 additions and 16 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue