diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 4405a84..9d5cc22 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -134,6 +134,54 @@ function isTokenRevokedMessage(input: string) { ) } +function formatApiError(responseText: string, statusCode: number): string { + try { + const json = JSON.parse(responseText) + if (json.error === "Payload invalido" || json.error === "Payload inválido") { + const details = typeof json.details === "string" ? JSON.parse(json.details) : json.details + if (Array.isArray(details) && details.length > 0) { + const fieldLabels: Record = { + "collaborator.email": "E-mail", + "collaborator.name": "Nome", + email: "E-mail", + name: "Nome", + provisioningCode: "Código de ativação", + hostname: "Nome do computador", + } + const messages: string[] = [] + for (const err of details) { + const path = Array.isArray(err.path) ? err.path.join(".") : String(err.path ?? "") + const fieldLabel = fieldLabels[path] || path || "Campo" + if (err.code === "invalid_format" && err.format === "email") { + messages.push(`${fieldLabel}: formato de e-mail inválido`) + } else if (err.code === "invalid_format") { + messages.push(`${fieldLabel}: formato inválido`) + } else if (err.code === "too_small" || err.code === "too_short") { + messages.push(`${fieldLabel}: muito curto`) + } else if (err.code === "too_big" || err.code === "too_long") { + messages.push(`${fieldLabel}: muito longo`) + } else if (err.code === "invalid_type") { + messages.push(`${fieldLabel}: valor inválido`) + } else if (err.message) { + messages.push(`${fieldLabel}: ${err.message}`) + } else { + messages.push(`${fieldLabel}: erro de validação`) + } + } + if (messages.length > 0) { + return messages.join("\n") + } + } + } + if (json.error) { + return json.error + } + } catch { + // Não é JSON, retorna o texto original + } + return `Erro no servidor (${statusCode})` +} + function buildRemoteAccessPayload(info: RustdeskInfo | null) { if (!info) return null const payload: Record = { @@ -1038,7 +1086,7 @@ const resolvedAppUrl = useMemo(() => { }) if (!res.ok) { const text = await res.text() - throw new Error(`Falha no registro (${res.status}): ${text.slice(0, 300)}`) + throw new Error(formatApiError(text, res.status)) } const data = (await res.json()) as MachineRegisterResponse @@ -1242,7 +1290,7 @@ const resolvedAppUrl = useMemo(() => { }) if (!res.ok) { const text = await res.text() - throw new Error(`Falha ao enviar inventário (${res.status}): ${text.slice(0, 200)}`) + throw new Error(formatApiError(text, res.status)) } } catch (err) { setError(err instanceof Error ? err.message : String(err)) diff --git a/convex/machines.ts b/convex/machines.ts index 7b6a342..785fd7e 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -1282,6 +1282,7 @@ export const listOpenTickets = query({ type MachineTicketsHistoryFilter = { statusFilter: "all" | "open" | "resolved" priorityFilter: string | null + requesterEmail: string | null from: number | null to: number | null } @@ -1290,6 +1291,7 @@ type ListTicketsHistoryArgs = { machineId: Id<"machines"> status?: "all" | "open" | "resolved" priority?: string + requesterEmail?: string search?: string from?: number to?: number @@ -1300,6 +1302,7 @@ type GetTicketsHistoryStatsArgs = { machineId: Id<"machines"> status?: "all" | "open" | "resolved" priority?: string + requesterEmail?: string search?: string from?: number to?: number @@ -1343,6 +1346,13 @@ function createMachineTicketsQuery( return working } +function matchesRequesterEmail(ticket: Doc<"tickets">, requesterEmail: string | null): boolean { + if (!requesterEmail) return true + const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined + if (!requesterSnapshot?.email) return false + return requesterSnapshot.email.toLowerCase() === requesterEmail.toLowerCase() +} + function matchesTicketSearch(ticket: Doc<"tickets">, searchTerm: string): boolean { const normalized = searchTerm.trim().toLowerCase() if (!normalized) return true @@ -1383,19 +1393,27 @@ export async function listTicketsHistoryHandler(ctx: QueryCtx, args: ListTickets const normalizedStatusFilter = args.status ?? "all" const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null + const normalizedRequesterEmail = args.requesterEmail?.trim().toLowerCase() ?? 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, + requesterEmail: normalizedRequesterEmail, 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 + let page = pageResult.page + if (normalizedRequesterEmail) { + page = page.filter((ticket) => matchesRequesterEmail(ticket, normalizedRequesterEmail)) + } + if (searchTerm) { + page = page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) + } const queueCache = new Map | null>() const items = await Promise.all( page.map(async (ticket) => { @@ -1448,6 +1466,7 @@ export const listTicketsHistory = query({ machineId: v.id("machines"), status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))), priority: v.optional(v.string()), + requesterEmail: v.optional(v.string()), search: v.optional(v.string()), from: v.optional(v.number()), to: v.optional(v.number()), @@ -1467,12 +1486,14 @@ export async function getTicketsHistoryStatsHandler( const normalizedStatusFilter = args.status ?? "all" const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null + const normalizedRequesterEmail = args.requesterEmail?.trim().toLowerCase() ?? 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, + requesterEmail: normalizedRequesterEmail, from, to, } @@ -1487,7 +1508,13 @@ export async function getTicketsHistoryStatsHandler( numItems: MACHINE_TICKETS_STATS_PAGE_SIZE, cursor, }) - const page = searchTerm ? pageResult.page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) : pageResult.page + let page = pageResult.page + if (normalizedRequesterEmail) { + page = page.filter((ticket) => matchesRequesterEmail(ticket, normalizedRequesterEmail)) + } + if (searchTerm) { + page = page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) + } total += page.length for (const ticket of page) { if (OPEN_TICKET_STATUSES.has(normalizeStatus(ticket.status))) { @@ -1513,6 +1540,7 @@ export const getTicketsHistoryStats = query({ machineId: v.id("machines"), status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))), priority: v.optional(v.string()), + requesterEmail: v.optional(v.string()), search: v.optional(v.string()), from: v.optional(v.number()), to: v.optional(v.number()), @@ -1520,6 +1548,44 @@ export const getTicketsHistoryStats = query({ handler: getTicketsHistoryStatsHandler, }) +// Lista os solicitantes unicos que abriram tickets nesta maquina +export const listMachineRequesters = query({ + args: { + machineId: v.id("machines"), + }, + handler: async (ctx, args) => { + const machine = await ctx.db.get(args.machineId) + if (!machine) { + return [] + } + + const tickets = await ctx.db + .query("tickets") + .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", args.machineId)) + .collect() + + const requestersMap = new Map() + for (const ticket of tickets) { + const snapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined + if (snapshot?.email) { + const emailLower = snapshot.email.toLowerCase() + if (!requestersMap.has(emailLower)) { + requestersMap.set(emailLower, { + email: snapshot.email, + name: snapshot.name ?? null, + }) + } + } + } + + return Array.from(requestersMap.values()).sort((a, b) => { + const nameA = a.name ?? a.email + const nameB = b.name ?? b.email + return nameA.localeCompare(nameB) + }) + }, +}) + export async function updatePersonaHandler( ctx: MutationCtx, args: { diff --git a/src/components/admin/devices/device-tickets-history.client.tsx b/src/components/admin/devices/device-tickets-history.client.tsx index ab3278b..cad6928 100644 --- a/src/components/admin/devices/device-tickets-history.client.tsx +++ b/src/components/admin/devices/device-tickets-history.client.tsx @@ -45,6 +45,7 @@ type DeviceTicketsHistoryArgs = { machineId: Id<"machines"> status?: "open" | "resolved" priority?: string + requesterEmail?: string search?: string from?: number to?: number @@ -146,12 +147,15 @@ function getPriorityMeta(priority: TicketPriority | string | null | undefined) { export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { tenantId: string; deviceId: string }) { const [statusFilter, setStatusFilter] = useState<"all" | "open" | "resolved">("all") const [priorityFilter, setPriorityFilter] = useState("ALL") + const [requesterFilter, setRequesterFilter] = useState("ALL") const [periodPreset, setPeriodPreset] = useState("90d") const [customFrom, setCustomFrom] = useState("") const [customTo, setCustomTo] = useState("") const [searchValue, setSearchValue] = useState("") const [debouncedSearch, setDebouncedSearch] = useState("") + const requesters = useQuery(api.devices.listMachineRequesters, { machineId: deviceId as Id<"machines"> }) + useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchValue.trim()) @@ -178,6 +182,9 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { if (priorityFilter !== "ALL") { args.priority = priorityFilter } + if (requesterFilter !== "ALL") { + args.requesterEmail = requesterFilter + } if (debouncedSearch) { args.search = debouncedSearch } @@ -188,7 +195,7 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { args.to = range.to } return args - }, [debouncedSearch, deviceId, priorityFilter, range.from, range.to, statusFilter]) + }, [debouncedSearch, deviceId, priorityFilter, requesterFilter, range.from, range.to, statusFilter]) const { results: tickets, status: paginationStatus, loadMore } = usePaginatedQuery( api.devices.listTicketsHistory, @@ -208,6 +215,7 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { const resetFilters = () => { setStatusFilter("all") setPriorityFilter("ALL") + setRequesterFilter("ALL") setPeriodPreset("90d") setCustomFrom("") setCustomTo("") @@ -271,6 +279,19 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { Baixa +