chore(types): remove explicit any, fix hook deps, and unused vars across admin/api/tickets; tighten zod server schemas; adjust UI types; fix pdf export expression; minor cleanup
This commit is contained in:
parent
0556502685
commit
6ffd6c6392
17 changed files with 104 additions and 59 deletions
|
|
@ -8,7 +8,19 @@ export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
export default async function AdminCompaniesPage() {
|
export default async function AdminCompaniesPage() {
|
||||||
const companiesRaw = await prisma.company.findMany({ orderBy: { name: "asc" } })
|
const companiesRaw = await prisma.company.findMany({ orderBy: { name: "asc" } })
|
||||||
const companies = companiesRaw.map((c: any) => ({ ...c, isAvulso: Boolean(c.isAvulso ?? false) }))
|
const companies = companiesRaw.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
tenantId: c.tenantId,
|
||||||
|
name: c.name,
|
||||||
|
slug: c.slug,
|
||||||
|
isAvulso: Boolean(c.isAvulso ?? false),
|
||||||
|
contractedHoursPerMonth: c.contractedHoursPerMonth ?? null,
|
||||||
|
cnpj: c.cnpj ?? null,
|
||||||
|
domain: c.domain ?? null,
|
||||||
|
phone: c.phone ?? null,
|
||||||
|
description: c.description ?? null,
|
||||||
|
address: c.address ?? null,
|
||||||
|
}))
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
header={
|
header={
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { Prisma } from "@prisma/client"
|
||||||
import { assertAdminSession } from "@/lib/auth-server"
|
import { assertAdminSession } from "@/lib/auth-server"
|
||||||
|
|
||||||
export const runtime = "nodejs"
|
export const runtime = "nodejs"
|
||||||
|
|
@ -9,20 +10,34 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
||||||
const session = await assertAdminSession()
|
const session = await assertAdminSession()
|
||||||
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const body = await request.json()
|
const raw = (await request.json()) as Partial<{
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
cnpj: string | null
|
||||||
|
domain: string | null
|
||||||
|
phone: string | null
|
||||||
|
description: string | null
|
||||||
|
address: string | null
|
||||||
|
isAvulso: boolean
|
||||||
|
contractedHoursPerMonth: number | string | null
|
||||||
|
}>
|
||||||
|
|
||||||
const updates: Record<string, any> = {}
|
const updates: Prisma.CompanyUpdateInput = {}
|
||||||
for (const key of ["name", "slug", "cnpj", "domain", "phone", "description", "address"]) {
|
if (typeof raw.name === "string" && raw.name.trim()) updates.name = raw.name.trim()
|
||||||
if (key in body) updates[key] = body[key] ?? null
|
if (typeof raw.slug === "string" && raw.slug.trim()) updates.slug = raw.slug.trim()
|
||||||
}
|
if ("cnpj" in raw) updates.cnpj = raw.cnpj ?? null
|
||||||
if ("isAvulso" in body) updates.isAvulso = Boolean(body.isAvulso)
|
if ("domain" in raw) updates.domain = raw.domain ?? null
|
||||||
if ("contractedHoursPerMonth" in body) {
|
if ("phone" in raw) updates.phone = raw.phone ?? null
|
||||||
const raw = body.contractedHoursPerMonth
|
if ("description" in raw) updates.description = raw.description ?? null
|
||||||
updates.contractedHoursPerMonth = typeof raw === "number" ? raw : raw ? Number(raw) : null
|
if ("address" in raw) updates.address = raw.address ?? null
|
||||||
|
if ("isAvulso" in raw) updates.isAvulso = Boolean(raw.isAvulso)
|
||||||
|
if ("contractedHoursPerMonth" in raw) {
|
||||||
|
const v = raw.contractedHoursPerMonth
|
||||||
|
updates.contractedHoursPerMonth = typeof v === "number" ? v : v ? Number(v) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const company = await prisma.company.update({ where: { id }, data: updates as any })
|
const company = await prisma.company.update({ where: { id }, data: updates })
|
||||||
return NextResponse.json({ company })
|
return NextResponse.json({ company })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update company", error)
|
console.error("Failed to update company", error)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,17 @@ export async function POST(request: Request) {
|
||||||
const session = await assertAdminSession()
|
const session = await assertAdminSession()
|
||||||
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||||
|
|
||||||
const body = await request.json()
|
const body = (await request.json()) as Partial<{
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
isAvulso: boolean
|
||||||
|
cnpj: string | null
|
||||||
|
domain: string | null
|
||||||
|
phone: string | null
|
||||||
|
description: string | null
|
||||||
|
address: string | null
|
||||||
|
contractedHoursPerMonth: number | string | null
|
||||||
|
}>
|
||||||
const { name, slug, isAvulso, cnpj, domain, phone, description, address, contractedHoursPerMonth } = body ?? {}
|
const { name, slug, isAvulso, cnpj, domain, phone, description, address, contractedHoursPerMonth } = body ?? {}
|
||||||
if (!name || !slug) {
|
if (!name || !slug) {
|
||||||
return NextResponse.json({ error: "Nome e slug são obrigatórios" }, { status: 400 })
|
return NextResponse.json({ error: "Nome e slug são obrigatórios" }, { status: 400 })
|
||||||
|
|
@ -27,7 +37,7 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const company = await prisma.company.create({
|
const company = await prisma.company.create({
|
||||||
data: ({
|
data: {
|
||||||
tenantId: session.user.tenantId ?? "tenant-atlas",
|
tenantId: session.user.tenantId ?? "tenant-atlas",
|
||||||
name: String(name),
|
name: String(name),
|
||||||
slug: String(slug),
|
slug: String(slug),
|
||||||
|
|
@ -38,7 +48,7 @@ export async function POST(request: Request) {
|
||||||
phone: phone ? String(phone) : null,
|
phone: phone ? String(phone) : null,
|
||||||
description: description ? String(description) : null,
|
description: description ? String(description) : null,
|
||||||
address: address ? String(address) : null,
|
address: address ? String(address) : null,
|
||||||
} as any),
|
},
|
||||||
})
|
})
|
||||||
return NextResponse.json({ company })
|
return NextResponse.json({ company })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { NextResponse } from "next/server"
|
||||||
import { ConvexHttpClient } from "convex/browser"
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
|
|
||||||
import { assertAdminSession } from "@/lib/auth-server"
|
import { assertAdminSession } from "@/lib/auth-server"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
|
|
||||||
export const runtime = "nodejs"
|
export const runtime = "nodejs"
|
||||||
|
|
@ -10,7 +11,7 @@ export async function POST(request: Request) {
|
||||||
const session = await assertAdminSession()
|
const session = await assertAdminSession()
|
||||||
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||||
|
|
||||||
const body = await request.json().catch(() => null) as { email?: string; companyId?: string }
|
const body = (await request.json().catch(() => null)) as { email?: string; companyId?: string }
|
||||||
const email = body?.email?.trim().toLowerCase()
|
const email = body?.email?.trim().toLowerCase()
|
||||||
const companyId = body?.companyId
|
const companyId = body?.companyId
|
||||||
if (!email || !companyId) {
|
if (!email || !companyId) {
|
||||||
|
|
@ -25,8 +26,8 @@ export async function POST(request: Request) {
|
||||||
await client.mutation(api.users.assignCompany, {
|
await client.mutation(api.users.assignCompany, {
|
||||||
tenantId: session.user.tenantId ?? "tenant-atlas",
|
tenantId: session.user.tenantId ?? "tenant-atlas",
|
||||||
email,
|
email,
|
||||||
companyId: companyId as any,
|
companyId: companyId as Id<"companies">,
|
||||||
actorId: (session.user as any).convexUserId ?? (session.user.id as any),
|
actorId: (session.user as unknown as { convexUserId?: Id<"users">; id?: Id<"users"> }).convexUserId ?? (session.user.id as unknown as Id<"users">),
|
||||||
})
|
})
|
||||||
return NextResponse.json({ ok: true })
|
return NextResponse.json({ ok: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -34,4 +35,3 @@ export async function POST(request: Request) {
|
||||||
return NextResponse.json({ error: "Falha ao vincular usuário" }, { status: 500 })
|
return NextResponse.json({ error: "Falha ao vincular usuário" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export async function GET(request: Request) {
|
||||||
role: session.user.role.toUpperCase(),
|
role: session.user.role.toUpperCase(),
|
||||||
})
|
})
|
||||||
viewerId = ensuredUser?._id ?? null
|
viewerId = ensuredUser?._id ?? null
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 })
|
return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 })
|
||||||
}
|
}
|
||||||
if (!viewerId) return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 })
|
if (!viewerId) return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 })
|
||||||
|
|
@ -75,7 +75,7 @@ export async function GET(request: Request) {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
return NextResponse.json({ error: "Falha ao gerar CSV de horas por cliente" }, { status: 500 })
|
return NextResponse.json({ error: "Falha ao gerar CSV de horas por cliente" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,11 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st
|
||||||
const badgeTextWidth = doc.widthOfString(statusText)
|
const badgeTextWidth = doc.widthOfString(statusText)
|
||||||
const badgeHeight = badgeFontSize + badgePaddingY * 2
|
const badgeHeight = badgeFontSize + badgePaddingY * 2
|
||||||
const badgeWidth = badgeTextWidth + badgePaddingX * 2
|
const badgeWidth = badgeTextWidth + badgePaddingX * 2
|
||||||
D.roundedRect?.(badgeX, badgeY, badgeWidth, badgeHeight, 4) ?? doc.rect(badgeX, badgeY, badgeWidth, badgeHeight)
|
if (typeof D.roundedRect === "function") {
|
||||||
|
D.roundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 4)
|
||||||
|
} else {
|
||||||
|
doc.rect(badgeX, badgeY, badgeWidth, badgeHeight)
|
||||||
|
}
|
||||||
doc.fill(badgeColor)
|
doc.fill(badgeColor)
|
||||||
doc.fillColor("#FFFFFF").text(statusText, badgeX + badgePaddingX, badgeY + badgePaddingY)
|
doc.fillColor("#FFFFFF").text(statusText, badgeX + badgePaddingX, badgeY + badgePaddingY)
|
||||||
doc.restore()
|
doc.restore()
|
||||||
|
|
@ -316,7 +320,7 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st
|
||||||
const colGap = 24
|
const colGap = 24
|
||||||
const colWidth = (doc.page.width - doc.page.margins.left - doc.page.margins.right - colGap) / 2
|
const colWidth = (doc.page.width - doc.page.margins.left - doc.page.margins.right - colGap) / 2
|
||||||
const rightX = leftX + colWidth + colGap
|
const rightX = leftX + colWidth + colGap
|
||||||
const startY = doc.y
|
// const startY = doc.y
|
||||||
const drawMeta = (x: number, lines: string[]) => {
|
const drawMeta = (x: number, lines: string[]) => {
|
||||||
doc.save()
|
doc.save()
|
||||||
doc.x = x
|
doc.x = x
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState, useTransition } from "react"
|
import { useEffect, useMemo, useState, useTransition } from "react"
|
||||||
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
|
@ -102,12 +102,12 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
const normalizedRoles = useMemo(() => roleOptions ?? ROLE_OPTIONS, [roleOptions])
|
const normalizedRoles = useMemo(() => roleOptions ?? ROLE_OPTIONS, [roleOptions])
|
||||||
|
|
||||||
// load companies for association
|
// load companies for association
|
||||||
useMemo(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/api/admin/companies", { credentials: "include" })
|
const r = await fetch("/api/admin/companies", { credentials: "include" })
|
||||||
const j = await r.json()
|
const j = (await r.json()) as { companies?: Array<{ id: string; name: string }> }
|
||||||
const items = (j.companies ?? []).map((c: any) => ({ id: c.id as string, name: c.name as string }))
|
const items = (j.companies ?? []).map((c) => ({ id: c.id, name: c.name }))
|
||||||
setCompanies(items)
|
setCompanies(items)
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState, useTransition } from "react"
|
import { useEffect, useMemo, useState, useTransition } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
@ -64,7 +64,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useMemo(() => { void loadLastAlerts(companies) }, [])
|
useEffect(() => { void loadLastAlerts(companies) }, [companies])
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ type MachineInventory = {
|
||||||
// Dados enviados pelo agente desktop (inventário básico/estendido)
|
// Dados enviados pelo agente desktop (inventário básico/estendido)
|
||||||
disks?: Array<{ name?: string; mountPoint?: string; fs?: string; totalBytes?: number; availableBytes?: number }>
|
disks?: Array<{ name?: string; mountPoint?: string; fs?: string; totalBytes?: number; availableBytes?: number }>
|
||||||
extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended }
|
extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended }
|
||||||
|
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachinesQueryItem = {
|
type MachinesQueryItem = {
|
||||||
|
|
@ -352,8 +353,8 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
|
||||||
{Array.isArray(machine.inventory?.disks) ? (
|
{Array.isArray(machine.inventory?.disks) ? (
|
||||||
<Badge variant="outline">{machine.inventory?.disks?.length ?? 0} discos</Badge>
|
<Badge variant="outline">{machine.inventory?.disks?.length ?? 0} discos</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{Array.isArray((machine.inventory as any)?.services) ? (
|
{Array.isArray(machine.inventory?.services) ? (
|
||||||
<Badge variant="outline">{(machine.inventory as any).services.length} serviços</Badge>
|
<Badge variant="outline">{machine.inventory?.services?.length ?? 0} serviços</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -617,7 +618,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(network as any[]).map((iface, idx) => (
|
{(network as Array<{ name?: string; mac?: string; ip?: string }>).map((iface, idx) => (
|
||||||
<TableRow key={`iface-${idx}`} className="border-slate-100">
|
<TableRow key={`iface-${idx}`} className="border-slate-100">
|
||||||
<TableCell className="text-sm">{iface?.name ?? "—"}</TableCell>
|
<TableCell className="text-sm">{iface?.name ?? "—"}</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">{iface?.mac ?? "—"}</TableCell>
|
<TableCell className="text-sm text-muted-foreground">{iface?.mac ?? "—"}</TableCell>
|
||||||
|
|
@ -711,7 +712,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
<div className="rounded-md border border-slate-200 bg-emerald-50/40 p-3 dark:bg-emerald-900/10">
|
<div className="rounded-md border border-slate-200 bg-emerald-50/40 p-3 dark:bg-emerald-900/10">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">SMART</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">SMART</p>
|
||||||
<div className="mt-2 grid gap-2">
|
<div className="mt-2 grid gap-2">
|
||||||
{linuxExt.smart.map((s: any, idx: number) => {
|
{linuxExt.smart.map((s: { smart_status?: { passed?: boolean }; model_name?: string; model_family?: string; serial_number?: string; device?: { name?: string } }, idx: number) => {
|
||||||
const ok = s?.smart_status?.passed !== false
|
const ok = s?.smart_status?.passed !== false
|
||||||
const model = s?.model_name ?? s?.model_family ?? "Disco"
|
const model = s?.model_name ?? s?.model_family ?? "Disco"
|
||||||
const serial = s?.serial_number ?? s?.device?.name ?? "—"
|
const serial = s?.serial_number ?? s?.device?.name ?? "—"
|
||||||
|
|
@ -758,7 +759,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(windowsExt.services as any[]).slice(0, 10).map((svc: any, i: number) => (
|
{(windowsExt.services as Array<{ Name?: string; DisplayName?: string; Status?: string }>).slice(0, 10).map((svc, i: number) => (
|
||||||
<TableRow key={`svc-${i}`} className="border-slate-100">
|
<TableRow key={`svc-${i}`} className="border-slate-100">
|
||||||
<TableCell className="text-sm">{svc?.Name ?? "—"}</TableCell>
|
<TableCell className="text-sm">{svc?.Name ?? "—"}</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">{svc?.DisplayName ?? "—"}</TableCell>
|
<TableCell className="text-sm text-muted-foreground">{svc?.DisplayName ?? "—"}</TableCell>
|
||||||
|
|
@ -775,7 +776,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">Softwares (amostra)</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">Softwares (amostra)</p>
|
||||||
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||||
{(windowsExt.software as any[]).slice(0, 8).map((s: any, i: number) => (
|
{(windowsExt.software as Array<{ DisplayName?: string; name?: string; DisplayVersion?: string; Publisher?: string }>).slice(0, 8).map((s, i: number) => (
|
||||||
<li key={`sw-${i}`}>
|
<li key={`sw-${i}`}>
|
||||||
<span className="font-medium text-foreground">{s?.DisplayName ?? s?.name ?? "—"}</span>
|
<span className="font-medium text-foreground">{s?.DisplayName ?? s?.name ?? "—"}</span>
|
||||||
{s?.DisplayVersion ? <span className="ml-1">{s.DisplayVersion}</span> : null}
|
{s?.DisplayVersion ? <span className="ml-1">{s.DisplayVersion}</span> : null}
|
||||||
|
|
@ -823,7 +824,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h4 className="text-sm font-semibold">Alertas de postura</h4>
|
<h4 className="text-sm font-semibold">Alertas de postura</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{machine?.postureAlerts?.map((a: any, i: number) => (
|
{machine?.postureAlerts?.map((a: { kind?: string; message?: string; severity?: string }, i: number) => (
|
||||||
<div key={`alert-${i}`} className={cn("flex items-center justify-between rounded-md border px-3 py-2 text-sm",
|
<div key={`alert-${i}`} className={cn("flex items-center justify-between rounded-md border px-3 py-2 text-sm",
|
||||||
(a?.severity ?? "warning").toLowerCase() === "critical" ? "border-rose-500/20 bg-rose-500/10" : "border-amber-500/20 bg-amber-500/10")
|
(a?.severity ?? "warning").toLowerCase() === "critical" ? "border-rose-500/20 bg-rose-500/10" : "border-amber-500/20 bg-amber-500/10")
|
||||||
}>
|
}>
|
||||||
|
|
@ -844,8 +845,8 @@ function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
{Array.isArray(software) && software.length > 0 ? (
|
{Array.isArray(software) && software.length > 0 ? (
|
||||||
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{Array.isArray((metadata as any)?.services) && (metadata as any).services.length > 0 ? (
|
{Array.isArray(metadata?.services) && metadata.services.length > 0 ? (
|
||||||
<Button size="sm" variant="outline" onClick={() => exportCsv((metadata as any).services, "servicos.csv")}>Serviços CSV</Button>
|
<Button size="sm" variant="outline" onClick={() => exportCsv(metadata.services as Array<Record<string, unknown>>, "servicos.csv")}>Serviços CSV</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{fleet ? (
|
{fleet ? (
|
||||||
|
|
@ -979,7 +980,7 @@ function exportCsv(items: Array<Record<string, unknown>>, filename: string) {
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
const row = headers
|
const row = headers
|
||||||
.map((h) => {
|
.map((h) => {
|
||||||
const v = (it as any)?.[h]
|
const v = (it as Record<string, unknown>)[h]
|
||||||
if (v === undefined || v === null) return ""
|
if (v === undefined || v === null) return ""
|
||||||
const s = typeof v === "string" ? v : JSON.stringify(v)
|
const s = typeof v === "string" ? v : JSON.stringify(v)
|
||||||
return `"${s.replace(/"/g, '""')}"`
|
return `"${s.replace(/"/g, '""')}"`
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ function BacklogPriorityPie() {
|
||||||
const chartData = keys
|
const chartData = keys
|
||||||
.map((k) => ({ name: PRIORITY_LABELS[k] ?? k, value: data.priorityCounts?.[k] ?? 0, fill: fills[k as keyof typeof fills] }))
|
.map((k) => ({ name: PRIORITY_LABELS[k] ?? k, value: data.priorityCounts?.[k] ?? 0, fill: fills[k as keyof typeof fills] }))
|
||||||
.filter((d) => d.value > 0)
|
.filter((d) => d.value > 0)
|
||||||
const chartConfig: any = { value: { label: "Tickets" } }
|
const chartConfig: Record<string, { label: string }> = { value: { label: "Tickets" } }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col">
|
<Card className="flex flex-col">
|
||||||
|
|
@ -118,7 +118,7 @@ function QueuesOpenBar() {
|
||||||
|
|
||||||
if (!data) return <Skeleton className="h-[300px] w-full" />
|
if (!data) return <Skeleton className="h-[300px] w-full" />
|
||||||
const chartData = (data.queueBreakdown ?? []).map((q) => ({ queue: q.name, open: q.open }))
|
const chartData = (data.queueBreakdown ?? []).map((q) => ({ queue: q.name, open: q.open }))
|
||||||
const chartConfig: any = { open: { label: "Abertos" } }
|
const chartConfig: Record<string, { label: string }> = { open: { label: "Abertos" } }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,15 @@ export function HoursReport() {
|
||||||
convexUserId ? { tenantId, viewerId: convexUserId as Id<"users">, range: timeRange } : "skip"
|
convexUserId ? { tenantId, viewerId: convexUserId as Id<"users">, range: timeRange } : "skip"
|
||||||
) as { rangeDays: number; items: HoursItem[] } | undefined
|
) as { rangeDays: number; items: HoursItem[] } | undefined
|
||||||
|
|
||||||
const items = data?.items ?? []
|
|
||||||
const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined
|
const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
|
const items = data?.items ?? []
|
||||||
const q = query.trim().toLowerCase()
|
const q = query.trim().toLowerCase()
|
||||||
let list = items
|
let list = items
|
||||||
if (companyId !== "all") list = list.filter((it) => String(it.companyId) === companyId)
|
if (companyId !== "all") list = list.filter((it) => String(it.companyId) === companyId)
|
||||||
if (q) list = list.filter((it) => it.name.toLowerCase().includes(q))
|
if (q) list = list.filter((it) => it.name.toLowerCase().includes(q))
|
||||||
return list
|
return list
|
||||||
}, [items, query, companyId])
|
}, [data?.items, query, companyId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ const submitButtonClass =
|
||||||
export function TicketComments({ ticket }: TicketCommentsProps) {
|
export function TicketComments({ ticket }: TicketCommentsProps) {
|
||||||
const { convexUserId, isStaff, role } = useAuth()
|
const { convexUserId, isStaff, role } = useAuth()
|
||||||
const normalizedRole = role ?? null
|
const normalizedRole = role ?? null
|
||||||
const isManager = normalizedRole === "manager"
|
|
||||||
const canSeeInternalComments = normalizedRole === "admin" || normalizedRole === "agent"
|
const canSeeInternalComments = normalizedRole === "admin" || normalizedRole === "agent"
|
||||||
const addComment = useMutation(api.tickets.addComment)
|
const addComment = useMutation(api.tickets.addComment)
|
||||||
const removeAttachment = useMutation(api.tickets.removeCommentAttachment)
|
const removeAttachment = useMutation(api.tickets.removeCommentAttachment)
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId
|
return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId
|
||||||
}, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId])
|
}, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId])
|
||||||
const currentQueueName = ticket.queue ?? ""
|
const currentQueueName = ticket.queue ?? ""
|
||||||
const isAvulso = Boolean((ticket as any).company?.isAvulso ?? false)
|
const isAvulso = Boolean(((ticket.company ?? null) as { isAvulso?: boolean } | null)?.isAvulso ?? false)
|
||||||
const [queueSelection, setQueueSelection] = useState(currentQueueName)
|
const [queueSelection, setQueueSelection] = useState(currentQueueName)
|
||||||
const queueDirty = useMemo(() => queueSelection !== currentQueueName, [queueSelection, currentQueueName])
|
const queueDirty = useMemo(() => queueSelection !== currentQueueName, [queueSelection, currentQueueName])
|
||||||
const formDirty = dirty || categoryDirty || queueDirty
|
const formDirty = dirty || categoryDirty || queueDirty
|
||||||
|
|
@ -307,9 +307,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
ticket.id,
|
ticket.id,
|
||||||
|
ticket.workSummary,
|
||||||
ticket.workSummary?.totalWorkedMs,
|
ticket.workSummary?.totalWorkedMs,
|
||||||
ticket.workSummary?.internalWorkedMs,
|
ticket.workSummary?.internalWorkedMs,
|
||||||
ticket.workSummary?.externalWorkedMs,
|
ticket.workSummary?.externalWorkedMs,
|
||||||
|
ticketActiveSession,
|
||||||
ticketActiveSession?.id,
|
ticketActiveSession?.id,
|
||||||
ticketActiveSessionStartedAtMs,
|
ticketActiveSessionStartedAtMs,
|
||||||
ticketActiveSessionWorkType,
|
ticketActiveSessionWorkType,
|
||||||
|
|
@ -352,7 +354,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
setNow(Date.now())
|
setNow(Date.now())
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [workSummary?.activeSession?.id])
|
}, [workSummary?.activeSession, workSummary?.activeSession?.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pauseDialogOpen) {
|
if (!pauseDialogOpen) {
|
||||||
|
|
@ -382,7 +384,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
toast.dismiss("work")
|
toast.dismiss("work")
|
||||||
toast.loading("Iniciando atendimento...", { id: "work" })
|
toast.loading("Iniciando atendimento...", { id: "work" })
|
||||||
try {
|
try {
|
||||||
const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType } as any)
|
const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType })
|
||||||
if (result?.status === "already_started") {
|
if (result?.status === "already_started") {
|
||||||
toast.info("O atendimento já estava em andamento", { id: "work" })
|
toast.info("O atendimento já estava em andamento", { id: "work" })
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { format, formatDistanceToNow, formatDistanceToNowStrict } from "date-fns"
|
import { format, formatDistanceToNowStrict } from "date-fns"
|
||||||
import { ptBR } from "date-fns/locale"
|
import { ptBR } from "date-fns/locale"
|
||||||
import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react"
|
import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react"
|
||||||
|
|
||||||
|
|
@ -235,8 +235,8 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={`${cellClass} hidden lg:table-cell pl-8`}>
|
<TableCell className={`${cellClass} hidden lg:table-cell pl-8`}>
|
||||||
<span className="max-w-[160px] truncate text-sm text-neutral-800" title={(ticket as any).company?.name ?? "—"}>
|
<span className="max-w-[160px] truncate text-sm text-neutral-800" title={((ticket.company ?? null) as { name?: string } | null)?.name ?? "—"}>
|
||||||
{(ticket as any).company?.name ?? "—"}
|
{((ticket.company ?? null) as { name?: string } | null)?.name ?? "—"}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={`${cellClass} hidden md:table-cell pl-1 pr-8`}>
|
<TableCell className={`${cellClass} hidden md:table-cell pl-1 pr-8`}>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export function TicketsView() {
|
||||||
const companies = useMemo(() => {
|
const companies = useMemo(() => {
|
||||||
const set = new Set<string>()
|
const set = new Set<string>()
|
||||||
for (const t of tickets) {
|
for (const t of tickets) {
|
||||||
const name = (t as any).company?.name as string | undefined
|
const name = ((t as unknown as { company?: { name?: string } })?.company?.name) as string | undefined
|
||||||
if (name) set.add(name)
|
if (name) set.add(name)
|
||||||
}
|
}
|
||||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR"))
|
return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR"))
|
||||||
|
|
@ -64,11 +64,11 @@ export function TicketsView() {
|
||||||
working = working.filter((t) => t.queue === filters.queue)
|
working = working.filter((t) => t.queue === filters.queue)
|
||||||
}
|
}
|
||||||
if (filters.company) {
|
if (filters.company) {
|
||||||
working = working.filter((t) => ((t as any).company?.name ?? null) === filters.company)
|
working = working.filter((t) => (((t as unknown as { company?: { name?: string } })?.company?.name) ?? null) === filters.company)
|
||||||
}
|
}
|
||||||
|
|
||||||
return working
|
return working
|
||||||
}, [tickets, filters.queue, filters.status, filters.view])
|
}, [tickets, filters.queue, filters.status, filters.view, filters.company])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ const serverTicketSchema = z.object({
|
||||||
workSummary: z
|
workSummary: z
|
||||||
.object({
|
.object({
|
||||||
totalWorkedMs: z.number(),
|
totalWorkedMs: z.number(),
|
||||||
|
internalWorkedMs: z.number().optional(),
|
||||||
|
externalWorkedMs: z.number().optional(),
|
||||||
activeSession: z
|
activeSession: z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|
@ -117,6 +119,10 @@ const serverTicketWithDetailsSchema = serverTicketSchema.extend({
|
||||||
customFields: z.record(z.string(), serverCustomFieldValueSchema).optional(),
|
customFields: z.record(z.string(), serverCustomFieldValueSchema).optional(),
|
||||||
timeline: z.array(serverEventSchema),
|
timeline: z.array(serverEventSchema),
|
||||||
comments: z.array(serverCommentSchema),
|
comments: z.array(serverCommentSchema),
|
||||||
|
company: z
|
||||||
|
.object({ id: z.string(), name: z.string(), isAvulso: z.boolean().optional() })
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function mapTicketFromServer(input: unknown) {
|
export function mapTicketFromServer(input: unknown) {
|
||||||
|
|
@ -135,8 +141,8 @@ export function mapTicketFromServer(input: unknown) {
|
||||||
workSummary: s.workSummary
|
workSummary: s.workSummary
|
||||||
? {
|
? {
|
||||||
totalWorkedMs: s.workSummary.totalWorkedMs,
|
totalWorkedMs: s.workSummary.totalWorkedMs,
|
||||||
internalWorkedMs: (s.workSummary as any).internalWorkedMs ?? 0,
|
internalWorkedMs: s.workSummary.internalWorkedMs ?? 0,
|
||||||
externalWorkedMs: (s.workSummary as any).externalWorkedMs ?? 0,
|
externalWorkedMs: s.workSummary.externalWorkedMs ?? 0,
|
||||||
activeSession: s.workSummary.activeSession
|
activeSession: s.workSummary.activeSession
|
||||||
? {
|
? {
|
||||||
...s.workSummary.activeSession,
|
...s.workSummary.activeSession,
|
||||||
|
|
@ -185,9 +191,7 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
|
||||||
dueAt: s.dueAt ? new Date(s.dueAt) : null,
|
dueAt: s.dueAt ? new Date(s.dueAt) : null,
|
||||||
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
||||||
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null,
|
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null,
|
||||||
company: (s as any).company
|
company: s.company ? { id: s.company.id, name: s.company.name, isAvulso: s.company.isAvulso ?? false } : undefined,
|
||||||
? ({ id: (s as any).company.id, name: (s as any).company.name, isAvulso: (s as any).company.isAvulso } as any)
|
|
||||||
: undefined,
|
|
||||||
timeline: s.timeline.map((e) => ({ ...e, createdAt: new Date(e.createdAt) })),
|
timeline: s.timeline.map((e) => ({ ...e, createdAt: new Date(e.createdAt) })),
|
||||||
comments: s.comments.map((c) => ({
|
comments: s.comments.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ export function usePersistentCompanyFilter(initial: string = "all") {
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const update = (value: string) => {
|
const update = (value: string) => {
|
||||||
|
|
@ -28,4 +27,3 @@ export function usePersistentCompanyFilter(initial: string = "all") {
|
||||||
|
|
||||||
return [companyId, update] as const
|
return [companyId, update] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue