From 8b82284e8cd2bdb0c95da31f03690290c4325c19 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Fri, 31 Oct 2025 17:27:51 -0300 Subject: [PATCH] chore: expand reports coverage and upgrade next --- ...sonalizadoCLIENTES_Inventario_Estações.xlsx | Bin 0 -> 19218 bytes convex/machines.ts | 744 +++++-- convex/reports.ts | 1110 +++++----- docs/admin/admin-inventory-ui.md | 14 + machine-inventory-plc-est-025 (2).xlsx | Bin 0 -> 301511 bytes package.json | 4 +- pnpm-lock.yaml | 1939 +---------------- src/app/admin/machines/[id]/tickets/page.tsx | 26 + .../machines/admin-machines-overview.tsx | 103 +- .../machines/machine-breadcrumbs.client.tsx | 45 +- .../machine-tickets-history.client.tsx | 439 ++++ .../tickets/close-ticket-dialog.tsx | 2 +- src/components/ui/searchable-combobox.tsx | 3 +- tests/machines.getById.test.ts | 7 +- tests/machines.listTicketsHistory.test.ts | 255 +++ tests/machines.updatePersona.test.ts | 10 +- tests/reports.productivity-dashboard.test.ts | 267 +++ tests/reports.sla-backlog.test.ts | 199 ++ tests/reports.ticketsByChannel.test.ts | 111 + tests/reports.timeline-hours.test.ts | 260 +++ tests/utils/report-test-helpers.ts | 127 ++ 21 files changed, 2952 insertions(+), 2713 deletions(-) create mode 100644 Relatório personalizadoCLIENTES_Inventario_Estações.xlsx create mode 100644 machine-inventory-plc-est-025 (2).xlsx create mode 100644 src/app/admin/machines/[id]/tickets/page.tsx create mode 100644 src/components/admin/machines/machine-tickets-history.client.tsx create mode 100644 tests/machines.listTicketsHistory.test.ts create mode 100644 tests/reports.productivity-dashboard.test.ts create mode 100644 tests/reports.sla-backlog.test.ts create mode 100644 tests/reports.ticketsByChannel.test.ts create mode 100644 tests/reports.timeline-hours.test.ts create mode 100644 tests/utils/report-test-helpers.ts diff --git a/Relatório personalizadoCLIENTES_Inventario_Estações.xlsx b/Relatório personalizadoCLIENTES_Inventario_Estações.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..38653d26ec66c560e3266e318402cd1bbcd186c3 GIT binary patch literal 19218 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 literal 0 HcmV?d00001 diff --git a/convex/machines.ts b/convex/machines.ts index ac8c5a7..d19caec 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -1,7 +1,8 @@ // ci: trigger convex functions deploy (no-op) import { mutation, query } from "./_generated/server" import { api } from "./_generated/api" -import { ConvexError, v } from "convex/values" +import { paginationOptsValidator } from "convex/server" +import { ConvexError, v, Infer } from "convex/values" import { sha256 } from "@noble/hashes/sha256" import { randomBytes } from "@noble/hashes/utils" import type { Doc, Id } from "./_generated/dataModel" @@ -12,6 +13,8 @@ const DEFAULT_TENANT_ID = "tenant-atlas" const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"]) const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000 +const OPEN_TICKET_STATUSES = new Set(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"]) +const MACHINE_TICKETS_STATS_PAGE_SIZE = 200 type NormalizedIdentifiers = { macs: string[] @@ -876,123 +879,128 @@ export const listByTenant = query({ }, }) +export async function getByIdHandler( + ctx: QueryCtx, + args: { id: Id<"machines">; includeMetadata?: boolean } +) { + const includeMetadata = Boolean(args.includeMetadata) + const now = Date.now() + + const machine = await ctx.db.get(args.id) + if (!machine) return null + + const companyFromId = machine.companyId ? await ctx.db.get(machine.companyId) : null + const machineSlug = machine.companySlug ?? null + let companyFromSlug: typeof companyFromId | null = null + if (!companyFromId && machineSlug) { + companyFromSlug = await ctx.db + .query("companies") + .withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", machineSlug)) + .unique() + } + const resolvedCompany = companyFromId ?? companyFromSlug + + const activeToken = await findActiveMachineToken(ctx, machine._id, now) + + const offlineThresholdMs = getOfflineThresholdMs() + const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs) + const manualStatus = (machine.status ?? "").toLowerCase() + let derivedStatus: string + if (machine.isActive === false) { + derivedStatus = "deactivated" + } else if (["maintenance", "blocked"].includes(manualStatus)) { + derivedStatus = manualStatus + } else if (machine.lastHeartbeatAt) { + const age = now - machine.lastHeartbeatAt + if (age <= offlineThresholdMs) { + derivedStatus = "online" + } else if (age <= staleThresholdMs) { + derivedStatus = "offline" + } else { + derivedStatus = "stale" + } + } else { + derivedStatus = machine.status ?? "unknown" + } + + const meta = includeMetadata ? (machine.metadata ?? null) : null + let metrics: Record | null = null + let inventory: Record | null = null + let postureAlerts: Array> | null = null + let lastPostureAt: number | null = null + if (meta && typeof meta === "object") { + const metaRecord = meta as Record + if (metaRecord.metrics && typeof metaRecord.metrics === "object") { + metrics = metaRecord.metrics as Record + } + if (metaRecord.inventory && typeof metaRecord.inventory === "object") { + inventory = metaRecord.inventory as Record + } + if (Array.isArray(metaRecord.postureAlerts)) { + postureAlerts = metaRecord.postureAlerts as Array> + } + if (typeof metaRecord.lastPostureAt === "number") { + lastPostureAt = metaRecord.lastPostureAt as number + } + } + + const linkedUserIds = machine.linkedUserIds ?? [] + const linkedUsers = await Promise.all( + linkedUserIds.map(async (id) => { + const u = await ctx.db.get(id) + if (!u) return null + return { id: u._id, email: u.email, name: u.name } + }) + ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>) + + return { + id: machine._id, + tenantId: machine.tenantId, + hostname: machine.hostname, + companyId: machine.companyId ?? null, + companySlug: machine.companySlug ?? resolvedCompany?.slug ?? null, + companyName: resolvedCompany?.name ?? null, + osName: machine.osName, + osVersion: machine.osVersion ?? null, + architecture: machine.architecture ?? null, + macAddresses: machine.macAddresses, + serialNumbers: machine.serialNumbers, + authUserId: machine.authUserId ?? null, + authEmail: machine.authEmail ?? null, + persona: machine.persona ?? null, + assignedUserId: machine.assignedUserId ?? null, + assignedUserEmail: machine.assignedUserEmail ?? null, + assignedUserName: machine.assignedUserName ?? null, + assignedUserRole: machine.assignedUserRole ?? null, + linkedUsers, + status: derivedStatus, + isActive: machine.isActive ?? true, + lastHeartbeatAt: machine.lastHeartbeatAt ?? null, + heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.lastHeartbeatAt : null, + registeredBy: machine.registeredBy ?? null, + createdAt: machine.createdAt, + updatedAt: machine.updatedAt, + token: activeToken + ? { + expiresAt: activeToken.expiresAt, + lastUsedAt: activeToken.lastUsedAt ?? null, + usageCount: activeToken.usageCount ?? 0, + } + : null, + metrics, + inventory, + postureAlerts, + lastPostureAt, + remoteAccess: machine.remoteAccess ?? null, + } +} + export const getById = query({ args: { id: v.id("machines"), includeMetadata: v.optional(v.boolean()), }, - handler: async (ctx, args) => { - const includeMetadata = Boolean(args.includeMetadata) - const now = Date.now() - - const machine = await ctx.db.get(args.id) - if (!machine) return null - - const companyFromId = machine.companyId ? await ctx.db.get(machine.companyId) : null - const machineSlug = machine.companySlug ?? null - let companyFromSlug: typeof companyFromId | null = null - if (!companyFromId && machineSlug) { - companyFromSlug = await ctx.db - .query("companies") - .withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", machineSlug)) - .unique() - } - const resolvedCompany = companyFromId ?? companyFromSlug - - const activeToken = await findActiveMachineToken(ctx, machine._id, now) - - const offlineThresholdMs = getOfflineThresholdMs() - const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs) - const manualStatus = (machine.status ?? "").toLowerCase() - let derivedStatus: string - if (machine.isActive === false) { - derivedStatus = "deactivated" - } else if (["maintenance", "blocked"].includes(manualStatus)) { - derivedStatus = manualStatus - } else if (machine.lastHeartbeatAt) { - const age = now - machine.lastHeartbeatAt - if (age <= offlineThresholdMs) { - derivedStatus = "online" - } else if (age <= staleThresholdMs) { - derivedStatus = "offline" - } else { - derivedStatus = "stale" - } - } else { - derivedStatus = machine.status ?? "unknown" - } - - const meta = includeMetadata ? (machine.metadata ?? null) : null - let metrics: Record | null = null - let inventory: Record | null = null - let postureAlerts: Array> | null = null - let lastPostureAt: number | null = null - if (meta && typeof meta === "object") { - const metaRecord = meta as Record - if (metaRecord.metrics && typeof metaRecord.metrics === "object") { - metrics = metaRecord.metrics as Record - } - if (metaRecord.inventory && typeof metaRecord.inventory === "object") { - inventory = metaRecord.inventory as Record - } - if (Array.isArray(metaRecord.postureAlerts)) { - postureAlerts = metaRecord.postureAlerts as Array> - } - if (typeof metaRecord.lastPostureAt === "number") { - lastPostureAt = metaRecord.lastPostureAt as number - } - } - - const linkedUserIds = machine.linkedUserIds ?? [] - const linkedUsers = await Promise.all( - linkedUserIds.map(async (id) => { - const u = await ctx.db.get(id) - if (!u) return null - return { id: u._id, email: u.email, name: u.name } - }) - ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>) - - return { - id: machine._id, - tenantId: machine.tenantId, - hostname: machine.hostname, - companyId: machine.companyId ?? null, - companySlug: machine.companySlug ?? resolvedCompany?.slug ?? null, - companyName: resolvedCompany?.name ?? null, - osName: machine.osName, - osVersion: machine.osVersion ?? null, - architecture: machine.architecture ?? null, - macAddresses: machine.macAddresses, - serialNumbers: machine.serialNumbers, - authUserId: machine.authUserId ?? null, - authEmail: machine.authEmail ?? null, - persona: machine.persona ?? null, - assignedUserId: machine.assignedUserId ?? null, - assignedUserEmail: machine.assignedUserEmail ?? null, - assignedUserName: machine.assignedUserName ?? null, - assignedUserRole: machine.assignedUserRole ?? null, - linkedUsers, - status: derivedStatus, - isActive: machine.isActive ?? true, - lastHeartbeatAt: machine.lastHeartbeatAt ?? null, - heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.lastHeartbeatAt : null, - registeredBy: machine.registeredBy ?? null, - createdAt: machine.createdAt, - updatedAt: machine.updatedAt, - token: activeToken - ? { - expiresAt: activeToken.expiresAt, - lastUsedAt: activeToken.lastUsedAt ?? null, - usageCount: activeToken.usageCount ?? 0, - } - : null, - metrics, - inventory, - postureAlerts, - lastPostureAt, - remoteAccess: machine.remoteAccess ?? null, - } - }, + handler: getByIdHandler, }) export const listAlerts = query({ @@ -1029,7 +1037,7 @@ export const listOpenTickets = query({ handler: async (ctx, { machineId, limit }) => { const machine = await ctx.db.get(machineId) if (!machine) { - return [] + return { totalOpen: 0, hasMore: false, tickets: [] } } const takeLimit = Math.max(1, Math.min(limit ?? 10, 50)) const candidates = await ctx.db @@ -1041,31 +1049,392 @@ export const listOpenTickets = query({ const openTickets = candidates .filter((ticket) => normalizeStatus(ticket.status) !== "RESOLVED") .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)) - .slice(0, takeLimit) + const totalOpen = openTickets.length + const limited = openTickets.slice(0, takeLimit) - return openTickets.map((ticket) => ({ - id: ticket._id, - reference: ticket.reference, - subject: ticket.subject, - status: normalizeStatus(ticket.status), - priority: ticket.priority ?? "MEDIUM", - updatedAt: ticket.updatedAt, - createdAt: ticket.createdAt, - assignee: ticket.assigneeSnapshot - ? { - name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null, - email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null, - } - : null, - machine: { - id: String(ticket.machineId ?? machineId), - hostname: - ((ticket.machineSnapshot as { hostname?: string } | undefined)?.hostname ?? machine.hostname ?? null), - }, - })) + return { + totalOpen, + hasMore: totalOpen > takeLimit, + tickets: limited.map((ticket) => ({ + id: ticket._id, + reference: ticket.reference, + subject: ticket.subject, + status: normalizeStatus(ticket.status), + priority: ticket.priority ?? "MEDIUM", + updatedAt: ticket.updatedAt, + createdAt: ticket.createdAt, + assignee: ticket.assigneeSnapshot + ? { + name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null, + email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null, + } + : null, + machine: { + id: String(ticket.machineId ?? machineId), + hostname: + ((ticket.machineSnapshot as { hostname?: string } | undefined)?.hostname ?? machine.hostname ?? null), + }, + })), + } }, }) +type MachineTicketsHistoryFilter = { + statusFilter: "all" | "open" | "resolved" + priorityFilter: string | null + from: number | null + to: number | null +} + +type ListTicketsHistoryArgs = { + machineId: Id<"machines"> + status?: "all" | "open" | "resolved" + priority?: string + search?: string + from?: number + to?: number + paginationOpts: Infer +} + +type GetTicketsHistoryStatsArgs = { + machineId: Id<"machines"> + status?: "all" | "open" | "resolved" + priority?: string + search?: string + from?: number + to?: number +} + +function createMachineTicketsQuery( + ctx: QueryCtx, + machine: Doc<"machines">, + machineId: Id<"machines">, + filters: MachineTicketsHistoryFilter +) { + let working = ctx.db + .query("tickets") + .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", machineId)) + .order("desc") + + if (filters.statusFilter === "open") { + working = working.filter((q) => + q.or( + q.eq(q.field("status"), "PENDING"), + q.eq(q.field("status"), "AWAITING_ATTENDANCE"), + q.eq(q.field("status"), "PAUSED") + ) + ) + } else if (filters.statusFilter === "resolved") { + working = working.filter((q) => q.eq(q.field("status"), "RESOLVED")) + } + + if (filters.priorityFilter) { + working = working.filter((q) => q.eq(q.field("priority"), filters.priorityFilter)) + } + + if (typeof filters.from === "number") { + working = working.filter((q) => q.gte(q.field("updatedAt"), filters.from!)) + } + + if (typeof filters.to === "number") { + working = working.filter((q) => q.lte(q.field("updatedAt"), filters.to!)) + } + + return working +} + +function matchesTicketSearch(ticket: Doc<"tickets">, searchTerm: string): boolean { + const normalized = searchTerm.trim().toLowerCase() + if (!normalized) return true + + const subject = ticket.subject.toLowerCase() + if (subject.includes(normalized)) return true + + const summary = typeof ticket.summary === "string" ? ticket.summary.toLowerCase() : "" + if (summary.includes(normalized)) return true + + const reference = `#${ticket.reference}`.toLowerCase() + if (reference.includes(normalized)) return true + + const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined + if (requesterSnapshot) { + if (requesterSnapshot.name?.toLowerCase().includes(normalized)) return true + if (requesterSnapshot.email?.toLowerCase().includes(normalized)) return true + } + + const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined + if (assigneeSnapshot) { + if (assigneeSnapshot.name?.toLowerCase().includes(normalized)) return true + if (assigneeSnapshot.email?.toLowerCase().includes(normalized)) return true + } + + return false +} + +export async function listTicketsHistoryHandler(ctx: QueryCtx, args: ListTicketsHistoryArgs) { + const machine = await ctx.db.get(args.machineId) + if (!machine) { + return { + page: [], + isDone: true, + continueCursor: args.paginationOpts.cursor ?? "", + } + } + + const normalizedStatusFilter = args.status ?? "all" + const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null + const searchTerm = args.search?.trim().toLowerCase() ?? null + const from = typeof args.from === "number" ? args.from : null + const to = typeof args.to === "number" ? args.to : null + const filters: MachineTicketsHistoryFilter = { + statusFilter: normalizedStatusFilter, + priorityFilter: normalizedPriorityFilter, + from, + to, + } + + const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate(args.paginationOpts) + + const page = searchTerm ? pageResult.page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) : pageResult.page + const queueCache = new Map | null>() + const items = await Promise.all( + page.map(async (ticket) => { + let queueName: string | null = null + if (ticket.queueId) { + const key = String(ticket.queueId) + if (!queueCache.has(key)) { + queueCache.set(key, (await ctx.db.get(ticket.queueId)) as Doc<"queues"> | null) + } + queueName = queueCache.get(key)?.name ?? null + } + const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined + const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined + return { + id: ticket._id, + reference: ticket.reference, + subject: ticket.subject, + status: normalizeStatus(ticket.status), + priority: (ticket.priority ?? "MEDIUM").toString().toUpperCase(), + updatedAt: ticket.updatedAt ?? ticket.createdAt ?? 0, + createdAt: ticket.createdAt ?? 0, + queue: queueName, + requester: requesterSnapshot + ? { + name: requesterSnapshot.name ?? null, + email: requesterSnapshot.email ?? null, + } + : null, + assignee: assigneeSnapshot + ? { + name: assigneeSnapshot.name ?? null, + email: assigneeSnapshot.email ?? null, + } + : null, + } + }) + ) + + return { + page: items, + isDone: pageResult.isDone, + continueCursor: pageResult.continueCursor, + splitCursor: pageResult.splitCursor ?? undefined, + pageStatus: pageResult.pageStatus ?? undefined, + } +} + +export const listTicketsHistory = query({ + args: { + machineId: v.id("machines"), + status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))), + priority: v.optional(v.string()), + search: v.optional(v.string()), + from: v.optional(v.number()), + to: v.optional(v.number()), + paginationOpts: paginationOptsValidator, + }, + handler: listTicketsHistoryHandler, +}) + +export async function getTicketsHistoryStatsHandler( + ctx: QueryCtx, + args: GetTicketsHistoryStatsArgs +) { + const machine = await ctx.db.get(args.machineId) + if (!machine) { + return { total: 0, openCount: 0, resolvedCount: 0 } + } + + const normalizedStatusFilter = args.status ?? "all" + const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null + const searchTerm = args.search?.trim().toLowerCase() ?? "" + const from = typeof args.from === "number" ? args.from : null + const to = typeof args.to === "number" ? args.to : null + const filters: MachineTicketsHistoryFilter = { + statusFilter: normalizedStatusFilter, + priorityFilter: normalizedPriorityFilter, + from, + to, + } + + let cursor: string | null = null + let total = 0 + let openCount = 0 + let done = false + + while (!done) { + const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate({ + numItems: MACHINE_TICKETS_STATS_PAGE_SIZE, + cursor, + }) + const page = searchTerm ? pageResult.page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) : pageResult.page + total += page.length + for (const ticket of page) { + if (OPEN_TICKET_STATUSES.has(normalizeStatus(ticket.status))) { + openCount += 1 + } + } + done = pageResult.isDone + cursor = pageResult.continueCursor ?? null + if (!cursor) { + done = true + } + } + + return { + total, + openCount, + resolvedCount: total - openCount, + } +} + +export const getTicketsHistoryStats = query({ + args: { + machineId: v.id("machines"), + status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))), + priority: v.optional(v.string()), + search: v.optional(v.string()), + from: v.optional(v.number()), + to: v.optional(v.number()), + }, + handler: getTicketsHistoryStatsHandler, +}) + +export async function updatePersonaHandler( + ctx: MutationCtx, + args: { + machineId: Id<"machines"> + persona?: string | null + assignedUserId?: Id<"users"> + assignedUserEmail?: string | null + assignedUserName?: string | null + assignedUserRole?: string | null + } +) { + const machine = await ctx.db.get(args.machineId) + if (!machine) { + throw new ConvexError("Máquina não encontrada") + } + + let nextPersona = machine.persona ?? undefined + const personaProvided = args.persona !== undefined + if (args.persona !== undefined) { + const trimmed = (args.persona ?? "").trim().toLowerCase() + if (!trimmed) { + nextPersona = undefined + } else if (!ALLOWED_MACHINE_PERSONAS.has(trimmed)) { + throw new ConvexError("Perfil inválido para a máquina") + } else { + nextPersona = trimmed + } + } + + let nextAssignedUserId = machine.assignedUserId ?? undefined + if (args.assignedUserId !== undefined) { + nextAssignedUserId = args.assignedUserId + } + + let nextAssignedEmail = machine.assignedUserEmail ?? undefined + if (args.assignedUserEmail !== undefined) { + const trimmedEmail = (args.assignedUserEmail ?? "").trim().toLowerCase() + nextAssignedEmail = trimmedEmail || undefined + } + + let nextAssignedName = machine.assignedUserName ?? undefined + if (args.assignedUserName !== undefined) { + const trimmedName = (args.assignedUserName ?? "").trim() + nextAssignedName = trimmedName || undefined + } + + let nextAssignedRole = machine.assignedUserRole ?? undefined + if (args.assignedUserRole !== undefined) { + const trimmedRole = (args.assignedUserRole ?? "").trim().toUpperCase() + nextAssignedRole = trimmedRole || undefined + } + + if (personaProvided && !nextPersona) { + nextAssignedUserId = undefined + nextAssignedEmail = undefined + nextAssignedName = undefined + nextAssignedRole = undefined + } + + if (nextPersona && !nextAssignedUserId) { + throw new ConvexError("Associe um usuário ao definir a persona da máquina") + } + + if (nextPersona && nextAssignedUserId) { + const assignedUser = await ctx.db.get(nextAssignedUserId) + if (!assignedUser) { + throw new ConvexError("Usuário vinculado não encontrado") + } + if (assignedUser.tenantId !== machine.tenantId) { + throw new ConvexError("Usuário vinculado pertence a outro tenant") + } + } + + let nextMetadata = machine.metadata + if (nextPersona) { + const collaboratorMeta = { + email: nextAssignedEmail ?? null, + name: nextAssignedName ?? null, + role: nextPersona, + } + nextMetadata = mergeMetadata(machine.metadata, { collaborator: collaboratorMeta }) + } + + const patch: Record = { + persona: nextPersona, + assignedUserId: nextPersona ? nextAssignedUserId : undefined, + assignedUserEmail: nextPersona ? nextAssignedEmail : undefined, + assignedUserName: nextPersona ? nextAssignedName : undefined, + assignedUserRole: nextPersona ? nextAssignedRole : undefined, + updatedAt: Date.now(), + } + if (nextMetadata !== machine.metadata) { + patch.metadata = nextMetadata + } + + if (personaProvided) { + patch.persona = nextPersona + } + + if (nextPersona) { + patch.assignedUserId = nextAssignedUserId + patch.assignedUserEmail = nextAssignedEmail + patch.assignedUserName = nextAssignedName + patch.assignedUserRole = nextAssignedRole + } else if (personaProvided) { + patch.assignedUserId = undefined + patch.assignedUserEmail = undefined + patch.assignedUserName = undefined + patch.assignedUserRole = undefined + } + + await ctx.db.patch(machine._id, patch) + return { ok: true, persona: nextPersona ?? null } +} + export const updatePersona = mutation({ args: { machineId: v.id("machines"), @@ -1075,110 +1444,7 @@ export const updatePersona = mutation({ assignedUserName: v.optional(v.string()), assignedUserRole: v.optional(v.string()), }, - handler: async (ctx, args) => { - const machine = await ctx.db.get(args.machineId) - if (!machine) { - throw new ConvexError("Máquina não encontrada") - } - - let nextPersona = machine.persona ?? undefined - const personaProvided = args.persona !== undefined - if (args.persona !== undefined) { - const trimmed = args.persona.trim().toLowerCase() - if (!trimmed) { - nextPersona = undefined - } else if (!ALLOWED_MACHINE_PERSONAS.has(trimmed)) { - throw new ConvexError("Perfil inválido para a máquina") - } else { - nextPersona = trimmed - } - } - - let nextAssignedUserId = machine.assignedUserId ?? undefined - if (args.assignedUserId !== undefined) { - nextAssignedUserId = args.assignedUserId - } - - let nextAssignedEmail = machine.assignedUserEmail ?? undefined - if (args.assignedUserEmail !== undefined) { - const trimmedEmail = args.assignedUserEmail.trim().toLowerCase() - nextAssignedEmail = trimmedEmail || undefined - } - - let nextAssignedName = machine.assignedUserName ?? undefined - if (args.assignedUserName !== undefined) { - const trimmedName = args.assignedUserName.trim() - nextAssignedName = trimmedName || undefined - } - - let nextAssignedRole = machine.assignedUserRole ?? undefined - if (args.assignedUserRole !== undefined) { - const trimmedRole = args.assignedUserRole.trim().toUpperCase() - nextAssignedRole = trimmedRole || undefined - } - - if (personaProvided && !nextPersona) { - nextAssignedUserId = undefined - nextAssignedEmail = undefined - nextAssignedName = undefined - nextAssignedRole = undefined - } - - if (nextPersona && !nextAssignedUserId) { - throw new ConvexError("Associe um usuário ao definir a persona da máquina") - } - - if (nextPersona && nextAssignedUserId) { - const assignedUser = await ctx.db.get(nextAssignedUserId) - if (!assignedUser) { - throw new ConvexError("Usuário vinculado não encontrado") - } - if (assignedUser.tenantId !== machine.tenantId) { - throw new ConvexError("Usuário vinculado pertence a outro tenant") - } - } - - let nextMetadata = machine.metadata - if (nextPersona) { - const collaboratorMeta = { - email: nextAssignedEmail ?? null, - name: nextAssignedName ?? null, - role: nextPersona, - } - nextMetadata = mergeMetadata(machine.metadata, { collaborator: collaboratorMeta }) - } - - const patch: Record = { - persona: nextPersona, - assignedUserId: nextPersona ? nextAssignedUserId : undefined, - assignedUserEmail: nextPersona ? nextAssignedEmail : undefined, - assignedUserName: nextPersona ? nextAssignedName : undefined, - assignedUserRole: nextPersona ? nextAssignedRole : undefined, - updatedAt: Date.now(), - } - if (nextMetadata !== machine.metadata) { - patch.metadata = nextMetadata - } - - if (personaProvided) { - patch.persona = nextPersona - } - - if (nextPersona) { - patch.assignedUserId = nextAssignedUserId - patch.assignedUserEmail = nextAssignedEmail - patch.assignedUserName = nextAssignedName - patch.assignedUserRole = nextAssignedRole - } else if (personaProvided) { - patch.assignedUserId = undefined - patch.assignedUserEmail = undefined - patch.assignedUserName = undefined - patch.assignedUserRole = undefined - } - - await ctx.db.patch(machine._id, patch) - return { ok: true, persona: nextPersona ?? null } - }, + handler: updatePersonaHandler, }) export const getContext = query({ diff --git a/convex/reports.ts b/convex/reports.ts index f718ec1..6dcfc4f 100644 --- a/convex/reports.ts +++ b/convex/reports.ts @@ -195,397 +195,474 @@ function formatDateKey(timestamp: number) { return `${year}-${month}-${day}`; } +export async function slaOverviewHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + // Optional range filter (createdAt) for reporting purposes, similar ao backlog/csat + 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 inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs); + const queues = await fetchQueues(ctx, tenantId); + + const now = Date.now(); + const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); + const resolvedTickets = inRange.filter((ticket) => { + const status = normalizeStatus(ticket.status); + return status === "RESOLVED"; + }); + const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now); + + const firstResponseTimes = inRange + .filter((ticket) => ticket.firstResponseAt) + .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); + const resolutionTimes = resolvedTickets + .filter((ticket) => ticket.resolvedAt) + .map((ticket) => (ticket.resolvedAt! - ticket.createdAt) / 60000); + + const queueBreakdown = queues.map((queue) => { + const count = openTickets.filter((ticket) => ticket.queueId === queue._id).length; + return { + id: queue._id, + name: queue.name, + open: count, + }; + }); + + return { + totals: { + total: inRange.length, + open: openTickets.length, + resolved: resolvedTickets.length, + overdue: overdueTickets.length, + }, + response: { + averageFirstResponseMinutes: average(firstResponseTimes), + responsesRegistered: firstResponseTimes.length, + }, + resolution: { + averageResolutionMinutes: average(resolutionTimes), + resolvedCount: resolutionTimes.length, + }, + queueBreakdown, + rangeDays: days, + }; +} + export const slaOverview = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - let tickets = await fetchScopedTickets(ctx, tenantId, viewer); - if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) - // Optional range filter (createdAt) for reporting purposes, similar ao backlog/csat - 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 inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs); - const queues = await fetchQueues(ctx, tenantId); - - const now = Date.now(); - const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); - const resolvedTickets = inRange.filter((ticket) => { - const status = normalizeStatus(ticket.status); - return status === "RESOLVED"; - }); - const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now); - - const firstResponseTimes = inRange - .filter((ticket) => ticket.firstResponseAt) - .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); - const resolutionTimes = resolvedTickets - .filter((ticket) => ticket.resolvedAt) - .map((ticket) => (ticket.resolvedAt! - ticket.createdAt) / 60000); - - const queueBreakdown = queues.map((queue) => { - const count = openTickets.filter((ticket) => ticket.queueId === queue._id).length; - return { - id: queue._id, - name: queue.name, - open: count, - }; - }); - - return { - totals: { - total: inRange.length, - open: openTickets.length, - resolved: resolvedTickets.length, - overdue: overdueTickets.length, - }, - response: { - averageFirstResponseMinutes: average(firstResponseTimes), - responsesRegistered: firstResponseTimes.length, - }, - resolution: { - averageResolutionMinutes: average(resolutionTimes), - resolvedCount: resolutionTimes.length, - }, - queueBreakdown, - rangeDays: days, - }; - }, + handler: slaOverviewHandler, }); +export async function csatOverviewHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + const surveysAll = await collectCsatSurveys(ctx, tickets); + 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 surveys = surveysAll.filter((s) => s.receivedAt >= startMs && s.receivedAt < endMs); + + const averageScore = average(surveys.map((item) => item.score)); + const distribution = [1, 2, 3, 4, 5].map((score) => ({ + score, + total: surveys.filter((item) => item.score === score).length, + })); + + return { + totalSurveys: surveys.length, + averageScore, + distribution, + recent: surveys + .slice() + .sort((a, b) => b.receivedAt - a.receivedAt) + .slice(0, 10) + .map((item) => ({ + ticketId: item.ticketId, + reference: item.reference, + score: item.score, + receivedAt: item.receivedAt, + })), + rangeDays: days, + }; +} + export const csatOverview = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - let tickets = await fetchScopedTickets(ctx, tenantId, viewer); - if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) - const surveysAll = await collectCsatSurveys(ctx, tickets); - 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 surveys = surveysAll.filter((s) => s.receivedAt >= startMs && s.receivedAt < endMs); - - const averageScore = average(surveys.map((item) => item.score)); - const distribution = [1, 2, 3, 4, 5].map((score) => ({ - score, - total: surveys.filter((item) => item.score === score).length, - })); - - return { - totalSurveys: surveys.length, - averageScore, - distribution, - recent: surveys - .slice() - .sort((a, b) => b.receivedAt - a.receivedAt) - .slice(0, 10) - .map((item) => ({ - ticketId: item.ticketId, - reference: item.reference, - score: item.score, - receivedAt: item.receivedAt, - })), - rangeDays: days, - }; - }, + handler: csatOverviewHandler, }); +export async function openedResolvedByDayHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + + 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 opened: Record = {} + const resolved: Record = {} + + for (let i = days - 1; i >= 0; i--) { + const d = new Date(endMs - (i + 1) * ONE_DAY_MS) + const key = formatDateKey(d.getTime()) + opened[key] = 0 + resolved[key] = 0 + } + + for (const t of tickets) { + if (t.createdAt >= startMs && t.createdAt < endMs) { + const key = formatDateKey(t.createdAt) + opened[key] = (opened[key] ?? 0) + 1 + } + if (t.resolvedAt && t.resolvedAt >= startMs && t.resolvedAt < endMs) { + const key = formatDateKey(t.resolvedAt) + resolved[key] = (resolved[key] ?? 0) + 1 + } + } + + const series: Array<{ date: string; opened: number; resolved: number }> = [] + for (let i = days - 1; i >= 0; i--) { + const d = new Date(endMs - (i + 1) * ONE_DAY_MS) + const key = formatDateKey(d.getTime()) + series.push({ date: key, opened: opened[key] ?? 0, resolved: resolved[key] ?? 0 }) + } + + return { rangeDays: days, series } +} + export const openedResolvedByDay = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - let tickets = await fetchScopedTickets(ctx, tenantId, viewer); - if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) - - 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 opened: Record = {} - const resolved: Record = {} - - // pre-fill buckets - for (let i = days - 1; i >= 0; i--) { - const d = new Date(endMs - (i + 1) * ONE_DAY_MS) - const key = formatDateKey(d.getTime()) - opened[key] = 0 - resolved[key] = 0 - } - - for (const t of tickets) { - if (t.createdAt >= startMs && t.createdAt < endMs) { - const key = formatDateKey(t.createdAt) - opened[key] = (opened[key] ?? 0) + 1 - } - if (t.resolvedAt && t.resolvedAt >= startMs && t.resolvedAt < endMs) { - const key = formatDateKey(t.resolvedAt) - resolved[key] = (resolved[key] ?? 0) + 1 - } - } - - const series: Array<{ date: string; opened: number; resolved: number }> = [] - for (let i = days - 1; i >= 0; i--) { - const d = new Date(endMs - (i + 1) * ONE_DAY_MS) - const key = formatDateKey(d.getTime()) - series.push({ date: key, opened: opened[key] ?? 0, resolved: resolved[key] ?? 0 }) - } - - return { rangeDays: days, series } - }, + handler: openedResolvedByDayHandler, }) +export async function backlogOverviewHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + // Optional range filter (createdAt) for reporting purposes + 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 inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId); + + const statusCounts = inRange.reduce>((acc, ticket) => { + const status = normalizeStatus(ticket.status); + acc[status] = (acc[status] ?? 0) + 1; + return acc; + }, {} as Record); + + const priorityCounts = inRange.reduce>((acc, ticket) => { + acc[ticket.priority] = (acc[ticket.priority] ?? 0) + 1; + return acc; + }, {}); + + const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); + + const queueMap = new Map(); + for (const ticket of openTickets) { + const queueId = ticket.queueId ? ticket.queueId : "sem-fila"; + const current = queueMap.get(queueId) ?? { name: queueId === "sem-fila" ? "Sem fila" : "", count: 0 }; + current.count += 1; + queueMap.set(queueId, current); + } + + const queues = await fetchQueues(ctx, tenantId); + + for (const queue of queues) { + const entry = queueMap.get(queue._id) ?? { name: queue.name, count: 0 }; + entry.name = queue.name; + queueMap.set(queue._id, entry); + } + + return { + rangeDays: days, + statusCounts, + priorityCounts, + queueCounts: Array.from(queueMap.entries()).map(([id, data]) => ({ + id, + name: data.name, + total: data.count, + })), + totalOpen: openTickets.length, + }; +} + export const backlogOverview = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - // Optional range filter (createdAt) for reporting purposes - 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 inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId); - - const statusCounts = inRange.reduce>((acc, ticket) => { - const status = normalizeStatus(ticket.status); - acc[status] = (acc[status] ?? 0) + 1; - return acc; - }, {} as Record); - - const priorityCounts = inRange.reduce>((acc, ticket) => { - acc[ticket.priority] = (acc[ticket.priority] ?? 0) + 1; - return acc; - }, {}); - - const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); - - const queueMap = new Map(); - for (const ticket of openTickets) { - const queueId = ticket.queueId ? ticket.queueId : "sem-fila"; - const current = queueMap.get(queueId) ?? { name: queueId === "sem-fila" ? "Sem fila" : "", count: 0 }; - current.count += 1; - queueMap.set(queueId, current); - } - - const queues = await fetchQueues(ctx, tenantId); - - for (const queue of queues) { - const entry = queueMap.get(queue._id) ?? { name: queue.name, count: 0 }; - entry.name = queue.name; - queueMap.set(queue._id, entry); - } - - return { - rangeDays: days, - statusCounts, - priorityCounts, - queueCounts: Array.from(queueMap.entries()).map(([id, data]) => ({ - id, - name: data.name, - total: data.count, - })), - totalOpen: openTickets.length, - }; - }, + handler: backlogOverviewHandler, }); // Touch to ensure CI convex_deploy runs and that agentProductivity is deployed +export async function agentProductivityHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId) + let tickets = await fetchScopedTickets(ctx, tenantId, viewer) + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + + 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 inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs) + type Acc = { + agentId: Id<"users"> + name: string | null + email: string | null + open: number + resolved: number + avgFirstResponseMinValues: number[] + avgResolutionMinValues: number[] + workedMs: number + } + const map = new Map() + + for (const t of inRange) { + const assigneeId = t.assigneeId ?? null + if (!assigneeId) continue + let acc = map.get(assigneeId) + if (!acc) { + const user = await ctx.db.get(assigneeId) + acc = { + agentId: assigneeId, + name: user?.name ?? null, + email: user?.email ?? null, + open: 0, + resolved: 0, + avgFirstResponseMinValues: [], + avgResolutionMinValues: [], + workedMs: 0, + } + map.set(assigneeId, acc) + } + const status = normalizeStatus(t.status) + if (OPEN_STATUSES.has(status)) acc.open += 1 + if (status === "RESOLVED") acc.resolved += 1 + if (t.firstResponseAt) acc.avgFirstResponseMinValues.push((t.firstResponseAt - t.createdAt) / 60000) + if (t.resolvedAt) acc.avgResolutionMinValues.push((t.resolvedAt - t.createdAt) / 60000) + } + + for (const [agentId, acc] of map) { + const sessions = await ctx.db + .query("ticketWorkSessions") + .withIndex("by_agent", (q) => q.eq("agentId", agentId as Id<"users">)) + .collect() + let total = 0 + for (const s of sessions) { + const started = s.startedAt + const ended = s.stoppedAt ?? s.startedAt + if (ended < startMs || started >= endMs) continue + total += s.durationMs ?? Math.max(0, (s.stoppedAt ?? Date.now()) - s.startedAt) + } + acc.workedMs = total + } + + const items = Array.from(map.values()).map((acc) => ({ + agentId: acc.agentId, + name: acc.name, + email: acc.email, + open: acc.open, + resolved: acc.resolved, + avgFirstResponseMinutes: average(acc.avgFirstResponseMinValues), + avgResolutionMinutes: average(acc.avgResolutionMinValues), + workedHours: Math.round((acc.workedMs / 3600000) * 100) / 100, + })) + items.sort((a, b) => b.resolved - a.resolved) + return { rangeDays: days, items } +} + export const agentProductivity = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId) - let tickets = await fetchScopedTickets(ctx, tenantId, viewer) - if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) - - 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 inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs) - type Acc = { - agentId: Id<"users"> - name: string | null - email: string | null - open: number - resolved: number - avgFirstResponseMinValues: number[] - avgResolutionMinValues: number[] - workedMs: number - } - const map = new Map() - - for (const t of inRange) { - const assigneeId = t.assigneeId ?? null - if (!assigneeId) continue - let acc = map.get(assigneeId) - if (!acc) { - const user = await ctx.db.get(assigneeId) - acc = { - agentId: assigneeId, - name: user?.name ?? null, - email: user?.email ?? null, - open: 0, - resolved: 0, - avgFirstResponseMinValues: [], - avgResolutionMinValues: [], - workedMs: 0, - } - map.set(assigneeId, acc) - } - const status = normalizeStatus(t.status) - if (OPEN_STATUSES.has(status)) acc.open += 1 - if (status === 'RESOLVED') acc.resolved += 1 - if (t.firstResponseAt) acc.avgFirstResponseMinValues.push((t.firstResponseAt - t.createdAt) / 60000) - if (t.resolvedAt) acc.avgResolutionMinValues.push((t.resolvedAt - t.createdAt) / 60000) - } - - // Sum work sessions by agent - for (const [agentId, acc] of map) { - const sessions = await ctx.db - .query('ticketWorkSessions') - .withIndex('by_agent', (q) => q.eq('agentId', agentId as Id<'users'>)) - .collect() - let total = 0 - for (const s of sessions) { - const started = s.startedAt - const ended = s.stoppedAt ?? s.startedAt - if (ended < startMs || started >= endMs) continue - total += s.durationMs ?? Math.max(0, (s.stoppedAt ?? Date.now()) - s.startedAt) - } - acc.workedMs = total - } - - const items = Array.from(map.values()).map((acc) => ({ - agentId: acc.agentId, - name: acc.name, - email: acc.email, - open: acc.open, - resolved: acc.resolved, - avgFirstResponseMinutes: average(acc.avgFirstResponseMinValues), - avgResolutionMinutes: average(acc.avgResolutionMinValues), - workedHours: Math.round((acc.workedMs / 3600000) * 100) / 100, - })) - // sort by resolved desc - items.sort((a, b) => b.resolved - a.resolved) - return { rangeDays: days, items } - } + handler: agentProductivityHandler, }) +export async function dashboardOverviewHandler( + ctx: QueryCtx, + { tenantId, viewerId }: { tenantId: string; viewerId: Id<"users"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + const tickets = await fetchScopedTickets(ctx, tenantId, viewer); + const now = Date.now(); + + const lastDayStart = now - ONE_DAY_MS; + const previousDayStart = now - 2 * ONE_DAY_MS; + + const newTickets = tickets.filter((ticket) => ticket.createdAt >= lastDayStart); + const previousTickets = tickets.filter( + (ticket) => ticket.createdAt >= previousDayStart && ticket.createdAt < lastDayStart + ); + + const trend = percentageChange(newTickets.length, previousTickets.length); + + const inProgressCurrent = tickets.filter((ticket) => { + if (!ticket.firstResponseAt) return false; + const status = normalizeStatus(ticket.status); + if (status === "RESOLVED") return false; + return !ticket.resolvedAt; + }); + + const inProgressPrevious = tickets.filter((ticket) => { + if (!ticket.firstResponseAt || ticket.firstResponseAt >= lastDayStart) return false; + if (ticket.resolvedAt && ticket.resolvedAt < lastDayStart) return false; + const status = normalizeStatus(ticket.status); + return status !== "RESOLVED" || !ticket.resolvedAt; + }); + + const inProgressTrend = percentageChange(inProgressCurrent.length, inProgressPrevious.length); + + const lastWindowStart = now - 7 * ONE_DAY_MS; + const previousWindowStart = now - 14 * ONE_DAY_MS; + + const firstResponseWindow = tickets + .filter( + (ticket) => + ticket.createdAt >= lastWindowStart && + ticket.createdAt < now && + ticket.firstResponseAt + ) + .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); + const firstResponsePrevious = tickets + .filter( + (ticket) => + ticket.createdAt >= previousWindowStart && + ticket.createdAt < lastWindowStart && + ticket.firstResponseAt + ) + .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); + + const averageWindow = average(firstResponseWindow); + const averagePrevious = average(firstResponsePrevious); + const deltaMinutes = + averageWindow !== null && averagePrevious !== null ? averageWindow - averagePrevious : null; + + const awaitingTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); + const atRiskTickets = awaitingTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now); + + const resolvedLastWindow = tickets.filter( + (ticket) => ticket.resolvedAt && ticket.resolvedAt >= lastWindowStart && ticket.resolvedAt < now + ); + const resolvedPreviousWindow = tickets.filter( + (ticket) => + ticket.resolvedAt && + ticket.resolvedAt >= previousWindowStart && + ticket.resolvedAt < lastWindowStart + ); + const resolutionRate = tickets.length > 0 ? (resolvedLastWindow.length / tickets.length) * 100 : null; + const resolutionDelta = + resolvedPreviousWindow.length > 0 + ? ((resolvedLastWindow.length - resolvedPreviousWindow.length) / resolvedPreviousWindow.length) * 100 + : null; + + return { + newTickets: { + last24h: newTickets.length, + previous24h: previousTickets.length, + trendPercentage: trend, + }, + inProgress: { + current: inProgressCurrent.length, + previousSnapshot: inProgressPrevious.length, + trendPercentage: inProgressTrend, + }, + firstResponse: { + averageMinutes: averageWindow, + previousAverageMinutes: averagePrevious, + deltaMinutes, + responsesCount: firstResponseWindow.length, + }, + awaitingAction: { + total: awaitingTickets.length, + atRisk: atRiskTickets.length, + }, + resolution: { + resolvedLast7d: resolvedLastWindow.length, + previousResolved: resolvedPreviousWindow.length, + rate: resolutionRate, + deltaPercentage: resolutionDelta, + }, + }; +} + export const dashboardOverview = query({ args: { tenantId: v.string(), viewerId: v.id("users") }, - handler: async (ctx, { tenantId, viewerId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - const tickets = await fetchScopedTickets(ctx, tenantId, viewer); - const now = Date.now(); - - const lastDayStart = now - ONE_DAY_MS; - const previousDayStart = now - 2 * ONE_DAY_MS; - - const newTickets = tickets.filter((ticket) => ticket.createdAt >= lastDayStart); - const previousTickets = tickets.filter( - (ticket) => ticket.createdAt >= previousDayStart && ticket.createdAt < lastDayStart - ); - - const trend = percentageChange(newTickets.length, previousTickets.length); - - const inProgressCurrent = tickets.filter((ticket) => { - if (!ticket.firstResponseAt) return false; - const status = normalizeStatus(ticket.status); - if (status === "RESOLVED") return false; - return !ticket.resolvedAt; - }); - - const inProgressPrevious = tickets.filter((ticket) => { - if (!ticket.firstResponseAt || ticket.firstResponseAt >= lastDayStart) return false; - if (ticket.resolvedAt && ticket.resolvedAt < lastDayStart) return false; - const status = normalizeStatus(ticket.status); - return status !== "RESOLVED" || !ticket.resolvedAt; - }); - - const inProgressTrend = percentageChange(inProgressCurrent.length, inProgressPrevious.length); - - const lastWindowStart = now - 7 * ONE_DAY_MS; - const previousWindowStart = now - 14 * ONE_DAY_MS; - - const firstResponseWindow = tickets - .filter( - (ticket) => - ticket.createdAt >= lastWindowStart && - ticket.createdAt < now && - ticket.firstResponseAt - ) - .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); - const firstResponsePrevious = tickets - .filter( - (ticket) => - ticket.createdAt >= previousWindowStart && - ticket.createdAt < lastWindowStart && - ticket.firstResponseAt - ) - .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000); - - const averageWindow = average(firstResponseWindow); - const averagePrevious = average(firstResponsePrevious); - const deltaMinutes = - averageWindow !== null && averagePrevious !== null ? averageWindow - averagePrevious : null; - - const awaitingTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status))); - const atRiskTickets = awaitingTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now); - - const resolvedLastWindow = tickets.filter( - (ticket) => ticket.resolvedAt && ticket.resolvedAt >= lastWindowStart && ticket.resolvedAt < now - ); - const resolvedPreviousWindow = tickets.filter( - (ticket) => - ticket.resolvedAt && - ticket.resolvedAt >= previousWindowStart && - ticket.resolvedAt < lastWindowStart - ); - const resolutionRate = tickets.length > 0 ? (resolvedLastWindow.length / tickets.length) * 100 : null; - const resolutionDelta = - resolvedPreviousWindow.length > 0 - ? ((resolvedLastWindow.length - resolvedPreviousWindow.length) / resolvedPreviousWindow.length) * 100 - : null; - - return { - newTickets: { - last24h: newTickets.length, - previous24h: previousTickets.length, - trendPercentage: trend, - }, - inProgress: { - current: inProgressCurrent.length, - previousSnapshot: inProgressPrevious.length, - trendPercentage: inProgressTrend, - }, - firstResponse: { - averageMinutes: averageWindow, - previousAverageMinutes: averagePrevious, - deltaMinutes, - responsesCount: firstResponseWindow.length, - }, - awaitingAction: { - total: awaitingTickets.length, - atRisk: atRiskTickets.length, - }, - resolution: { - resolvedLast7d: resolvedLastWindow.length, - previousResolved: resolvedPreviousWindow.length, - rate: resolutionRate, - deltaPercentage: resolutionDelta, - }, - }; - }, + handler: dashboardOverviewHandler, }); +export async function ticketsByChannelHandler( + ctx: QueryCtx, + { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + 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 timeline = new Map>(); + for (let ts = startMs; ts < endMs; ts += ONE_DAY_MS) { + timeline.set(formatDateKey(ts), new Map()); + } + + const channels = new Set(); + + for (const ticket of tickets) { + if (ticket.createdAt < startMs || ticket.createdAt >= endMs) continue; + const dateKey = formatDateKey(ticket.createdAt); + const channelKey = ticket.channel ?? "OUTRO"; + channels.add(channelKey); + const dayMap = timeline.get(dateKey) ?? new Map(); + dayMap.set(channelKey, (dayMap.get(channelKey) ?? 0) + 1); + timeline.set(dateKey, dayMap); + } + + const sortedChannels = Array.from(channels).sort(); + + const points = Array.from(timeline.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([date, map]) => { + const values: Record = {}; + for (const channel of sortedChannels) { + values[channel] = map.get(channel) ?? 0; + } + return { date, values }; + }); + + return { + rangeDays: days, + channels: sortedChannels, + points, + }; +} + export const ticketsByChannel = query({ args: { tenantId: v.string(), @@ -593,184 +670,147 @@ export const ticketsByChannel = query({ range: v.optional(v.string()), companyId: v.optional(v.id("companies")), }, - handler: async (ctx, { tenantId, viewerId, range, companyId }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId); - let tickets = await fetchScopedTickets(ctx, tenantId, viewer); - if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) - 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 timeline = new Map>(); - for (let ts = startMs; ts < endMs; ts += ONE_DAY_MS) { - timeline.set(formatDateKey(ts), new Map()); - } - - const channels = new Set(); - - for (const ticket of tickets) { - if (ticket.createdAt < startMs || ticket.createdAt >= endMs) continue; - const dateKey = formatDateKey(ticket.createdAt); - const channelKey = ticket.channel ?? "OUTRO"; - channels.add(channelKey); - const dayMap = timeline.get(dateKey) ?? new Map(); - dayMap.set(channelKey, (dayMap.get(channelKey) ?? 0) + 1); - timeline.set(dateKey, dayMap); - } - - const sortedChannels = Array.from(channels).sort(); - - const points = Array.from(timeline.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([date, map]) => { - const values: Record = {}; - for (const channel of sortedChannels) { - values[channel] = map.get(channel) ?? 0; - } - return { date, values }; - }); - - return { - rangeDays: days, - channels: sortedChannels, - points, - }; - }, + handler: ticketsByChannelHandler, }); +export async function hoursByClientHandler( + ctx: QueryCtx, + { tenantId, viewerId, range }: { tenantId: string; viewerId: Id<"users">; range?: string } +) { + const viewer = await requireStaff(ctx, viewerId, tenantId) + const tickets = await fetchScopedTickets(ctx, tenantId, viewer) + + 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 + + type Acc = { + companyId: Id<"companies"> + name: string + isAvulso: boolean + internalMs: number + externalMs: number + totalMs: number + contractedHoursPerMonth?: number | null + } + const map = new Map() + + for (const t of tickets) { + if (t.updatedAt < startMs || t.updatedAt >= endMs) continue + const companyId = t.companyId ?? null + if (!companyId) continue + + let acc = map.get(companyId) + if (!acc) { + const company = await ctx.db.get(companyId) + acc = { + companyId, + name: company?.name ?? "Sem empresa", + isAvulso: Boolean(company?.isAvulso ?? false), + internalMs: 0, + externalMs: 0, + totalMs: 0, + contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null, + } + map.set(companyId, acc) + } + const internal = t.internalWorkedMs ?? 0 + const external = t.externalWorkedMs ?? 0 + acc.internalMs += internal + acc.externalMs += external + acc.totalMs += internal + external + } + + const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs) + return { + rangeDays: days, + items: items.map((i) => ({ + companyId: i.companyId, + name: i.name, + isAvulso: i.isAvulso, + internalMs: i.internalMs, + externalMs: i.externalMs, + totalMs: i.totalMs, + contractedHoursPerMonth: i.contractedHoursPerMonth ?? null, + })), + } +} + export const hoursByClient = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) }, - handler: async (ctx, { tenantId, viewerId, range }) => { - const viewer = await requireStaff(ctx, viewerId, tenantId) - const tickets = await fetchScopedTickets(ctx, tenantId, viewer) - - 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 - - // Accumulate by company - type Acc = { - companyId: Id<"companies"> - name: string - isAvulso: boolean - internalMs: number - externalMs: number - totalMs: number - contractedHoursPerMonth?: number | null - } - const map = new Map() - - for (const t of tickets) { - // only consider tickets updated in range as a proxy for recent work - if (t.updatedAt < startMs || t.updatedAt >= endMs) continue - const companyId = t.companyId ?? null - if (!companyId) continue - - let acc = map.get(companyId) - if (!acc) { - const company = await ctx.db.get(companyId) - acc = { - companyId, - name: company?.name ?? "Sem empresa", - isAvulso: Boolean(company?.isAvulso ?? false), - internalMs: 0, - externalMs: 0, - totalMs: 0, - contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null, - } - map.set(companyId, acc) - } - const internal = t.internalWorkedMs ?? 0 - const external = t.externalWorkedMs ?? 0 - acc.internalMs += internal - acc.externalMs += external - acc.totalMs += internal + external - } - - const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs) - return { - rangeDays: days, - items: items.map((i) => ({ - companyId: i.companyId, - name: i.name, - isAvulso: i.isAvulso, - internalMs: i.internalMs, - externalMs: i.externalMs, - totalMs: i.totalMs, - contractedHoursPerMonth: i.contractedHoursPerMonth ?? null, - })), - } - }, + handler: hoursByClientHandler, }) // Internal variant used by scheduled jobs: skips viewer scoping and aggregates for the whole tenant +export async function hoursByClientInternalHandler( + ctx: QueryCtx, + { tenantId, range }: { tenantId: string; range?: string } +) { + const tickets = await fetchTickets(ctx, 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 + + type Acc = { + companyId: Id<"companies"> + name: string + isAvulso: boolean + internalMs: number + externalMs: number + totalMs: number + contractedHoursPerMonth?: number | null + } + const map = new Map() + + for (const t of tickets) { + if (t.updatedAt < startMs || t.updatedAt >= endMs) continue + const companyId = t.companyId ?? null + if (!companyId) continue + + let acc = map.get(companyId) + if (!acc) { + const company = await ctx.db.get(companyId) + acc = { + companyId, + name: company?.name ?? "Sem empresa", + isAvulso: Boolean(company?.isAvulso ?? false), + internalMs: 0, + externalMs: 0, + totalMs: 0, + contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null, + } + map.set(companyId, acc) + } + const internal = t.internalWorkedMs ?? 0 + const external = t.externalWorkedMs ?? 0 + acc.internalMs += internal + acc.externalMs += external + acc.totalMs += internal + external + } + + const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs) + return { + rangeDays: days, + items: items.map((i) => ({ + companyId: i.companyId, + name: i.name, + isAvulso: i.isAvulso, + internalMs: i.internalMs, + externalMs: i.externalMs, + totalMs: i.totalMs, + contractedHoursPerMonth: i.contractedHoursPerMonth ?? null, + })), + } +} + export const hoursByClientInternal = query({ args: { tenantId: v.string(), range: v.optional(v.string()) }, - handler: async (ctx, { tenantId, range }) => { - const tickets = await fetchTickets(ctx, 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 - - type Acc = { - companyId: Id<"companies"> - name: string - isAvulso: boolean - internalMs: number - externalMs: number - totalMs: number - contractedHoursPerMonth?: number | null - } - const map = new Map() - - for (const t of tickets) { - if (t.updatedAt < startMs || t.updatedAt >= endMs) continue - const companyId = t.companyId ?? null - if (!companyId) continue - - let acc = map.get(companyId) - if (!acc) { - const company = await ctx.db.get(companyId) - acc = { - companyId, - name: company?.name ?? "Sem empresa", - isAvulso: Boolean(company?.isAvulso ?? false), - internalMs: 0, - externalMs: 0, - totalMs: 0, - contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null, - } - map.set(companyId, acc) - } - const internal = t.internalWorkedMs ?? 0 - const external = t.externalWorkedMs ?? 0 - acc.internalMs += internal - acc.externalMs += external - acc.totalMs += internal + external - } - - const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs) - return { - rangeDays: days, - items: items.map((i) => ({ - companyId: i.companyId, - name: i.name, - isAvulso: i.isAvulso, - internalMs: i.internalMs, - externalMs: i.externalMs, - totalMs: i.totalMs, - contractedHoursPerMonth: i.contractedHoursPerMonth ?? null, - })), - } - }, + handler: hoursByClientInternalHandler, }) diff --git a/docs/admin/admin-inventory-ui.md b/docs/admin/admin-inventory-ui.md index 7cebada..5f6757e 100644 --- a/docs/admin/admin-inventory-ui.md +++ b/docs/admin/admin-inventory-ui.md @@ -24,6 +24,20 @@ A página Admin > Máquinas agora exibe um inventário detalhado e pesquisável ## Exportação - Exportar CSV de softwares ou serviços diretamente da seção detalhada (quando disponíveis). +- Exportar planilha XLSX completa (`/admin/machines/:id/inventory.xlsx`). A partir de 31/10/2025 a planilha contém: + - **Resumo**: data de geração, filtros aplicados, contagem por status e total de acessos remotos/alertas. + - **Inventário**: colunas principais exibidas na UI (status, persona, hardware, token, build/licença do SO, domínio, colaborador, Fleet, etc.). + - **Vínculos**: usuários associados à máquina. + - **Softwares**: lista deduplicada (nome + versão + origem/publisher). A coluna “Softwares instalados” no inventário bate com o total desta aba. + - **Partições**: nome/mount/FS/capacidade/livre, convertendo unidades (ex.: 447 GB → bytes). + - **Discos físicos**: modelo, tamanho, interface, tipo e serial de cada drive. + - **Rede**: interfaces com MAC/IP de todas as fontes (agente, Fleet). + - **Acessos remotos**: TeamViewer/AnyDesk/etc. com notas, URL, última verificação e metadados brutos. + - **Serviços**: serviços coletados (Windows/Linux) com nome, display name e status. + - **Alertas**: postura recente (tipo, mensagem, severidade, criado em). + - **Métricas**: CPU/Memória/Disco/GPU com timestamp coletado. + - **Labels**: tags aplicadas à máquina. + - **Sistema**: visão categorizada (Sistema, Dispositivo, Hardware, Acesso, Token, Fleet) contendo build, licença, domínio, fabricante, serial, colaborador, contagem de acessos, etc. ## Notas - Os dados vêm de duas fontes: diff --git a/machine-inventory-plc-est-025 (2).xlsx b/machine-inventory-plc-est-025 (2).xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6b2b93352f99cc89fe9aa6f0378e35b0654ecf9a GIT binary patch literal 301511 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.0.0'} @@ -1046,56 +1019,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.0.0': - resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} + '@next/env@16.0.1': + resolution: {integrity: sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==} - '@next/eslint-plugin-next@16.0.0': - resolution: {integrity: sha512-IB7RzmmtrPOrpAgEBR1PIQPD0yea5lggh5cq54m51jHjjljU80Ia+czfxJYMlSDl1DPvpzb8S9TalCc0VMo9Hw==} + '@next/eslint-plugin-next@16.0.1': + resolution: {integrity: sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==} - '@next/swc-darwin-arm64@16.0.0': - resolution: {integrity: sha512-/CntqDCnk5w2qIwMiF0a9r6+9qunZzFmU0cBX4T82LOflE72zzH6gnOjCwUXYKOBlQi8OpP/rMj8cBIr18x4TA==} + '@next/swc-darwin-arm64@16.0.1': + resolution: {integrity: sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.0.0': - resolution: {integrity: sha512-hB4GZnJGKa8m4efvTGNyii6qs76vTNl+3dKHTCAUaksN6KjYy4iEO3Q5ira405NW2PKb3EcqWiRaL9DrYJfMHg==} + '@next/swc-darwin-x64@16.0.1': + resolution: {integrity: sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.0.0': - resolution: {integrity: sha512-E2IHMdE+C1k+nUgndM13/BY/iJY9KGCphCftMh7SXWcaQqExq/pJU/1Hgn8n/tFwSoLoYC/yUghOv97tAsIxqg==} + '@next/swc-linux-arm64-gnu@16.0.1': + resolution: {integrity: sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.0': - resolution: {integrity: sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg==} + '@next/swc-linux-arm64-musl@16.0.1': + resolution: {integrity: sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.0': - resolution: {integrity: sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg==} + '@next/swc-linux-x64-gnu@16.0.1': + resolution: {integrity: sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.0': - resolution: {integrity: sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg==} + '@next/swc-linux-x64-musl@16.0.1': + resolution: {integrity: sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.0.0': - resolution: {integrity: sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA==} + '@next/swc-win32-arm64-msvc@16.0.1': + resolution: {integrity: sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.0': - resolution: {integrity: sha512-O1cJbT+lZp+cTjYyZGiDwsOjO3UHHzSqobkPNipdlnnuPb1swfcuY6r3p8dsKU4hAIEO4cO67ZCfVVH/M1ETXA==} + '@next/swc-win32-x64-msvc@16.0.1': + resolution: {integrity: sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1681,297 +1654,6 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/autocomplete@3.0.0-rc.3': - resolution: {integrity: sha512-vemf7h3hvIDk3MxiiPryysfYgJDg8R72X46dRIeg0+cXKYxjPYou64/DTucSV2z5J6RC5JalINu0jIDaLhEILw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/breadcrumbs@3.5.29': - resolution: {integrity: sha512-rKS0dryllaZJqrr3f/EAf2liz8CBEfmL5XACj+Z1TAig6GIYe1QuA3BtkX0cV9OkMugXdX8e3cbA7nD10ORRqg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/button@3.14.2': - resolution: {integrity: sha512-VbLIA+Kd6f/MDjd+TJBUg2+vNDw66pnvsj2E4RLomjI9dfBuN7d+Yo2UnsqKVyhePjCUZ6xxa2yDuD63IOSIYA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/calendar@3.9.2': - resolution: {integrity: sha512-uSLxLgOPRnEU4Jg59lAhUVA+uDx/55NBg4lpfsP2ynazyiJ5LCXmYceJi+VuOqMml7d9W0dB87OldOeLdIxYVA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/checkbox@3.16.2': - resolution: {integrity: sha512-29Mj9ZqXioJ0bcMnNGooHztnTau5pikZqX3qCRj5bYR3by/ZFFavYoMroh9F7s/MbFm/tsKX+Sf02lYFEdXRjA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/collections@3.0.0': - resolution: {integrity: sha512-vCFztpsl1AYjQn3lH7CwzYiiRAGfnm7+EXaXIt7yS4O6YC8C3FfOBf3jdxcFjE5u8CEfiL4X+4ABkfio10nneg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/color@3.1.2': - resolution: {integrity: sha512-jCC+Q7rAQGLQBkHjkPAeDuGYuMbc4neifjlNRiyZ9as1z4gg63H8MteoWYYk6K4vCKKxSixgt8MfI29XWMOWPQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/combobox@3.14.0': - resolution: {integrity: sha512-z4ro0Hma//p4nL2IJx5iUa7NwxeXbzSoZ0se5uTYjG1rUUMszg+wqQh/AQoL+eiULn7rs18JY9wwNbVIkRNKWA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/datepicker@3.15.2': - resolution: {integrity: sha512-th078hyNqPf4P2K10su/y32zPDjs3lOYVdHvsL9/+5K1dnTvLHCK5vgUyLuyn8FchhF7cmHV49D+LZVv65PEpQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dialog@3.5.31': - resolution: {integrity: sha512-inxQMyrzX0UBW9Mhraq0nZ4HjHdygQvllzloT1E/RlDd61lr3RbmJR6pLsrbKOTtSvDIBJpCso1xEdHCFNmA0Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/disclosure@3.1.0': - resolution: {integrity: sha512-5996BeBpnj+yKXYysz+UuhFQxGFPvaZZ3zNBd052wz/i+TVFVGSqqYJ6cwZyO1AfBR8zOT0ZIiK4EC3ETwSvtQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dnd@3.11.3': - resolution: {integrity: sha512-MyTziciik1Owz3rqDghu0K3ZtTFvmj/R2ZsLDwbU9N4hKqGX/BKnrI8SytTn8RDqVv5LmA/GhApLngiupTAsXw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/focus@3.21.2': - resolution: {integrity: sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/form@3.1.2': - resolution: {integrity: sha512-R3i7L7Ci61PqZQvOrnL9xJeWEbh28UkTVgkj72EvBBn39y4h7ReH++0stv7rRs8p5ozETSKezBbGfu4UsBewWw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/grid@3.14.5': - resolution: {integrity: sha512-XHw6rgjlTqc85e3zjsWo3U0EVwjN5MOYtrolCKc/lc2ItNdcY3OlMhpsU9+6jHwg/U3VCSWkGvwAz9hg7krd8Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/gridlist@3.14.1': - resolution: {integrity: sha512-keS03Am07aOn7RuNaRsMOyh0jscyhDn95asCVy4lxhl9A9TFk1Jw0o2L6q6cWRj1gFiKeacj/otG5H8ZKQQ2Wg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/i18n@3.12.13': - resolution: {integrity: sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/interactions@3.25.6': - resolution: {integrity: sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/label@3.7.22': - resolution: {integrity: sha512-jLquJeA5ZNqDT64UpTc9XJ7kQYltUlNcgxZ37/v4mHe0UZ7QohCKdKQhXHONb0h2jjNUpp2HOZI8J9++jOpzxA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/landmark@3.0.7': - resolution: {integrity: sha512-t8c610b8hPLS6Vwv+rbuSyljZosI1s5+Tosfa0Fk4q7d+Ex6Yj7hLfUFy59GxZAufhUYfGX396fT0gPqAbU1tg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/link@3.8.6': - resolution: {integrity: sha512-7F7UDJnwbU9IjfoAdl6f3Hho5/WB7rwcydUOjUux0p7YVWh/fTjIFjfAGyIir7MJhPapun1D0t97QQ3+8jXVcg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/listbox@3.15.0': - resolution: {integrity: sha512-Ub1Wu79R9sgxM7h4HeEdjOgOKDHwduvYcnDqsSddGXgpkL8ADjsy2YUQ0hHY5VnzA4BxK36bLp4mzSna8Qvj1w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/live-announcer@3.4.4': - resolution: {integrity: sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==} - - '@react-aria/menu@3.19.3': - resolution: {integrity: sha512-52fh8y8b2776R2VrfZPpUBJYC9oTP7XDy+zZuZTxPEd7Ywk0JNUl5F92y6ru22yPkS13sdhrNM/Op+V/KulmAg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/meter@3.4.27': - resolution: {integrity: sha512-andOOdJkgRJF9vBi5VWRmFodK+GT+5X1lLeNUmb4qOX8/MVfX/RbK72LDeIhd7xC7rSCFHj3WvZ198rK4q0k3w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/numberfield@3.12.2': - resolution: {integrity: sha512-M2b+z0HIXiXpGAWOQkO2kpIjaLNUXJ5Q3/GMa3Fkr+B1piFX0VuOynYrtddKVrmXCe+r5t+XcGb0KS29uqv7nQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/overlays@3.30.0': - resolution: {integrity: sha512-UpjqSjYZx5FAhceWCRVsW6fX1sEwya1fQ/TKkL53FAlLFR8QKuoKqFlmiL43YUFTcGK3UdEOy3cWTleLQwdSmQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/progress@3.4.27': - resolution: {integrity: sha512-0OA1shs1575g1zmO8+rWozdbTnxThFFhOfuoL1m7UV5Dley6FHpueoKB1ECv7B+Qm4dQt6DoEqLg7wsbbQDhmg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/radio@3.12.2': - resolution: {integrity: sha512-I11f6I90neCh56rT/6ieAs3XyDKvEfbj/QmbU5cX3p+SJpRRPN0vxQi5D1hkh0uxDpeClxygSr31NmZsd4sqfg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/searchfield@3.8.9': - resolution: {integrity: sha512-Yt2pj8Wb5/XsUr2T0DQqFv+DlFpzzWIWnNr9cJATUcWV/xw6ok7YFEg9+7EHtBmsCQxFFJtock1QfZzBw6qLtQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/select@3.17.0': - resolution: {integrity: sha512-q5ZuyAn5jSOeI0Ys99951TaGcF4O7u1SSBVxPMwVVXOU8ZhToCNx+WG3n/JDYHEjqdo7sbsVRaPA7LkBzBGf5w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/selection@3.26.0': - resolution: {integrity: sha512-ZBH3EfWZ+RfhTj01dH8L17uT7iNbXWS8u77/fUpHgtrm0pwNVhx0TYVnLU1YpazQ/3WVpvWhmBB8sWwD1FlD/g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/separator@3.4.13': - resolution: {integrity: sha512-0NlcrdBfQbcjWEXdHl3+uSY1272n2ljT1gWL2RIf6aQsQWTZ0gz0rTgRHy0MTXN+y+tICItUERJT4vmTLtIzVg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/slider@3.8.2': - resolution: {integrity: sha512-6KyUGaVzRE4xAz1LKHbNh1q5wzxe58pdTHFSnxNe6nk1SCoHw7NfI4h2s2m6LgJ0megFxsT0Ir8aHaFyyxmbgg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/spinbutton@3.6.19': - resolution: {integrity: sha512-xOIXegDpts9t3RSHdIN0iYQpdts0FZ3LbpYJIYVvdEHo9OpDS+ElnDzCGtwZLguvZlwc5s1LAKuKopDUsAEMkw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.10': - resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/switch@3.7.8': - resolution: {integrity: sha512-AfsUq1/YiuoprhcBUD9vDPyWaigAwctQNW1fMb8dROL+i/12B+Zekj8Ml+jbU69/kIVtfL0Jl7/0Bo9KK3X0xQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/table@3.17.8': - resolution: {integrity: sha512-bXiZoxTMbsqUJsYDhHPzKc3jw0HFJ/xMsJ49a0f7mp5r9zACxNLeIU0wJ4Uvx37dnYOHKzGliG+rj5l4sph7MA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tabs@3.10.8': - resolution: {integrity: sha512-sPPJyTyoAqsBh76JinBAxStOcbjZvyWFYKpJ9Uqw+XT0ObshAPPFSGeh8DiQemPs02RwJdrfARPMhyqiX8t59A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tag@3.7.2': - resolution: {integrity: sha512-JV679P5r4DftbqyNBRt7Nw9mP7dxaKPfikjyQuvUoEOa06wBLbM/hU9RJUPRvqK+Un6lgBDAmXD9NNf4N2xpdw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/textfield@3.18.2': - resolution: {integrity: sha512-G+lM8VYSor6g9Yptc6hLZ6BF+0cq0pYol1z6wdQUQgJN8tg4HPtzq75lsZtlCSIznL3amgRAxJtd0dUrsAnvaQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toast@3.0.8': - resolution: {integrity: sha512-rfJIms6AkMyQ7ZgKrMZgGfPwGcB/t1JoEwbc1PAmXcAvFI/hzF6YF7ZFDXiq38ucFsP9PnHmbXIzM9w4ccl18A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toggle@3.12.2': - resolution: {integrity: sha512-g25XLYqJuJpt0/YoYz2Rab8ax+hBfbssllcEFh0v0jiwfk2gwTWfRU9KAZUvxIqbV8Nm8EBmrYychDpDcvW1kw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toolbar@3.0.0-beta.21': - resolution: {integrity: sha512-yRCk/GD8g+BhdDgxd3I0a0c8Ni4Wyo6ERzfSoBkPkwQ4X2E2nkopmraM9D0fXw4UcIr4bnmvADzkHXtBN0XrBg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tooltip@3.8.8': - resolution: {integrity: sha512-CmHUqtXtFWmG4AHMEr9hIVex+oscK6xcM2V47gq9ijNInxe3M6UBu/dBdkgGP/jYv9N7tzCAjTR8nNIHQXwvWw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tree@3.1.4': - resolution: {integrity: sha512-6pbFeN0dAsCOrFGUKU39CNjft20zCAjLfMqfkRWisL+JkUHI2nq6odUJF5jJTsU1C+1951+3oFOmVxPX+K+akQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/utils@3.31.0': - resolution: {integrity: sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/virtualizer@4.1.10': - resolution: {integrity: sha512-s0xOFh602ybTWuDrV/i6fV7Pz7vYghsY7F/RpYL/5IX9qCZ5C1FWFePpVktQAZghnd3ljH8hS8DULPeDfVLCrg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/visually-hidden@3.8.28': - resolution: {integrity: sha512-KRRjbVVob2CeBidF24dzufMxBveEUtUu7IM+hpdZKB+gxVROoh4XRLPv9SFmaH89Z7D9To3QoykVZoWD0lan6Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-pdf/fns@3.1.2': resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==} @@ -2015,161 +1697,6 @@ packages: '@react-pdf/types@2.9.1': resolution: {integrity: sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==} - '@react-stately/autocomplete@3.0.0-beta.3': - resolution: {integrity: sha512-YfP/TrvkOCp6j7oqpZxJSvmSeXn+XtbKSOiBOuo+m2zCIhW2ncThmDB9uAUOkpmikDv/LkGKni40RQE8USdGdA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/calendar@3.9.0': - resolution: {integrity: sha512-U5Nf2kx9gDhJRxdDUm5gjfyUlt/uUfOvM1vDW2UA62cA6+2k2cavMLc2wNlXOb/twFtl6p0joYKHG7T4xnEFkg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/checkbox@3.7.2': - resolution: {integrity: sha512-j1ycUVz5JmqhaL6mDZgDNZqBilOB8PBW096sDPFaTtuYreDx2HOd1igxiIvwlvPESZwsJP7FVM3mYnaoXtpKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/collections@3.12.8': - resolution: {integrity: sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/color@3.9.2': - resolution: {integrity: sha512-F+6Do8W3yu/4n7MpzZtbXwVukcLTFYYDIUtpoR+Jl52UmAr9Hf1CQgkyTI2azv1ZMzj1mVrTBhpBL0q27kFZig==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/combobox@3.12.0': - resolution: {integrity: sha512-A6q9R/7cEa/qoQsBkdslXWvD7ztNLLQ9AhBhVN9QvzrmrH5B4ymUwcTU8lWl22ykH7RRwfonLeLXJL4C+/L2oQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/data@3.14.1': - resolution: {integrity: sha512-lDNc4gZ6kVZcrABeeQZPTTnP+1ykNylSvFzAC/Hq1fs8+s54xLRvoENWIyG+yK19N9TIGEoA0AOFG8PoAun43g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/datepicker@3.15.2': - resolution: {integrity: sha512-S5GL+W37chvV8knv9v0JRv0L6hKo732qqabCCHXzOpYxkLIkV4f/y3cHdEzFWzpZ0O0Gkg7WgeYo160xOdBKYg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/disclosure@3.0.8': - resolution: {integrity: sha512-/Ce/Z76y85eSBZiemfU/uEyXkBBa1RdfLRaKD13rnfUV7/nS3ae1VtNlsXgmwQjWv2pmAiSuEKYMbZfVL7q/lQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/dnd@3.7.1': - resolution: {integrity: sha512-O1JBJ4HI1rVNKuoa5NXiC5FCrCEkr9KVBoKNlTZU8/cnQselhbEsUfMglAakO2EuwIaM1tIXoNF5J/N5P+6lTA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/flags@3.1.2': - resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - - '@react-stately/form@3.2.2': - resolution: {integrity: sha512-soAheOd7oaTO6eNs6LXnfn0tTqvOoe3zN9FvtIhhrErKz9XPc5sUmh3QWwR45+zKbitOi1HOjfA/gifKhZcfWw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/grid@3.11.6': - resolution: {integrity: sha512-vWPAkzpeTIsrurHfMubzMuqEw7vKzFhIJeEK5sEcLunyr1rlADwTzeWrHNbPMl66NAIAi70Dr1yNq+kahQyvMA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/layout@4.5.1': - resolution: {integrity: sha512-Zk92HM6a8KFdyPzslhLCOmrrsvJ28+vFBisgiKMwVhe96cWlax1m9i4ktmO43xaUpSZkn06DRD/2k0d1x+Uwjw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/list@3.13.1': - resolution: {integrity: sha512-eHaoauh21twbcl0kkwULhVJ+CzYcy1jUjMikNVMHOQdhr4WIBdExf7PmSgKHKqsSPhpGg6IpTCY2dUX3RycjDg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/menu@3.9.8': - resolution: {integrity: sha512-bo0NOhofnTHLESiYfsSSw6gyXiPVJJ0UlN2igUXtJk5PmyhWjFzUzTzcnd7B028OB0si9w3LIWM3stqz5271Eg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/numberfield@3.10.2': - resolution: {integrity: sha512-jlKVFYaH3RX5KvQ7a+SAMQuPccZCzxLkeYkBE64u1Zvi7YhJ8hkTMHG/fmZMbk1rHlseE2wfBdk0Rlya3MvoNQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/overlays@3.6.20': - resolution: {integrity: sha512-YAIe+uI8GUXX8F/0Pzr53YeC5c/bjqbzDFlV8NKfdlCPa6+Jp4B/IlYVjIooBj9+94QvbQdjylegvYWK/iPwlg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/radio@3.11.2': - resolution: {integrity: sha512-UM7L6AW+k8edhSBUEPZAqiWNRNadfOKK7BrCXyBiG79zTz0zPcXRR+N+gzkDn7EMSawDeyK1SHYUuoSltTactg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/searchfield@3.5.16': - resolution: {integrity: sha512-MRfqT1lZ24r94GuFNcGJXsfijZoWjSMySCT60T6NXtbOzVPuAF3K+pL70Rayq/EWLJjS2NPHND11VTs0VdcE0Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/select@3.8.0': - resolution: {integrity: sha512-A721nlt0DSCDit0wKvhcrXFTG5Vv1qkEVkeKvobmETZy6piKvwh0aaN8iQno5AFuZaj1iOZeNjZ/20TsDJR/4A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/selection@3.20.6': - resolution: {integrity: sha512-a0bjuP2pJYPKEiedz2Us1W1aSz0iHRuyeQEdBOyL6Z6VUa6hIMq9H60kvseir2T85cOa4QggizuRV7mcO6bU5w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/slider@3.7.2': - resolution: {integrity: sha512-EVBHUdUYwj++XqAEiQg2fGi8Reccznba0uyQ3gPejF0pAc390Q/J5aqiTEDfiCM7uJ6WHxTM6lcCqHQBISk2dQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/table@3.15.1': - resolution: {integrity: sha512-MhMAgE/LgAzHcAn1P3p/nQErzJ6DiixSJ1AOt2JlnAKEb5YJg4ATKWCb2IjBLwywt9ZCzfm3KMUzkctZqAoxwA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tabs@3.8.6': - resolution: {integrity: sha512-9RYxmgjVIxUpIsGKPIF7uRoHWOEz8muwaYiStCVeyiYBPmarvZoIYtTXcwSMN/vEs7heVN5uGCL6/bfdY4+WiA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/toast@3.1.2': - resolution: {integrity: sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/toggle@3.9.2': - resolution: {integrity: sha512-dOxs9wrVXHUmA7lc8l+N9NbTJMAaXcYsnNGsMwfXIXQ3rdq+IjWGNYJ52UmNQyRYFcg0jrzRrU16TyGbNjOdNQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tooltip@3.5.8': - resolution: {integrity: sha512-gkcUx2ROhCiGNAYd2BaTejakXUUNLPnnoJ5+V/mN480pN+OrO8/2V9pqb/IQmpqxLsso93zkM3A4wFHHLBBmPQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tree@3.9.3': - resolution: {integrity: sha512-ZngG79nLFxE/GYmpwX6E/Rma2MMkzdoJPRI3iWk3dgqnGMMzpPnUp/cvjDsU3UHF7xDVusC5BT6pjWN0uxCIFQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/utils@3.10.8': - resolution: {integrity: sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/virtualizer@4.4.4': - resolution: {integrity: sha512-ri8giqXSZOrznZDCCOE4U36wSkOhy+hrFK7yo/YVcpxTqqp3d3eisfKMqbDsgqBW+XTHycTU/xeAf0u9NqrfpQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-three/fiber@9.3.0': resolution: {integrity: sha512-myPe3YL/C8+Eq939/4qIVEPBW/uxV0iiUbmjfwrs9sGKYDG8ib8Dz3Okq7BQt8P+0k4igedONbjXMQy84aDFmQ==} peerDependencies: @@ -2195,146 +1722,6 @@ packages: react-native: optional: true - '@react-types/autocomplete@3.0.0-alpha.35': - resolution: {integrity: sha512-Wv5eU4WixfJ4M+fqvJUQqliWPbw7/VldRlgoJhqAlPwlNyLlHYwv5tlA64AySDXHGcSMIbzcS38LaHm44wt0AQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/breadcrumbs@3.7.17': - resolution: {integrity: sha512-IhvVTcfli5o/UDlGACXxjlor2afGlMQA8pNR3faH0bBUay1Fmm3IWktVw9Xwmk+KraV2RTAg9e+E6p8DOQZfiw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/button@3.14.1': - resolution: {integrity: sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/calendar@3.8.0': - resolution: {integrity: sha512-ZDZgfZgbz1ydWOFs1mH7QFfX3ioJrmb3Y/lkoubQE0HWXLZzyYNvhhKyFJRS1QJ40IofLSBHriwbQb/tsUnGlw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/checkbox@3.10.2': - resolution: {integrity: sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/color@3.1.2': - resolution: {integrity: sha512-NP0TAY3j4tlMztOp/bBfMlPwC9AQKTjSiTFmc2oQNkx5M4sl3QpPqFPosdt7jZ8M4nItvfCWZrlZGjST4SB83A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/combobox@3.13.9': - resolution: {integrity: sha512-G6GmLbzVkLW6VScxPAr/RtliEyPhBClfYaIllK1IZv+Z42SVnOpKzhnoe79BpmiFqy1AaC3+LjZX783mrsHCwA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/datepicker@3.13.2': - resolution: {integrity: sha512-+M6UZxJnejYY8kz0spbY/hP08QJ5rsZ3aNarRQQHc48xV2oelFLX5MhAqizfLEsvyfb0JYrhWoh4z1xZtAmYCg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/dialog@3.5.22': - resolution: {integrity: sha512-smSvzOcqKE196rWk0oqJDnz+ox5JM5+OT0PmmJXiUD4q7P5g32O6W5Bg7hMIFUI9clBtngo8kLaX2iMg+GqAzg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/form@3.7.16': - resolution: {integrity: sha512-Sb7KJoWEaQ/e4XIY+xRbjKvbP1luome98ZXevpD+zVSyGjEcfIroebizP6K1yMHCWP/043xH6GUkgEqWPoVGjg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/grid@3.3.6': - resolution: {integrity: sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/link@3.6.5': - resolution: {integrity: sha512-+I2s3XWBEvLrzts0GnNeA84mUkwo+a7kLUWoaJkW0TOBDG7my95HFYxF9WnqKye7NgpOkCqz4s3oW96xPdIniQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/listbox@3.7.4': - resolution: {integrity: sha512-p4YEpTl/VQGrqVE8GIfqTS5LkT5jtjDTbVeZgrkPnX/fiPhsfbTPiZ6g0FNap4+aOGJFGEEZUv2q4vx+rCORww==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/menu@3.10.5': - resolution: {integrity: sha512-HBTrKll2hm0VKJNM4ubIv1L9MNo8JuOnm2G3M+wXvb6EYIyDNxxJkhjsqsGpUXJdAOSkacHBDcNh2HsZABNX4A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/meter@3.4.13': - resolution: {integrity: sha512-EiarfbpHcvmeyXvXcr6XLaHkNHuGc4g7fBVEiDPwssFJKKfbUzqnnknDxPjyspqUVRcXC08CokS98J1jYobqDg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/numberfield@3.8.15': - resolution: {integrity: sha512-97r92D23GKCOjGIGMeW9nt+/KlfM3GeWH39Czcmd2/D5y3k6z4j0avbsfx2OttCtJszrnENjw3GraYGYI2KosQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/overlays@3.9.2': - resolution: {integrity: sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/progress@3.5.16': - resolution: {integrity: sha512-I9tSdCFfvQ7gHJtm90VAKgwdTWXQgVNvLRStEc0z9h+bXBxdvZb+QuiRPERChwFQ9VkK4p4rDqaFo69nDqWkpw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/radio@3.9.2': - resolution: {integrity: sha512-3UcJXu37JrTkRyP4GJPDBU7NmDTInrEdOe+bVzA1j4EegzdkJmLBkLg5cLDAbpiEHB+xIsvbJdx6dxeMuc+H3g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/searchfield@3.6.6': - resolution: {integrity: sha512-cl3itr/fk7wbIQc2Gz5Ie8aVeUmPjVX/mRGS5/EXlmzycAKNYTvqf2mlxwObLndtLISmt7IgNjRRhbUUDI8Ang==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/select@3.11.0': - resolution: {integrity: sha512-SzIsMFVPCbXE1Z1TLfpdfiwJ1xnIkcL1/CjGilmUKkNk5uT7rYX1xCJqWCjXI0vAU1xM4Qn+T3n8de4fw6HRBg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/shared@3.32.1': - resolution: {integrity: sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/slider@3.8.2': - resolution: {integrity: sha512-MQYZP76OEOYe7/yA2To+Dl0LNb0cKKnvh5JtvNvDnAvEprn1RuLiay8Oi/rTtXmc2KmBa4VdTcsXsmkbbkeN2Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/switch@3.5.15': - resolution: {integrity: sha512-r/ouGWQmIeHyYSP1e5luET+oiR7N7cLrAlWsrAfYRWHxqXOSNQloQnZJ3PLHrKFT02fsrQhx2rHaK2LfKeyN3A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/table@3.13.4': - resolution: {integrity: sha512-I/DYiZQl6aNbMmjk90J9SOhkzVDZvyA3Vn3wMWCiajkMNjvubFhTfda5DDf2SgFP5l0Yh6TGGH5XumRv9LqL5Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/tabs@3.3.19': - resolution: {integrity: sha512-fE+qI43yR5pAMpeqPxGqQq9jDHXEPqXskuxNHERMW0PYMdPyem2Cw6goc5F4qeZO3Hf6uPZgHkvJz2OAq7TbBw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/textfield@3.12.6': - resolution: {integrity: sha512-hpEVKE+M3uUkTjw2WrX1NrH/B3rqDJFUa+ViNK2eVranLY4ZwFqbqaYXSzHupOF3ecSjJJv2C103JrwFvx6TPQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/tooltip@3.4.21': - resolution: {integrity: sha512-ugGHOZU6WbOdeTdbjnaEc+Ms7/WhsUCg+T3PCOIeOT9FG02Ce189yJ/+hd7oqL/tVwIhEMYJIqSCgSELFox+QA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -3814,8 +3201,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.0.0: - resolution: {integrity: sha512-DWKT1YAO9ex2rK0/EeiPpKU++ghTiG59z6m08/ReLRECOYIaEv17maSCYT8zmFQLwIrY5lhJ+iaJPQdT4sJd4g==} + eslint-config-next@16.0.1: + resolution: {integrity: sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -4210,9 +3597,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - intl-messageformat@10.7.18: - resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -4606,8 +3990,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.0.0: - resolution: {integrity: sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg==} + next@16.0.1: + resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -4915,10 +4299,6 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-aria-components@1.13.0: {} - - react-aria@3.44.0: {} - react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -4972,11 +4352,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-stately@3.42.0: - resolution: {integrity: sha512-lYt2o1dd6dK8Bb4GRh08RG/2u64bSA1cqtRqtw4jEMgxC7Q17RFcIumBbChErndSdLzafEG/UBwV6shOfig6yw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -6186,32 +5561,6 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@formatjs/ecma402-abstract@2.3.6': - dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 - decimal.js: 10.6.0 - tslib: 2.8.1 - - '@formatjs/fast-memoize@2.2.7': - dependencies: - tslib: 2.8.1 - - '@formatjs/icu-messageformat-parser@2.11.4': - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/icu-skeleton-parser': 1.8.16 - tslib: 2.8.1 - - '@formatjs/icu-skeleton-parser@1.8.16': - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - '@hexagon/base64@1.1.28': {} '@hookform/resolvers@3.10.0(react-hook-form@7.64.0(react@19.2.0))': @@ -6318,23 +5667,6 @@ snapshots: '@img/sharp-win32-x64@0.34.4': optional: true - '@internationalized/date@3.10.0': - dependencies: - '@swc/helpers': 0.5.15 - - '@internationalized/message@3.1.8': - dependencies: - '@swc/helpers': 0.5.15 - intl-messageformat: 10.7.18 - - '@internationalized/number@3.6.5': - dependencies: - '@swc/helpers': 0.5.15 - - '@internationalized/string@3.2.7': - dependencies: - '@swc/helpers': 0.5.15 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -6367,34 +5699,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.0.0': {} + '@next/env@16.0.1': {} - '@next/eslint-plugin-next@16.0.0': + '@next/eslint-plugin-next@16.0.1': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.0.0': + '@next/swc-darwin-arm64@16.0.1': optional: true - '@next/swc-darwin-x64@16.0.0': + '@next/swc-darwin-x64@16.0.1': optional: true - '@next/swc-linux-arm64-gnu@16.0.0': + '@next/swc-linux-arm64-gnu@16.0.1': optional: true - '@next/swc-linux-arm64-musl@16.0.0': + '@next/swc-linux-arm64-musl@16.0.1': optional: true - '@next/swc-linux-x64-gnu@16.0.0': + '@next/swc-linux-x64-gnu@16.0.1': optional: true - '@next/swc-linux-x64-musl@16.0.0': + '@next/swc-linux-x64-musl@16.0.1': optional: true - '@next/swc-win32-arm64-msvc@16.0.0': + '@next/swc-win32-arm64-msvc@16.0.1': optional: true - '@next/swc-win32-x64-msvc@16.0.0': + '@next/swc-win32-x64-msvc@16.0.1': optional: true '@noble/ciphers@2.0.1': {} @@ -7164,638 +6496,6 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-aria/autocomplete@3.0.0-rc.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/combobox': 3.14.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/listbox': 3.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/searchfield': 3.8.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/autocomplete': 3.0.0-beta.3(react@19.2.0) - '@react-stately/combobox': 3.12.0(react@19.2.0) - '@react-types/autocomplete': 3.0.0-alpha.35(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/breadcrumbs@3.5.29(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/link': 3.8.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/breadcrumbs': 3.7.17(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/button@3.14.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/toolbar': 3.0.0-beta.21(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/toggle': 3.9.2(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/calendar@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/calendar': 3.9.0(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/calendar': 3.8.0(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/checkbox@3.16.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/form': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/toggle': 3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/checkbox': 3.7.2(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/toggle': 3.9.2(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/collections@3.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - - '@react-aria/color@3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/numberfield': 3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/slider': 3.8.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/spinbutton': 3.6.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/visually-hidden': 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/color': 3.9.2(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-types/color': 3.1.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/combobox@3.14.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/listbox': 3.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/menu': 3.19.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/combobox': 3.12.0(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/combobox': 3.13.9(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/datepicker@3.15.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/form': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/spinbutton': 3.6.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/datepicker': 3.15.2(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/calendar': 3.8.0(react@19.2.0) - '@react-types/datepicker': 3.13.2(react@19.2.0) - '@react-types/dialog': 3.5.22(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/dialog@3.5.31(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/dialog': 3.5.22(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/disclosure@3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/disclosure': 3.0.8(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/dnd@3.11.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/dnd': 3.7.1(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/focus@3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/form@3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/grid@3.14.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/grid': 3.11.6(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/gridlist@3.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/grid': 3.14.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-stately/tree': 3.9.3(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/i18n@3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@internationalized/message': 3.1.8 - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/interactions@3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/label@3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/landmark@3.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - - '@react-aria/link@3.8.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/link': 3.6.5(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/listbox@3.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-types/listbox': 3.7.4(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/live-announcer@3.4.4': - dependencies: - '@swc/helpers': 0.5.15 - - '@react-aria/menu@3.19.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/menu': 3.9.8(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/tree': 3.9.3(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/menu': 3.10.5(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/meter@3.4.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/progress': 3.4.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/meter': 3.4.13(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/numberfield@3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/spinbutton': 3.6.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/numberfield': 3.10.2(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/numberfield': 3.8.15(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/overlays@3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/visually-hidden': 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/overlays': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/progress@3.4.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/progress': 3.5.16(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/radio@3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/form': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/radio': 3.11.2(react@19.2.0) - '@react-types/radio': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/searchfield@3.8.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/searchfield': 3.5.16(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/searchfield': 3.6.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/select@3.17.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/form': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/listbox': 3.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/menu': 3.19.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/visually-hidden': 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/select': 3.8.0(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/select': 3.11.0(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/selection@3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/separator@3.4.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/slider@3.8.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/slider': 3.7.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/slider': 3.8.2(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/spinbutton@3.6.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/ssr@3.9.10(react@19.2.0)': - dependencies: - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-aria/switch@3.7.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/toggle': 3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/toggle': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/switch': 3.5.15(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/table@3.17.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/grid': 3.14.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/visually-hidden': 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/flags': 3.1.2 - '@react-stately/table': 3.15.1(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/table': 3.13.4(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/tabs@3.10.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/tabs': 3.8.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/tabs': 3.3.19(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/tag@3.7.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/gridlist': 3.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/textfield@3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/form': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/textfield': 3.12.6(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/toast@3.0.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/landmark': 3.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/toast': 3.1.2(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/toggle@3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/toggle': 3.9.2(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/toolbar@3.0.0-beta.21(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/tooltip@3.8.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/tooltip': 3.5.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/tooltip': 3.4.21(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/tree@3.1.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/gridlist': 3.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/tree': 3.9.3(react@19.2.0) - '@react-types/button': 3.14.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/utils@3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/virtualizer@4.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/virtualizer': 4.4.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-aria/visually-hidden@3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - '@react-pdf/fns@3.1.2': {} '@react-pdf/font@4.0.3': @@ -7897,263 +6597,6 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.1 - '@react-stately/autocomplete@3.0.0-beta.3(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/calendar@3.9.0(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/calendar': 3.8.0(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/checkbox@3.7.2(react@19.2.0)': - dependencies: - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/collections@3.12.8(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/color@3.9.2(react@19.2.0)': - dependencies: - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/numberfield': 3.10.2(react@19.2.0) - '@react-stately/slider': 3.7.2(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/color': 3.1.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/combobox@3.12.0(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/combobox': 3.13.9(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/data@3.14.1(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/datepicker@3.15.2(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/datepicker': 3.13.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/disclosure@3.0.8(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/dnd@3.7.1(react@19.2.0)': - dependencies: - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/flags@3.1.2': - dependencies: - '@swc/helpers': 0.5.15 - - '@react-stately/form@3.2.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/grid@3.11.6(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/layout@4.5.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/table': 3.15.1(react@19.2.0) - '@react-stately/virtualizer': 4.4.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/table': 3.13.4(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - '@react-stately/list@3.13.1(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/menu@3.9.8(react@19.2.0)': - dependencies: - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-types/menu': 3.10.5(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/numberfield@3.10.2(react@19.2.0)': - dependencies: - '@internationalized/number': 3.6.5 - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/numberfield': 3.8.15(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/overlays@3.6.20(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/overlays': 3.9.2(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/radio@3.11.2(react@19.2.0)': - dependencies: - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/radio': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/searchfield@3.5.16(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/searchfield': 3.6.6(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/select@3.8.0(react@19.2.0)': - dependencies: - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/select': 3.11.0(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/selection@3.20.6(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/slider@3.7.2(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/slider': 3.8.2(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/table@3.15.1(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/flags': 3.1.2 - '@react-stately/grid': 3.11.6(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/table': 3.13.4(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/tabs@3.8.6(react@19.2.0)': - dependencies: - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/tabs': 3.3.19(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/toast@3.1.2(react@19.2.0)': - dependencies: - '@swc/helpers': 0.5.15 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - - '@react-stately/toggle@3.9.2(react@19.2.0)': - dependencies: - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/checkbox': 3.10.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/tooltip@3.5.8(react@19.2.0)': - dependencies: - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-types/tooltip': 3.4.21(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/tree@3.9.3(react@19.2.0)': - dependencies: - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/utils@3.10.8(react@19.2.0)': - dependencies: - '@swc/helpers': 0.5.15 - react: 19.2.0 - - '@react-stately/virtualizer@4.4.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@swc/helpers': 0.5.15 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - '@react-three/fiber@9.3.0(@types/react@18.3.26)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.180.0)': dependencies: '@babel/runtime': 7.28.4 @@ -8176,158 +6619,6 @@ snapshots: - '@types/react' - immer - '@react-types/autocomplete@3.0.0-alpha.35(react@19.2.0)': - dependencies: - '@react-types/combobox': 3.13.9(react@19.2.0) - '@react-types/searchfield': 3.6.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/breadcrumbs@3.7.17(react@19.2.0)': - dependencies: - '@react-types/link': 3.6.5(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/button@3.14.1(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/calendar@3.8.0(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/checkbox@3.10.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/color@3.1.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/slider': 3.8.2(react@19.2.0) - react: 19.2.0 - - '@react-types/combobox@3.13.9(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/datepicker@3.13.2(react@19.2.0)': - dependencies: - '@internationalized/date': 3.10.0 - '@react-types/calendar': 3.8.0(react@19.2.0) - '@react-types/overlays': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/dialog@3.5.22(react@19.2.0)': - dependencies: - '@react-types/overlays': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/form@3.7.16(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/grid@3.3.6(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/link@3.6.5(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/listbox@3.7.4(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/menu@3.10.5(react@19.2.0)': - dependencies: - '@react-types/overlays': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/meter@3.4.13(react@19.2.0)': - dependencies: - '@react-types/progress': 3.5.16(react@19.2.0) - react: 19.2.0 - - '@react-types/numberfield@3.8.15(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/overlays@3.9.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/progress@3.5.16(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/radio@3.9.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/searchfield@3.6.6(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/textfield': 3.12.6(react@19.2.0) - react: 19.2.0 - - '@react-types/select@3.11.0(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/shared@3.32.1(react@19.2.0)': - dependencies: - react: 19.2.0 - - '@react-types/slider@3.8.2(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/switch@3.5.15(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/table@3.13.4(react@19.2.0)': - dependencies: - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/tabs@3.3.19(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/textfield@3.12.6(react@19.2.0)': - dependencies: - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - - '@react-types/tooltip@3.4.21(react@19.2.0)': - dependencies: - '@react-types/overlays': 3.9.2(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -9323,7 +7614,7 @@ snapshots: baseline-browser-mapping@2.8.16: {} - better-auth@1.3.26(next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + better-auth@1.3.26(next@16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@better-auth/core': 1.3.26 '@better-auth/utils': 0.3.0 @@ -9339,7 +7630,7 @@ snapshots: nanostores: 1.0.1 zod: 4.1.11 optionalDependencies: - next: 16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -9866,9 +8157,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.0.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 16.0.0 + '@next/eslint-plugin-next': 16.0.1 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) @@ -10345,13 +8636,6 @@ snapshots: internmap@2.0.3: {} - intl-messageformat@10.7.18: - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.4 - tslib: 2.8.1 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -10721,9 +9005,9 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 16.0.0 + '@next/env': 16.0.1 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001747 postcss: 8.4.31 @@ -10731,14 +9015,14 @@ snapshots: react-dom: 19.2.0(react@19.2.0) styled-jsx: 5.1.6(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.0 - '@next/swc-darwin-x64': 16.0.0 - '@next/swc-linux-arm64-gnu': 16.0.0 - '@next/swc-linux-arm64-musl': 16.0.0 - '@next/swc-linux-x64-gnu': 16.0.0 - '@next/swc-linux-x64-musl': 16.0.0 - '@next/swc-win32-arm64-msvc': 16.0.0 - '@next/swc-win32-x64-msvc': 16.0.0 + '@next/swc-darwin-arm64': 16.0.1 + '@next/swc-darwin-x64': 16.0.1 + '@next/swc-linux-arm64-gnu': 16.0.1 + '@next/swc-linux-arm64-musl': 16.0.1 + '@next/swc-linux-x64-gnu': 16.0.1 + '@next/swc-linux-x64-musl': 16.0.1 + '@next/swc-win32-arm64-msvc': 16.0.1 + '@next/swc-win32-x64-msvc': 16.0.1 sharp: 0.34.4 transitivePeerDependencies: - '@babel/core' @@ -11093,87 +9377,6 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-aria-components@1.13.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - '@internationalized/date': 3.10.0 - '@internationalized/string': 3.2.7 - '@react-aria/autocomplete': 3.0.0-rc.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/collections': 3.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/dnd': 3.11.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/toolbar': 3.0.0-beta.21(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/virtualizer': 4.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/autocomplete': 3.0.0-beta.3(react@19.2.0) - '@react-stately/layout': 4.5.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/table': 3.15.1(react@19.2.0) - '@react-stately/utils': 3.10.8(react@19.2.0) - '@react-stately/virtualizer': 4.4.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/form': 3.7.16(react@19.2.0) - '@react-types/grid': 3.3.6(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - '@react-types/table': 3.13.4(react@19.2.0) - '@swc/helpers': 0.5.15 - client-only: 0.0.1 - react: 19.2.0 - react-aria: 3.44.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react-dom: 19.2.0(react@19.2.0) - react-stately: 3.42.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - - react-aria@3.44.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/breadcrumbs': 3.5.29(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/button': 3.14.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/calendar': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/checkbox': 3.16.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/color': 3.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/combobox': 3.14.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/datepicker': 3.15.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/dialog': 3.5.31(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/disclosure': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/dnd': 3.11.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/gridlist': 3.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/i18n': 3.12.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/interactions': 3.25.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/label': 3.7.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/landmark': 3.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/link': 3.8.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/listbox': 3.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/menu': 3.19.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/meter': 3.4.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/numberfield': 3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/overlays': 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/progress': 3.4.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/radio': 3.12.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/searchfield': 3.8.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/select': 3.17.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/selection': 3.26.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/separator': 3.4.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/slider': 3.8.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/ssr': 3.9.10(react@19.2.0) - '@react-aria/switch': 3.7.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/table': 3.17.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/tabs': 3.10.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/tag': 3.7.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/textfield': 3.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/toast': 3.0.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/tooltip': 3.8.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/tree': 3.1.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/utils': 3.31.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-aria/visually-hidden': 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -11221,36 +9424,6 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react-stately@3.42.0(react@19.2.0): - dependencies: - '@react-stately/calendar': 3.9.0(react@19.2.0) - '@react-stately/checkbox': 3.7.2(react@19.2.0) - '@react-stately/collections': 3.12.8(react@19.2.0) - '@react-stately/color': 3.9.2(react@19.2.0) - '@react-stately/combobox': 3.12.0(react@19.2.0) - '@react-stately/data': 3.14.1(react@19.2.0) - '@react-stately/datepicker': 3.15.2(react@19.2.0) - '@react-stately/disclosure': 3.0.8(react@19.2.0) - '@react-stately/dnd': 3.7.1(react@19.2.0) - '@react-stately/form': 3.2.2(react@19.2.0) - '@react-stately/list': 3.13.1(react@19.2.0) - '@react-stately/menu': 3.9.8(react@19.2.0) - '@react-stately/numberfield': 3.10.2(react@19.2.0) - '@react-stately/overlays': 3.6.20(react@19.2.0) - '@react-stately/radio': 3.11.2(react@19.2.0) - '@react-stately/searchfield': 3.5.16(react@19.2.0) - '@react-stately/select': 3.8.0(react@19.2.0) - '@react-stately/selection': 3.20.6(react@19.2.0) - '@react-stately/slider': 3.7.2(react@19.2.0) - '@react-stately/table': 3.15.1(react@19.2.0) - '@react-stately/tabs': 3.8.6(react@19.2.0) - '@react-stately/toast': 3.1.2(react@19.2.0) - '@react-stately/toggle': 3.9.2(react@19.2.0) - '@react-stately/tooltip': 3.5.8(react@19.2.0) - '@react-stately/tree': 3.9.3(react@19.2.0) - '@react-types/shared': 3.32.1(react@19.2.0) - react: 19.2.0 - react-style-singleton@2.2.3(@types/react@18.3.26)(react@19.2.0): dependencies: get-nonce: 1.0.1 @@ -11844,12 +10017,12 @@ snapshots: pako: 0.2.9 tiny-inflate: 1.0.3 - unicornstudio-react@1.4.31(next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + unicornstudio-react@1.4.31(next@16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - next: 16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) unrs-resolver@1.11.1: dependencies: diff --git a/src/app/admin/machines/[id]/tickets/page.tsx b/src/app/admin/machines/[id]/tickets/page.tsx new file mode 100644 index 0000000..3f2b49f --- /dev/null +++ b/src/app/admin/machines/[id]/tickets/page.tsx @@ -0,0 +1,26 @@ +import { AppShell } from "@/components/app-shell" +import { SiteHeader } from "@/components/site-header" +import { DEFAULT_TENANT_ID } from "@/lib/constants" +import { MachineBreadcrumbs } from "@/components/admin/machines/machine-breadcrumbs.client" +import { MachineTicketsHistoryClient } from "@/components/admin/machines/machine-tickets-history.client" + +export const runtime = "nodejs" +export const dynamic = "force-dynamic" + +export default async function AdminMachineTicketsPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + + return ( + }> +
+ + +
+
+ ) +} diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 79b1b66..1db5d02 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -102,6 +102,12 @@ type MachineTicketSummary = { assignee: { name: string | null; email: string | null } | null } +type MachineOpenTicketsSummary = { + totalOpen: number + hasMore: boolean + tickets: MachineTicketSummary[] +} + type DetailLineProps = { label: string @@ -1454,9 +1460,14 @@ export function MachineDetails({ machine }: MachineDetailsProps) { const machineAlertsHistory = alertsHistory ?? [] const openTickets = useQuery( machine ? api.machines.listOpenTickets : "skip", - machine ? { machineId: machine.id as Id<"machines">, limit: 8 } : ("skip" as const) - ) as MachineTicketSummary[] | undefined - const machineTickets = openTickets ?? [] + machine ? { machineId: machine.id as Id<"machines">, limit: 6 } : ("skip" as const) + ) as MachineOpenTicketsSummary | undefined + const machineTickets = openTickets?.tickets ?? [] + const totalOpenTickets = openTickets?.totalOpen ?? machineTickets.length + const displayLimit = 3 + const displayedMachineTickets = machineTickets.slice(0, displayLimit) + const hasAdditionalOpenTickets = totalOpenTickets > displayedMachineTickets.length + const machineTicketsHref = machine ? `/admin/machines/${machine.id}/tickets` : null const metadata = machine?.inventory ?? null const metrics = machine?.metrics ?? null const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics]) @@ -2356,45 +2367,65 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
-
-

Tickets abertos por esta máquina

- {machineTickets.length === 0 ? ( -

Nenhum chamado em aberto registrado diretamente por esta máquina.

+
+
+

Tickets abertos por esta máquina

+ {machineTicketsHref ? ( + + Ver todos + + ) : null} +
+ {totalOpenTickets === 0 ? ( +

+ Nenhum chamado em aberto registrado diretamente por esta máquina. +

) : ( -
    - {machineTickets.map((ticket) => { - const priorityMeta = getTicketPriorityMeta(ticket.priority) - return ( -
  • - -
    -

    - #{ticket.reference} · {ticket.subject} -

    -

    - Atualizado {formatRelativeTime(new Date(ticket.updatedAt))} -

    -
    -
    - - {priorityMeta.label} - - -
    - -
  • - ) - })} -
+
+ {hasAdditionalOpenTickets ? ( +

+ Mostrando últimos {Math.min(displayLimit, totalOpenTickets)} de {totalOpenTickets} chamados + em aberto +

+ ) : null} +
    + {displayedMachineTickets.map((ticket) => { + const priorityMeta = getTicketPriorityMeta(ticket.priority) + return ( +
  • + +
    +

    + #{ticket.reference} · {ticket.subject} +

    +

    + Atualizado {formatRelativeTime(new Date(ticket.updatedAt))} +

    +
    +
    + + {priorityMeta.label} + + +
    + +
  • + ) + })} +
+
)}
- {machineTickets.length} + {totalOpenTickets}
diff --git a/src/components/admin/machines/machine-breadcrumbs.client.tsx b/src/components/admin/machines/machine-breadcrumbs.client.tsx index 58cc13e..6666681 100644 --- a/src/components/admin/machines/machine-breadcrumbs.client.tsx +++ b/src/components/admin/machines/machine-breadcrumbs.client.tsx @@ -7,7 +7,19 @@ import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" -export function MachineBreadcrumbs({ tenantId: _tenantId, machineId }: { tenantId: string; machineId: string }) { +type BreadcrumbSegment = { + label: string + href?: string | null +} + +type MachineBreadcrumbsProps = { + tenantId: string + machineId: string + machineHref?: string | null + extra?: BreadcrumbSegment[] +} + +export function MachineBreadcrumbs({ tenantId: _tenantId, machineId, machineHref, extra }: MachineBreadcrumbsProps) { const { convexUserId } = useAuth() const queryArgs = machineId && convexUserId ? ({ id: machineId as Id<"machines">, includeMetadata: false } as const) @@ -15,15 +27,36 @@ export function MachineBreadcrumbs({ tenantId: _tenantId, machineId }: { tenantI const item = useQuery(api.machines.getById, queryArgs) const hostname = useMemo(() => item?.hostname ?? "Detalhe", [item]) + const segments = useMemo(() => { + const trail: BreadcrumbSegment[] = [ + { label: "Máquinas", href: "/admin/machines" }, + { label: hostname, href: machineHref ?? undefined }, + ] + if (Array.isArray(extra) && extra.length > 0) { + trail.push(...extra.filter((segment): segment is BreadcrumbSegment => Boolean(segment?.label))) + } + return trail + }, [hostname, machineHref, extra]) return ( ) diff --git a/src/components/admin/machines/machine-tickets-history.client.tsx b/src/components/admin/machines/machine-tickets-history.client.tsx new file mode 100644 index 0000000..6af2e9f --- /dev/null +++ b/src/components/admin/machines/machine-tickets-history.client.tsx @@ -0,0 +1,439 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import Link from "next/link" +import { usePaginatedQuery, useQuery } from "convex/react" +import { format, formatDistanceToNowStrict } from "date-fns" +import { ptBR } from "date-fns/locale" + +import type { Id } from "@/convex/_generated/dataModel" +import { api } from "@/convex/_generated/api" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Spinner } from "@/components/ui/spinner" +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty" +import { TicketStatusBadge } from "@/components/tickets/status-badge" +import type { TicketPriority, TicketStatus } from "@/lib/schemas/ticket" + +type MachineTicketHistoryItem = { + id: string + reference: number + subject: string + status: TicketStatus + priority: TicketPriority | string + updatedAt: number + createdAt: number + queue: string | null + requester: { name: string | null; email: string | null } | null + assignee: { name: string | null; email: string | null } | null +} + +type MachineTicketsHistoryArgs = { + machineId: Id<"machines"> + status?: "open" | "resolved" + priority?: string + search?: string + from?: number + to?: number +} + +type MachineTicketsHistoryStats = { + total: number + openCount: number + resolvedCount: number +} + +type PeriodPreset = "7d" | "30d" | "90d" | "year" | "all" | "custom" + +function startOfDayMs(date: Date) { + const copy = new Date(date) + copy.setHours(0, 0, 0, 0) + return copy.getTime() +} + +function endOfDayMs(date: Date) { + const copy = new Date(date) + copy.setHours(23, 59, 59, 999) + return copy.getTime() +} + +function parseDateInput(value: string) { + if (!value) return null + const parsed = new Date(`${value}T00:00:00`) + if (Number.isNaN(parsed.getTime())) { + return null + } + return parsed +} + +function computeRange(preset: PeriodPreset, customFrom: string, customTo: string) { + if (preset === "all") { + return { from: null, to: null } + } + if (preset === "custom") { + const fromDate = parseDateInput(customFrom) + const toDate = parseDateInput(customTo) + return { + from: fromDate ? startOfDayMs(fromDate) : null, + to: toDate ? endOfDayMs(toDate) : null, + } + } + + const now = new Date() + const end = endOfDayMs(now) + const start = new Date(now) + + switch (preset) { + case "7d": + start.setDate(start.getDate() - 6) + break + case "30d": + start.setDate(start.getDate() - 29) + break + case "90d": + start.setDate(start.getDate() - 89) + break + case "year": + start.setMonth(0, 1) + start.setHours(0, 0, 0, 0) + break + default: + break + } + + return { from: startOfDayMs(start), to: end } +} + +function formatRelativeTime(timestamp?: number | null) { + if (!timestamp || timestamp <= 0) return "—" + return formatDistanceToNowStrict(timestamp, { addSuffix: true, locale: ptBR }) +} + +function formatAbsoluteTime(timestamp?: number | null) { + if (!timestamp || timestamp <= 0) return "—" + return format(timestamp, "dd/MM/yyyy HH:mm", { locale: ptBR }) +} + +function getPriorityMeta(priority: TicketPriority | string | null | undefined) { + const normalized = (priority ?? "MEDIUM").toString().toUpperCase() + switch (normalized) { + case "LOW": + return { label: "Baixa", badgeClass: "bg-emerald-100 text-emerald-700 border border-emerald-200" } + case "MEDIUM": + return { label: "Média", badgeClass: "bg-sky-100 text-sky-700 border border-sky-200" } + case "HIGH": + return { label: "Alta", badgeClass: "bg-amber-100 text-amber-700 border border-amber-200" } + case "URGENT": + return { label: "Urgente", badgeClass: "bg-rose-100 text-rose-700 border border-rose-200" } + default: + return { label: normalized, badgeClass: "bg-slate-100 text-slate-700 border border-slate-200" } + } +} + +export function MachineTicketsHistoryClient({ tenantId: _tenantId, machineId }: { tenantId: string; machineId: string }) { + const [statusFilter, setStatusFilter] = useState<"all" | "open" | "resolved">("all") + const [priorityFilter, setPriorityFilter] = useState("ALL") + const [periodPreset, setPeriodPreset] = useState("90d") + const [customFrom, setCustomFrom] = useState("") + const [customTo, setCustomTo] = useState("") + const [searchValue, setSearchValue] = useState("") + const [debouncedSearch, setDebouncedSearch] = useState("") + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchValue.trim()) + }, 300) + return () => clearTimeout(timer) + }, [searchValue]) + + useEffect(() => { + if (periodPreset !== "custom") { + setCustomFrom("") + setCustomTo("") + } + }, [periodPreset]) + + const range = useMemo(() => computeRange(periodPreset, customFrom, customTo), [periodPreset, customFrom, customTo]) + + const queryArgs = useMemo(() => { + const args: MachineTicketsHistoryArgs = { + machineId: machineId as Id<"machines">, + } + if (statusFilter !== "all") { + args.status = statusFilter + } + if (priorityFilter !== "ALL") { + args.priority = priorityFilter + } + if (debouncedSearch) { + args.search = debouncedSearch + } + if (range.from !== null) { + args.from = range.from + } + if (range.to !== null) { + args.to = range.to + } + return args + }, [debouncedSearch, machineId, priorityFilter, range.from, range.to, statusFilter]) + + const { results: tickets, status: paginationStatus, loadMore } = usePaginatedQuery( + api.machines.listTicketsHistory, + queryArgs, + { initialNumItems: 25 } + ) + + const stats = useQuery(api.machines.getTicketsHistoryStats, queryArgs) as MachineTicketsHistoryStats | undefined + const totalTickets = stats?.total ?? 0 + const openTickets = stats?.openCount ?? 0 + const resolvedTickets = stats?.resolvedCount ?? 0 + + const isLoadingFirstPage = paginationStatus === "LoadingFirstPage" + const isLoadingMore = paginationStatus === "LoadingMore" + const canLoadMore = paginationStatus === "CanLoadMore" + + const resetFilters = () => { + setStatusFilter("all") + setPriorityFilter("ALL") + setPeriodPreset("90d") + setCustomFrom("") + setCustomTo("") + setSearchValue("") + setDebouncedSearch("") + } + + return ( +
+
+
+

Chamados no período

+

+ {stats ? ( + totalTickets + ) : ( + + Atualizando... + + )} +

+
+
+

Em aberto

+

{stats ? openTickets : "—"}

+
+
+

Resolvidos

+

{stats ? resolvedTickets : "—"}

+
+
+ +
+
+
+ setSearchValue(event.target.value)} + placeholder="Buscar por assunto, #ID, solicitante ou responsável" + className="sm:max-w-sm" + /> + + +
+
+ + {periodPreset === "custom" ? ( +
+ setCustomFrom(event.target.value)} + className="sm:w-[160px]" + placeholder="Início" + /> + setCustomTo(event.target.value)} + className="sm:w-[160px]" + placeholder="Fim" + /> +
+ ) : null} + +
+
+
+ +
+ {isLoadingFirstPage ? ( +
+ + Carregando chamados... +
+ ) : tickets.length === 0 ? ( + + + Nenhum chamado encontrado + + Ajuste os filtros ou expanda o período para visualizar o histórico de chamados desta máquina. + + + + + + + ) : ( + <> +
+ + + + + Ticket + + + Status + + + Prioridade + + + Última atualização + + + Responsável + + + + + {tickets.map((ticket) => { + const priorityMeta = getPriorityMeta(ticket.priority) + const requesterLabel = ticket.requester?.name ?? ticket.requester?.email ?? "Solicitante não informado" + const assigneeLabel = ticket.assignee?.name ?? ticket.assignee?.email ?? "Sem responsável" + const updatedLabel = formatRelativeTime(ticket.updatedAt) + const updatedAbsolute = formatAbsoluteTime(ticket.updatedAt) + return ( + + +
+ + #{ticket.reference} · {ticket.subject} + +
+ {requesterLabel} + {ticket.queue ? ( + + {ticket.queue} + + ) : null} + Aberto em {formatAbsoluteTime(ticket.createdAt)} +
+
+
+ + + + + + {priorityMeta.label} + + + +
+ {updatedLabel} + {updatedAbsolute} +
+
+ +
+ {assigneeLabel} + {ticket.assignee?.email ? ( + {ticket.assignee.email} + ) : null} +
+
+
+ ) + })} +
+
+
+
+ {canLoadMore || isLoadingMore ? ( + + ) : ( + + Todos os chamados filtrados foram exibidos. + + )} +
+ + )} +
+
+ ) +} diff --git a/src/components/tickets/close-ticket-dialog.tsx b/src/components/tickets/close-ticket-dialog.tsx index db40780..59d2183 100644 --- a/src/components/tickets/close-ticket-dialog.tsx +++ b/src/components/tickets/close-ticket-dialog.tsx @@ -303,7 +303,7 @@ export function CloseTicketDialog({ return ( - + Encerrar ticket diff --git a/src/components/ui/searchable-combobox.tsx b/src/components/ui/searchable-combobox.tsx index 80a8fb8..0c7d417 100644 --- a/src/components/ui/searchable-combobox.tsx +++ b/src/components/ui/searchable-combobox.tsx @@ -93,7 +93,7 @@ export function SearchableCombobox({ aria-expanded={open} disabled={disabled} className={cn( - "flex h-9 w-full items-center justify-between rounded-full border border-input bg-background px-3 text-sm font-medium text-foreground shadow-sm transition focus-visible:ring-2 focus-visible:ring-ring", + "flex min-h-[42px] w-full items-center justify-between rounded-full border border-input bg-background px-3 py-2 text-sm font-medium text-foreground shadow-sm transition focus-visible:ring-2 focus-visible:ring-ring", className, )} > @@ -171,4 +171,3 @@ export function SearchableCombobox({ ) } - diff --git a/tests/machines.getById.test.ts b/tests/machines.getById.test.ts index 08bfeeb..b60d374 100644 --- a/tests/machines.getById.test.ts +++ b/tests/machines.getById.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from "vitest" import type { Doc, Id } from "../convex/_generated/dataModel" -import { getById } from "../convex/machines" +import { getByIdHandler } from "../convex/machines" const FIXED_NOW = 1_706_071_200_000 @@ -65,8 +65,8 @@ describe("convex.machines.getById", () => { })), } - const ctx = { db } as unknown as Parameters[0] - const result = await getById(ctx, { id: machine._id, includeMetadata: true }) + const ctx = { db } as unknown as Parameters[0] + const result = await getByIdHandler(ctx, { id: machine._id, includeMetadata: true }) expect(result).toBeTruthy() expect(result?.metrics).toBeTruthy() @@ -75,4 +75,3 @@ describe("convex.machines.getById", () => { expect(result?.token).toBeTruthy() }) }) - diff --git a/tests/machines.listTicketsHistory.test.ts b/tests/machines.listTicketsHistory.test.ts new file mode 100644 index 0000000..41221e5 --- /dev/null +++ b/tests/machines.listTicketsHistory.test.ts @@ -0,0 +1,255 @@ +import { describe, expect, it, vi } from "vitest" + +import type { Doc, Id } from "../convex/_generated/dataModel" +import { getTicketsHistoryStatsHandler, listTicketsHistoryHandler } from "../convex/machines" + +const MACHINE_ID = "machine_1" as Id<"machines"> +const TENANT_ID = "tenant-1" + +function buildMachine(overrides: Partial> = {}): Doc<"machines"> { + const machine: Record = { + _id: MACHINE_ID, + tenantId: TENANT_ID, + hostname: "desktop-01", + macAddresses: [], + serialNumbers: [], + fingerprint: "fp", + isActive: true, + lastHeartbeatAt: Date.now(), + createdAt: Date.now() - 10_000, + updatedAt: Date.now() - 5_000, + linkedUserIds: [], + remoteAccess: null, + } + return { ...(machine as Doc<"machines">), ...overrides } +} + +function buildTicket(overrides: Partial> = {}): Doc<"tickets"> { + const base: Record = { + _id: "ticket_base" as Id<"tickets">, + tenantId: TENANT_ID, + reference: 42600, + subject: "Generic ticket", + summary: "", + status: "PENDING", + priority: "MEDIUM", + channel: "EMAIL", + queueId: undefined, + requesterId: "user_req" as Id<"users">, + requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, + assigneeId: "user_assignee" as Id<"users">, + assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, + companyId: undefined, + companySnapshot: undefined, + machineId: MACHINE_ID, + machineSnapshot: undefined, + working: false, + dueAt: undefined, + firstResponseAt: undefined, + resolvedAt: undefined, + closedAt: undefined, + updatedAt: Date.now(), + createdAt: Date.now() - 2000, + tags: [], + customFields: [], + totalWorkedMs: 0, + internalWorkedMs: 0, + externalWorkedMs: 0, + activeSessionId: undefined, + } + return { ...(base as Doc<"tickets">), ...overrides } +} + +type PaginateHandler = (options: { cursor: string | null; numItems: number }) => Promise<{ + page: Doc<"tickets">[] + isDone: boolean + continueCursor: string +}> + +function createCtx({ + machine = buildMachine(), + queues = new Map>(), + paginate, +}: { + machine?: Doc<"machines"> + queues?: Map> + paginate: PaginateHandler +}) { + const createFilterBuilder = () => { + const builder: Record typeof builder> = {} + builder.eq = () => builder + builder.gte = () => builder + builder.lte = () => builder + builder.or = () => builder + return builder + } + + return { + db: { + get: vi.fn(async (id: Id<"machines"> | Id<"queues">) => { + if (machine && id === machine._id) return machine + const queue = queues.get(String(id)) + if (queue) return queue + return null + }), + query: vi.fn(() => { + const chain = { + filter: vi.fn((cb?: (builder: ReturnType) => unknown) => { + cb?.(createFilterBuilder()) + return chain + }), + paginate: vi.fn((options: { cursor: string | null; numItems: number }) => paginate(options)), + } + + return { + withIndex: vi.fn((_indexName: string, cb?: (builder: ReturnType) => unknown) => { + cb?.(createFilterBuilder()) + return { + order: vi.fn(() => chain), + } + }), + } + }), + }, + } as unknown as Parameters[0] +} + +describe("convex.machines.listTicketsHistory", () => { + it("maps tickets metadata and resolves queue names", async () => { + const machine = buildMachine() + const ticket = buildTicket({ + _id: "ticket_1" as Id<"tickets">, + subject: "Printer offline", + priority: "HIGH", + status: "PENDING", + queueId: "queue_1" as Id<"queues">, + updatedAt: 170000, + createdAt: 160000, + }) + + const paginate = vi.fn(async () => ({ + page: [ticket], + isDone: false, + continueCursor: "cursor-next", + })) + + const queues = new Map>([ + ["queue_1", { _id: "queue_1" as Id<"queues">, name: "Atendimento", tenantId: TENANT_ID } as Doc<"queues">], + ]) + + const ctx = createCtx({ machine, queues, paginate }) + + const result = await listTicketsHistoryHandler(ctx, { + machineId: machine._id, + paginationOpts: { numItems: 25, cursor: null }, + }) + + expect(paginate).toHaveBeenCalledWith({ numItems: 25, cursor: null }) + expect(result.page).toHaveLength(1) + expect(result.page[0]).toMatchObject({ + id: "ticket_1", + subject: "Printer offline", + priority: "HIGH", + status: "PENDING", + queue: "Atendimento", + }) + expect(result.continueCursor).toBe("cursor-next") + }) + + it("applies search filtering over paginated results", async () => { + const machine = buildMachine() + const ticketMatches = buildTicket({ + _id: "ticket_match" as Id<"tickets">, + reference: 44321, + subject: "Notebook com tela quebrada", + requesterSnapshot: { name: "Carla", email: "carla@example.com", avatarUrl: undefined, teams: [] }, + }) + const ticketIgnored = buildTicket({ + _id: "ticket_other" as Id<"tickets">, + subject: "Troca de teclado", + requesterSnapshot: { name: "Roberto", email: "roberto@example.com", avatarUrl: undefined, teams: [] }, + }) + + const paginate = vi.fn(async () => ({ + page: [ticketMatches, ticketIgnored], + isDone: true, + continueCursor: "", + })) + + const ctx = createCtx({ machine, paginate }) + + const result = await listTicketsHistoryHandler(ctx, { + machineId: machine._id, + search: "notebook", + paginationOpts: { numItems: 50, cursor: null }, + }) + + expect(result.page).toHaveLength(1) + expect(result.page[0].id).toBe("ticket_match") + expect(result.isDone).toBe(true) + }) +}) + +describe("convex.machines.getTicketsHistoryStats", () => { + it("aggregates totals across multiple pages respecting open status", async () => { + const machine = buildMachine() + const firstPageTicket = buildTicket({ + _id: "ticket_open" as Id<"tickets">, + status: "AWAITING_ATTENDANCE", + }) + const secondPageTicket = buildTicket({ + _id: "ticket_resolved" as Id<"tickets">, + status: "RESOLVED", + }) + + const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => { + if (!cursor) { + return { page: [firstPageTicket], isDone: false, continueCursor: "cursor-1" } + } + return { page: [secondPageTicket], isDone: true, continueCursor: "" } + }) + + const ctx = createCtx({ machine, paginate }) + + const stats = await getTicketsHistoryStatsHandler( + ctx as unknown as Parameters[0], + { + machineId: machine._id, + } + ) + + expect(stats).toEqual({ total: 2, openCount: 1, resolvedCount: 1 }) + expect(paginate).toHaveBeenCalledTimes(2) + }) + + it("filters results by search term when aggregating", async () => { + const machine = buildMachine() + const matchingTicket = buildTicket({ + _id: "ticket_search" as Id<"tickets">, + subject: "Notebook com lentidão", + }) + const nonMatchingTicket = buildTicket({ + _id: "ticket_ignored" as Id<"tickets">, + subject: "Impressora parada", + }) + + const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => { + if (!cursor) { + return { page: [matchingTicket, nonMatchingTicket], isDone: true, continueCursor: "" } + } + return { page: [], isDone: true, continueCursor: "" } + }) + + const ctx = createCtx({ machine, paginate }) + + const stats = await getTicketsHistoryStatsHandler( + ctx as unknown as Parameters[0], + { + machineId: machine._id, + search: "notebook", + } + ) + + expect(stats).toEqual({ total: 1, openCount: 1, resolvedCount: 0 }) + }) +}) diff --git a/tests/machines.updatePersona.test.ts b/tests/machines.updatePersona.test.ts index 7aa3dfa..9758677 100644 --- a/tests/machines.updatePersona.test.ts +++ b/tests/machines.updatePersona.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest" -import { updatePersona } from "../convex/machines" +import { updatePersonaHandler } from "../convex/machines" import type { Doc, Id } from "../convex/_generated/dataModel" const FIXED_NOW = 1_706_071_200_000 @@ -63,9 +63,9 @@ describe("convex.machines.updatePersona", () => { return null }) - const ctx = { db: { get, patch } } as unknown as Parameters[0] + const ctx = { db: { get, patch } } as unknown as Parameters[0] - const result = await updatePersona(ctx, { machineId: machine._id, persona: "" }) + const result = await updatePersonaHandler(ctx, { machineId: machine._id, persona: "" }) expect(result).toEqual({ ok: true, persona: null }) expect(patch).toHaveBeenCalledTimes(1) @@ -92,10 +92,10 @@ describe("convex.machines.updatePersona", () => { return null }) - const ctx = { db: { get, patch } } as unknown as Parameters[0] + const ctx = { db: { get, patch } } as unknown as Parameters[0] await expect( - updatePersona(ctx, { + updatePersonaHandler(ctx, { machineId: machine._id, persona: "collaborator", assignedUserId: missingUserId, diff --git a/tests/reports.productivity-dashboard.test.ts b/tests/reports.productivity-dashboard.test.ts new file mode 100644 index 0000000..a4f9ef6 --- /dev/null +++ b/tests/reports.productivity-dashboard.test.ts @@ -0,0 +1,267 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest" + +vi.mock("../convex/rbac", () => ({ + requireStaff: vi.fn(), +})) + +import type { Doc, Id } from "../convex/_generated/dataModel" +import { + agentProductivityHandler, + dashboardOverviewHandler, + hoursByClientInternalHandler, +} from "../convex/reports" +import { requireStaff } from "../convex/rbac" +import { createReportsCtx } from "./utils/report-test-helpers" + +const TENANT_ID = "tenant-1" +const VIEWER_ID = "user-agent" as Id<"users"> + +function buildTicket(overrides: Partial>): Doc<"tickets"> { + const base: Record = { + _id: "ticket_base" as Id<"tickets">, + tenantId: TENANT_ID, + reference: 50000, + subject: "Chamado", + summary: null, + status: "PENDING", + priority: "MEDIUM", + channel: "EMAIL", + queueId: undefined, + requesterId: "user_req" as Id<"users">, + requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, + assigneeId: "user_assignee" as Id<"users">, + assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, + companyId: undefined, + companySnapshot: undefined, + machineId: undefined, + machineSnapshot: undefined, + working: false, + dueAt: undefined, + firstResponseAt: undefined, + resolvedAt: undefined, + closedAt: undefined, + updatedAt: Date.now(), + createdAt: Date.now(), + tags: [], + customFields: [], + totalWorkedMs: 0, + internalWorkedMs: 0, + externalWorkedMs: 0, + activeSessionId: undefined, + } + return { ...(base as Doc<"tickets">), ...overrides } +} + +function buildCompany(overrides: Partial>): Doc<"companies"> { + const base: Record = { + _id: "company_base" as Id<"companies">, + tenantId: TENANT_ID, + name: "Empresa", + slug: "empresa", + createdAt: Date.now(), + updatedAt: Date.now(), + isAvulso: false, + contractedHoursPerMonth: 40, + } + return { ...(base as Doc<"companies">), ...overrides } +} + +describe("convex.reports.agentProductivity", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 6, 10, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("aggregates per-agent metrics including work sessions", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const agentA = { _id: "agent_a" as Id<"users">, name: "Ana", email: "ana@example.com" } as Doc<"users"> + const agentB = { _id: "agent_b" as Id<"users">, name: "Bruno", email: "bruno@example.com" } as Doc<"users"> + + const tickets = [ + buildTicket({ + _id: "ticket_open" as Id<"tickets">, + assigneeId: agentA._id, + createdAt: Date.UTC(2024, 6, 9, 9, 0, 0), + status: "PENDING", + firstResponseAt: Date.UTC(2024, 6, 9, 9, 30, 0), + internalWorkedMs: 45 * 60 * 1000, + }), + buildTicket({ + _id: "ticket_resolved" as Id<"tickets">, + assigneeId: agentA._id, + createdAt: Date.UTC(2024, 6, 8, 10, 0, 0), + firstResponseAt: Date.UTC(2024, 6, 8, 10, 20, 0), + resolvedAt: Date.UTC(2024, 6, 8, 12, 0, 0), + status: "RESOLVED", + }), + buildTicket({ + _id: "ticket_old" as Id<"tickets">, + assigneeId: agentB._id, + createdAt: Date.UTC(2024, 5, 20, 10, 0, 0), + status: "RESOLVED", + }), + ] + + const sessionsMap = new Map; startedAt: number; stoppedAt?: number; durationMs?: number }>>([ + [ + String(agentA._id), + [ + { agentId: agentA._id, startedAt: Date.UTC(2024, 6, 9, 9, 0, 0), stoppedAt: Date.UTC(2024, 6, 9, 10, 0, 0) }, + { agentId: agentA._id, startedAt: Date.UTC(2024, 6, 8, 11, 0, 0), durationMs: 30 * 60 * 1000 }, + ], + ], + ]) + + const ctx = createReportsCtx({ + tickets, + users: new Map>([ + [String(agentA._id), agentA], + [String(agentB._id), agentB], + ]), + ticketWorkSessionsByAgent: sessionsMap, + }) as Parameters[0] + + const result = await agentProductivityHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.items).toHaveLength(1) + expect(result.items[0]).toMatchObject({ + agentId: agentA._id, + open: 1, + resolved: 1, + avgFirstResponseMinutes: 25, + }) + expect(result.items[0]?.workedHours).toBeCloseTo(1.5, 1) + }) +}) + +describe("convex.reports.dashboardOverview", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 6, 15, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("returns trend metrics for new, in-progress and resolution data", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const tickets = [ + buildTicket({ + _id: "ticket_new" as Id<"tickets">, + createdAt: Date.UTC(2024, 6, 15, 8, 0, 0), + firstResponseAt: Date.UTC(2024, 6, 15, 8, 30, 0), + status: "PENDING", + }), + buildTicket({ + _id: "ticket_resolved_recent" as Id<"tickets">, + createdAt: Date.UTC(2024, 6, 8, 9, 0, 0), + firstResponseAt: Date.UTC(2024, 6, 8, 9, 15, 0), + resolvedAt: Date.UTC(2024, 6, 13, 12, 0, 0), + status: "RESOLVED", + }), + buildTicket({ + _id: "ticket_prev" as Id<"tickets">, + createdAt: Date.UTC(2024, 6, 13, 9, 0, 0), + firstResponseAt: Date.UTC(2024, 6, 13, 9, 45, 0), + status: "PAUSED", + dueAt: Date.UTC(2024, 6, 14, 9, 0, 0), + }), + buildTicket({ + _id: "ticket_prev_resolved" as Id<"tickets">, + createdAt: Date.UTC(2024, 6, 5, 9, 0, 0), + firstResponseAt: Date.UTC(2024, 6, 5, 9, 10, 0), + resolvedAt: Date.UTC(2024, 6, 7, 10, 0, 0), + status: "RESOLVED", + }), + ] + + const ctx = createReportsCtx({ tickets }) as Parameters[0] + + const result = await dashboardOverviewHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + }) + + expect(result.newTickets.last24h).toBe(1) + expect(result.inProgress.current).toBe(2) + expect(result.awaitingAction.total).toBe(2) + expect(result.resolution.resolvedLast7d).toBe(1) + }) +}) + +describe("convex.reports.hoursByClientInternal", () => { + const FIXED_NOW = Date.UTC(2024, 7, 1, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("sums internal and external hours per company", async () => { + const companyA = buildCompany({ _id: "company_a" as Id<"companies">, name: "Empresa A" }) + const companyB = buildCompany({ _id: "company_b" as Id<"companies">, name: "Empresa B", isAvulso: true }) + + const tickets = [ + buildTicket({ + _id: "ticket_a" as Id<"tickets">, + companyId: companyA._id, + updatedAt: Date.UTC(2024, 6, 30, 10, 0, 0), + internalWorkedMs: 2 * 3600000, + externalWorkedMs: 3600000, + }), + buildTicket({ + _id: "ticket_b" as Id<"tickets">, + companyId: companyB._id, + updatedAt: Date.UTC(2024, 6, 29, 14, 0, 0), + internalWorkedMs: 0, + externalWorkedMs: 2 * 3600000, + }), + ] + + const ctx = createReportsCtx({ + tickets, + companies: new Map([ + [String(companyA._id), companyA], + [String(companyB._id), companyB], + ]), + }) as Parameters[0] + + const result = await hoursByClientInternalHandler(ctx, { tenantId: TENANT_ID, range: "7d" }) + + expect(result.rangeDays).toBe(7) + expect(result.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ companyId: companyA._id, internalMs: 2 * 3600000, externalMs: 3600000 }), + expect.objectContaining({ companyId: companyB._id, internalMs: 0, externalMs: 2 * 3600000 }), + ]) + ) + }) +}) diff --git a/tests/reports.sla-backlog.test.ts b/tests/reports.sla-backlog.test.ts new file mode 100644 index 0000000..440859c --- /dev/null +++ b/tests/reports.sla-backlog.test.ts @@ -0,0 +1,199 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest" + +vi.mock("../convex/rbac", () => ({ + requireStaff: vi.fn(), +})) + +import type { Doc, Id } from "../convex/_generated/dataModel" +import { backlogOverviewHandler, slaOverviewHandler } from "../convex/reports" +import { requireStaff } from "../convex/rbac" +import { createReportsCtx } from "./utils/report-test-helpers" + +const TENANT_ID = "tenant-1" +const VIEWER_ID = "user-staff" as Id<"users"> + +function buildTicket(overrides: Partial>): Doc<"tickets"> { + const base: Record = { + _id: "ticket_base" as Id<"tickets">, + tenantId: TENANT_ID, + reference: 50000, + subject: "Chamado", + summary: null, + status: "PENDING", + priority: "MEDIUM", + channel: "EMAIL", + queueId: undefined, + requesterId: "user_req" as Id<"users">, + requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, + assigneeId: "user_agent" as Id<"users">, + assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, + companyId: undefined, + companySnapshot: undefined, + machineId: undefined, + machineSnapshot: undefined, + working: false, + dueAt: undefined, + firstResponseAt: undefined, + resolvedAt: undefined, + closedAt: undefined, + updatedAt: Date.now(), + createdAt: Date.now(), + tags: [], + customFields: [], + totalWorkedMs: 0, + internalWorkedMs: 0, + externalWorkedMs: 0, + activeSessionId: undefined, + } + return { ...(base as Doc<"tickets">), ...overrides } +} + +function buildQueue(overrides: Partial>): Doc<"queues"> { + const base: Record = { + _id: "queue_base" as Id<"queues">, + tenantId: TENANT_ID, + name: "Suporte", + slug: "suporte", + createdAt: Date.now(), + updatedAt: Date.now(), + } + return { ...(base as Doc<"queues">), ...overrides } +} + +describe("convex.reports.slaOverview", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 4, 8, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("summarizes totals, averages and queue breakdown", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const queue = buildQueue({ + _id: "queue_1" as Id<"queues">, + name: "Suporte Nível 1", + }) + + const tickets = [ + buildTicket({ + _id: "ticket_open" as Id<"tickets">, + status: "PENDING", + queueId: queue._id, + createdAt: Date.UTC(2024, 4, 7, 9, 0, 0), + dueAt: Date.UTC(2024, 4, 7, 11, 0, 0), + }), + buildTicket({ + _id: "ticket_resolved" as Id<"tickets">, + status: "RESOLVED", + queueId: queue._id, + createdAt: Date.UTC(2024, 4, 6, 8, 0, 0), + firstResponseAt: Date.UTC(2024, 4, 6, 8, 30, 0), + resolvedAt: Date.UTC(2024, 4, 6, 10, 0, 0), + }), + ] + + const ctx = createReportsCtx({ tickets, queues: [queue] }) as Parameters[0] + + const result = await slaOverviewHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.totals).toEqual({ total: 2, open: 1, resolved: 1, overdue: 1 }) + expect(result.response).toEqual({ averageFirstResponseMinutes: 30, responsesRegistered: 1 }) + expect(result.resolution).toEqual({ averageResolutionMinutes: 120, resolvedCount: 1 }) + expect(result.queueBreakdown).toEqual([{ id: queue._id, name: queue.name, open: 1 }]) + }) +}) + +describe("convex.reports.backlogOverview", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 6, 1, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("aggregates status, priority and queue counts for open tickets", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const queueA = buildQueue({ _id: "queue_a" as Id<"queues">, name: "Atendimento" }) + const queueB = buildQueue({ _id: "queue_b" as Id<"queues">, name: "Infraestrutura" }) + + const tickets = [ + buildTicket({ + _id: "ticket_pending" as Id<"tickets">, + status: "PENDING", + priority: "HIGH", + queueId: queueA._id, + createdAt: Date.UTC(2024, 5, 28, 10, 0, 0), + }), + buildTicket({ + _id: "ticket_paused" as Id<"tickets">, + status: "PAUSED", + priority: "MEDIUM", + queueId: queueB._id, + createdAt: Date.UTC(2024, 5, 29, 15, 0, 0), + }), + buildTicket({ + _id: "ticket_resolved" as Id<"tickets">, + status: "RESOLVED", + priority: "URGENT", + queueId: undefined, + createdAt: Date.UTC(2024, 5, 27, 9, 0, 0), + }), + ] + + const ctx = createReportsCtx({ + tickets, + createdRangeTickets: tickets, + queues: [queueA, queueB], + }) as Parameters[0] + + const result = await backlogOverviewHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.statusCounts).toEqual({ + PENDING: 1, + PAUSED: 1, + RESOLVED: 1, + }) + expect(result.priorityCounts).toEqual({ + HIGH: 1, + MEDIUM: 1, + URGENT: 1, + }) + expect(result.totalOpen).toBe(2) + expect(result.queueCounts).toHaveLength(2) + expect(result.queueCounts).toEqual( + expect.arrayContaining([ + { id: "queue_a", name: queueA.name, total: 1 }, + { id: "queue_b", name: queueB.name, total: 1 }, + ]) + ) + }) +}) diff --git a/tests/reports.ticketsByChannel.test.ts b/tests/reports.ticketsByChannel.test.ts new file mode 100644 index 0000000..70f8987 --- /dev/null +++ b/tests/reports.ticketsByChannel.test.ts @@ -0,0 +1,111 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest" + +vi.mock("../convex/rbac", () => ({ + requireStaff: vi.fn(), +})) + +import type { Doc, Id } from "../convex/_generated/dataModel" +import { ticketsByChannelHandler } from "../convex/reports" +import { requireStaff } from "../convex/rbac" +import { createReportsCtx } from "./utils/report-test-helpers" + +const TENANT_ID = "tenant-1" +const VIEWER_ID = "user-viewer" as Id<"users"> + +function buildTicket(overrides: Partial>): Doc<"tickets"> { + const base: Record = { + _id: "ticket_base" as Id<"tickets">, + tenantId: TENANT_ID, + reference: 50000, + subject: "Chamado", + summary: null, + status: "PENDING", + priority: "MEDIUM", + channel: "EMAIL", + queueId: undefined, + requesterId: "user_req" as Id<"users">, + requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, + assigneeId: "user_assignee" as Id<"users">, + assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, + companyId: undefined, + companySnapshot: undefined, + machineId: undefined, + machineSnapshot: undefined, + working: false, + dueAt: undefined, + firstResponseAt: undefined, + resolvedAt: undefined, + closedAt: undefined, + updatedAt: Date.now(), + createdAt: Date.now(), + tags: [], + customFields: [], + totalWorkedMs: 0, + internalWorkedMs: 0, + externalWorkedMs: 0, + activeSessionId: undefined, + } + return { ...(base as Doc<"tickets">), ...overrides } +} + +describe("convex.reports.ticketsByChannel", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 4, 8, 12, 0, 0) // 8 May 2024 12:00 UTC + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("builds timeline grouped by channel within the requested range", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const tickets = [ + buildTicket({ + _id: "ticket_email" as Id<"tickets">, + channel: "EMAIL", + createdAt: Date.UTC(2024, 4, 7, 10, 0, 0), + }), + buildTicket({ + _id: "ticket_chat" as Id<"tickets">, + channel: "CHAT", + createdAt: Date.UTC(2024, 4, 7, 12, 0, 0), + }), + buildTicket({ + _id: "ticket_other" as Id<"tickets">, + channel: undefined, + createdAt: Date.UTC(2024, 4, 6, 9, 0, 0), + }), + buildTicket({ + _id: "ticket_outside" as Id<"tickets">, + createdAt: Date.UTC(2024, 3, 25, 12, 0, 0), // outside 7-day window + }), + ] + + const ctx = createReportsCtx({ tickets }) as Parameters[0] + + const result = await ticketsByChannelHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.channels).toEqual(["CHAT", "EMAIL", "OUTRO"]) + + const may06 = result.points.find((point) => point.date === "2024-05-06") + const may07 = result.points.find((point) => point.date === "2024-05-07") + + expect(may06?.values).toEqual({ CHAT: 0, EMAIL: 0, OUTRO: 1 }) + expect(may07?.values).toEqual({ CHAT: 1, EMAIL: 1, OUTRO: 0 }) + expect(may06).toBeTruthy() + expect(may07).toBeTruthy() + }) +}) diff --git a/tests/reports.timeline-hours.test.ts b/tests/reports.timeline-hours.test.ts new file mode 100644 index 0000000..fc69ae8 --- /dev/null +++ b/tests/reports.timeline-hours.test.ts @@ -0,0 +1,260 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest" + +vi.mock("../convex/rbac", () => ({ + requireStaff: vi.fn(), +})) + +import type { Doc, Id } from "../convex/_generated/dataModel" +import { + csatOverviewHandler, + hoursByClientHandler, + openedResolvedByDayHandler, +} from "../convex/reports" +import { requireStaff } from "../convex/rbac" +import { createReportsCtx } from "./utils/report-test-helpers" + +const TENANT_ID = "tenant-1" +const VIEWER_ID = "user-reports" as Id<"users"> + +function buildTicket(overrides: Partial>): Doc<"tickets"> { + const base: Record = { + _id: "ticket_base" as Id<"tickets">, + tenantId: TENANT_ID, + reference: 50000, + subject: "Chamado", + summary: null, + status: "PENDING", + priority: "MEDIUM", + channel: "EMAIL", + queueId: undefined, + requesterId: "user_req" as Id<"users">, + requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, + assigneeId: "user_assignee" as Id<"users">, + assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, + companyId: undefined, + companySnapshot: undefined, + machineId: undefined, + machineSnapshot: undefined, + working: false, + dueAt: undefined, + firstResponseAt: undefined, + resolvedAt: undefined, + closedAt: undefined, + updatedAt: Date.now(), + createdAt: Date.now(), + tags: [], + customFields: [], + totalWorkedMs: 0, + internalWorkedMs: 0, + externalWorkedMs: 0, + activeSessionId: undefined, + } + return { ...(base as Doc<"tickets">), ...overrides } +} + +function buildCompany(overrides: Partial>): Doc<"companies"> { + const base: Record = { + _id: "company_base" as Id<"companies">, + tenantId: TENANT_ID, + name: "Empresa", + slug: "empresa", + createdAt: Date.now(), + updatedAt: Date.now(), + isAvulso: false, + contractedHoursPerMonth: 40, + } + return { ...(base as Doc<"companies">), ...overrides } +} + +describe("convex.reports.openedResolvedByDay", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 4, 15, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("counts opened and resolved tickets per day within the range", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const tickets = [ + buildTicket({ + _id: "ticket_open" as Id<"tickets">, + createdAt: Date.UTC(2024, 4, 13, 9, 0, 0), + status: "PENDING", + }), + buildTicket({ + _id: "ticket_resolved" as Id<"tickets">, + createdAt: Date.UTC(2024, 4, 12, 10, 0, 0), + resolvedAt: Date.UTC(2024, 4, 14, 8, 0, 0), + status: "RESOLVED", + }), + buildTicket({ + _id: "ticket_old" as Id<"tickets">, + createdAt: Date.UTC(2024, 3, 30, 10, 0, 0), + status: "PENDING", + }), + ] + + const ctx = createReportsCtx({ tickets }) as Parameters[0] + + const result = await openedResolvedByDayHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + const opened13 = result.series.find((point) => point.date === "2024-05-13") + const resolved14 = result.series.find((point) => point.date === "2024-05-14") + + expect(result.rangeDays).toBe(7) + expect(opened13).toMatchObject({ opened: 1, resolved: 0 }) + expect(resolved14).toMatchObject({ opened: 0, resolved: 1 }) + }) +}) + +describe("convex.reports.csatOverview", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 4, 20, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("summarizes survey averages and distribution", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const ticketA = buildTicket({ + _id: "ticket_a" as Id<"tickets">, + createdAt: Date.UTC(2024, 4, 18, 9, 0, 0), + }) + const ticketB = buildTicket({ + _id: "ticket_b" as Id<"tickets">, + createdAt: Date.UTC(2024, 4, 17, 15, 0, 0), + }) + + const eventsByTicket = new Map>([ + ["ticket_a", [{ type: "CSAT_RECEIVED", payload: { score: 5 }, createdAt: Date.UTC(2024, 4, 19, 8, 0, 0) }]], + [ + "ticket_b", + [ + { type: "CSAT_RECEIVED", payload: { score: 3 }, createdAt: Date.UTC(2024, 4, 18, 14, 0, 0) }, + { type: "COMMENT", payload: {}, createdAt: Date.UTC(2024, 4, 18, 15, 0, 0) }, + ], + ], + ]) + + const ctx = createReportsCtx({ tickets: [ticketA, ticketB], ticketEventsByTicket: eventsByTicket }) as Parameters[0] + + const result = await csatOverviewHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.totalSurveys).toBe(2) + expect(result.averageScore).toBe(4) + expect(result.distribution.find((entry) => entry.score === 5)?.total).toBe(1) + expect(result.distribution.find((entry) => entry.score === 3)?.total).toBe(1) + expect(result.recent[0]?.ticketId).toBe("ticket_a") + }) +}) + +describe("convex.reports.hoursByClient", () => { + const requireStaffMock = vi.mocked(requireStaff) + const FIXED_NOW = Date.UTC(2024, 5, 5, 12, 0, 0) + + beforeAll(() => { + vi.useFakeTimers() + vi.setSystemTime(FIXED_NOW) + }) + + afterAll(() => { + vi.useRealTimers() + }) + + it("aggregates worked hours by company", async () => { + requireStaffMock.mockResolvedValue({ + role: "ADMIN", + user: { companyId: undefined }, + } as unknown as Awaited>) + + const companyA = buildCompany({ _id: "company_a" as Id<"companies">, name: "Empresa A", contractedHoursPerMonth: 60 }) + const companyB = buildCompany({ _id: "company_b" as Id<"companies">, name: "Empresa B", isAvulso: true }) + + const tickets = [ + buildTicket({ + _id: "ticket_a" as Id<"tickets">, + companyId: companyA._id, + updatedAt: Date.UTC(2024, 5, 4, 10, 0, 0), + internalWorkedMs: 3 * 3600000, + externalWorkedMs: 1 * 3600000, + }), + buildTicket({ + _id: "ticket_b" as Id<"tickets">, + companyId: companyB._id, + updatedAt: Date.UTC(2024, 5, 3, 15, 0, 0), + internalWorkedMs: 3600000, + externalWorkedMs: 2 * 3600000, + }), + buildTicket({ + _id: "ticket_old" as Id<"tickets">, + companyId: companyA._id, + updatedAt: Date.UTC(2024, 4, 20, 12, 0, 0), + internalWorkedMs: 5 * 3600000, + externalWorkedMs: 0, + }), + ] + + const companies = new Map>([ + [String(companyA._id), companyA], + [String(companyB._id), companyB], + ]) + + const ctx = createReportsCtx({ tickets, companies }) as Parameters[0] + + const result = await hoursByClientHandler(ctx, { + tenantId: TENANT_ID, + viewerId: VIEWER_ID, + range: "7d", + }) + + expect(result.rangeDays).toBe(7) + expect(result.items).toHaveLength(2) + expect(result.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + companyId: companyA._id, + internalMs: 3 * 3600000, + externalMs: 3600000, + totalMs: 4 * 3600000, + contractedHoursPerMonth: 60, + }), + expect.objectContaining({ + companyId: companyB._id, + internalMs: 3600000, + externalMs: 2 * 3600000, + totalMs: 3 * 3600000, + isAvulso: true, + }), + ]) + ) + }) +}) diff --git a/tests/utils/report-test-helpers.ts b/tests/utils/report-test-helpers.ts new file mode 100644 index 0000000..eaf7c82 --- /dev/null +++ b/tests/utils/report-test-helpers.ts @@ -0,0 +1,127 @@ +import { vi } from "vitest" + +import type { Doc, Id } from "../../convex/_generated/dataModel" + +type ReportsCtxOptions = { + tickets?: Doc<"tickets">[] + createdRangeTickets?: Doc<"tickets">[] + queues?: Doc<"queues">[] + companies?: Map> + users?: Map> + ticketEventsByTicket?: Map> + ticketWorkSessionsByAgent?: Map; startedAt: number; stoppedAt?: number; durationMs?: number }>> +} + +const noopFilterBuilder = { + lt: () => noopFilterBuilder, + field: () => "createdAt", +} + +const noopIndexBuilder = { + eq: () => noopIndexBuilder, + gte: () => noopIndexBuilder, +} + +function ticketsChain(collection: Doc<"tickets">[]) { + const chain = { + filter: vi.fn((cb?: (builder: typeof noopFilterBuilder) => unknown) => { + cb?.(noopFilterBuilder) + return chain + }), + order: vi.fn(() => chain), + collect: vi.fn(async () => collection), + } + return chain +} + +export function createReportsCtx({ + tickets = [], + createdRangeTickets = tickets, + queues = [], + companies = new Map>(), + users = new Map>(), + ticketEventsByTicket = new Map>(), + ticketWorkSessionsByAgent = new Map; startedAt: number; stoppedAt?: number; durationMs?: number }>>(), +}: ReportsCtxOptions = {}) { + const db = { + get: vi.fn(async (id: Id<"companies"> | Id<"users">) => { + const company = companies.get(String(id)) + if (company) return company + const user = users.get(String(id)) + if (user) return user + return null + }), + query: vi.fn((table: string) => { + if (table === "tickets") { + return { + withIndex: vi.fn((indexName: string, cb?: (builder: typeof noopIndexBuilder) => unknown) => { + cb?.(noopIndexBuilder) + const collection = + indexName.includes("created") || indexName.includes("tenant_company_created") + ? createdRangeTickets + : tickets + return ticketsChain(collection) + }), + collect: vi.fn(async () => tickets), + } + } + + if (table === "queues") { + return { + withIndex: vi.fn((_indexName: string, cb?: (builder: typeof noopIndexBuilder) => unknown) => { + cb?.(noopIndexBuilder) + return { + collect: vi.fn(async () => queues), + } + }), + collect: vi.fn(async () => queues), + } + } + + if (table === "ticketEvents") { + return { + withIndex: vi.fn((_indexName: string, cb?: (builder: { eq: (field: unknown, value: unknown) => unknown }) => unknown) => { + let ticketId: string | null = null + const builder = { + eq: (_field: unknown, value: unknown) => { + ticketId = String(value) + return builder + }, + } + cb?.(builder as { eq: (field: unknown, value: unknown) => unknown }) + return { + collect: vi.fn(async () => (ticketId ? ticketEventsByTicket.get(ticketId) ?? [] : [])), + } + }), + } + } + + if (table === "ticketWorkSessions") { + return { + withIndex: vi.fn((_indexName: string, cb?: (builder: { eq: (field: unknown, value: unknown) => unknown }) => unknown) => { + let agentId: string | null = null + const builder = { + eq: (_field: unknown, value: unknown) => { + agentId = String(value) + return builder + }, + } + cb?.(builder as { eq: (field: unknown, value: unknown) => unknown }) + return { + collect: vi.fn(async () => (agentId ? ticketWorkSessionsByAgent.get(agentId) ?? [] : [])), + } + }), + } + } + + return { + withIndex: vi.fn(() => ({ + collect: vi.fn(async () => []), + })), + collect: vi.fn(async () => []), + } + }), + } + + return { db } as unknown +}