feat: export reports as xlsx and add machine inventory

This commit is contained in:
Esdras Renan 2025-10-27 18:00:28 -03:00
parent 29b865885c
commit 714b199879
34 changed files with 2304 additions and 245 deletions

View file

@ -1,6 +1,6 @@
import { NextResponse } from "next/server"
import type { Id } from "@/convex/_generated/dataModel"
import type { UserRole } from "@prisma/client"
import type { Prisma, UserRole } from "@prisma/client"
import { api } from "@/convex/_generated/api"
import { ConvexHttpClient } from "convex/browser"
import { prisma } from "@/lib/prisma"
@ -57,6 +57,9 @@ export async function GET(_: Request, { params }: { params: Promise<{ id: string
select: {
companyId: true,
company: { select: { name: true } },
jobTitle: true,
managerId: true,
manager: { select: { id: true, name: true, email: true } },
},
})
@ -72,6 +75,10 @@ export async function GET(_: Request, { params }: { params: Promise<{ id: string
companyId: domain?.companyId ?? null,
companyName: domain?.company?.name ?? null,
machinePersona: user.machinePersona ?? null,
jobTitle: domain?.jobTitle ?? null,
managerId: domain?.managerId ?? null,
managerName: domain?.manager?.name ?? null,
managerEmail: domain?.manager?.email ?? null,
},
})
}
@ -90,6 +97,8 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
role?: RoleOption
tenantId?: string
companyId?: string | null
jobTitle?: string | null
managerId?: string | null
} | null
if (!payload || typeof payload !== "object") {
@ -106,6 +115,39 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
const nextRole = normalizeRole(payload.role ?? user.role)
const nextTenant = (payload.tenantId ?? user.tenantId ?? DEFAULT_TENANT_ID).trim() || DEFAULT_TENANT_ID
const companyId = payload.companyId ? payload.companyId : null
const hasJobTitleField = Object.prototype.hasOwnProperty.call(payload, "jobTitle")
let jobTitle: string | null | undefined
if (hasJobTitleField) {
if (typeof payload.jobTitle === "string") {
const trimmed = payload.jobTitle.trim()
jobTitle = trimmed.length > 0 ? trimmed : null
} else {
jobTitle = null
}
}
const hasManagerField = Object.prototype.hasOwnProperty.call(payload, "managerId")
let managerIdValue: string | null | undefined
if (hasManagerField) {
if (typeof payload.managerId === "string") {
const trimmed = payload.managerId.trim()
managerIdValue = trimmed.length > 0 ? trimmed : null
} else {
managerIdValue = null
}
}
let managerRecord: { id: string; email: string; tenantId: string; name: string } | null = null
if (managerIdValue && managerIdValue !== null) {
managerRecord = await prisma.user.findUnique({
where: { id: managerIdValue },
select: { id: true, email: true, tenantId: true, name: true },
})
if (!managerRecord) {
return NextResponse.json({ error: "Gestor informado não foi encontrado." }, { status: 400 })
}
if (managerRecord.tenantId !== nextTenant) {
return NextResponse.json({ error: "Gestor pertence a outro cliente." }, { status: 400 })
}
}
if (!nextEmail || !nextEmail.includes("@")) {
return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 })
@ -159,33 +201,58 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
return NextResponse.json({ error: "Empresa não encontrada" }, { status: 400 })
}
if (domainUser && managerRecord && managerRecord.id === domainUser.id) {
return NextResponse.json({ error: "Um usuário não pode ser gestor de si mesmo." }, { status: 400 })
}
if (domainUser) {
const updateData: Prisma.UserUncheckedUpdateInput = {
email: nextEmail,
name: nextName || domainUser.name,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
}
if (hasJobTitleField) {
updateData.jobTitle = jobTitle ?? null
}
if (hasManagerField) {
updateData.managerId = managerRecord?.id ?? null
}
await prisma.user.update({
where: { id: domainUser.id },
data: {
email: nextEmail,
name: nextName || domainUser.name,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
},
data: updateData,
})
} else {
const upsertUpdate: Prisma.UserUncheckedUpdateInput = {
name: nextName || nextEmail,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
}
if (hasJobTitleField) {
upsertUpdate.jobTitle = jobTitle ?? null
}
if (hasManagerField) {
upsertUpdate.managerId = managerRecord?.id ?? null
}
const upsertCreate: Prisma.UserUncheckedCreateInput = {
email: nextEmail,
name: nextName || nextEmail,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
}
if (hasJobTitleField) {
upsertCreate.jobTitle = jobTitle ?? null
}
if (hasManagerField) {
upsertCreate.managerId = managerRecord?.id ?? null
}
await prisma.user.upsert({
where: { email: nextEmail },
update: {
name: nextName || nextEmail,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
},
create: {
email: nextEmail,
name: nextName || nextEmail,
role: mapToUserRole(nextRole),
tenantId: nextTenant,
companyId: companyId ?? null,
},
update: upsertUpdate,
create: upsertCreate,
})
}
@ -193,19 +260,60 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
if (convexUrl) {
try {
const convex = new ConvexHttpClient(convexUrl)
await convex.mutation(api.users.ensureUser, {
let managerConvexId: Id<"users"> | undefined
if (hasManagerField && managerRecord?.email) {
try {
const managerUser = await convex.query(api.users.findByEmail, {
tenantId: nextTenant,
email: managerRecord.email,
})
if (managerUser?._id) {
managerConvexId = managerUser._id as Id<"users">
}
} catch (error) {
console.warn("Falha ao localizar gestor no Convex", error)
}
}
const ensurePayload: {
tenantId: string
email: string
name: string
avatarUrl?: string
role: string
companyId?: Id<"companies">
jobTitle?: string | undefined
managerId?: Id<"users">
} = {
tenantId: nextTenant,
email: nextEmail,
name: nextName || nextEmail,
avatarUrl: updated.avatarUrl ?? undefined,
role: nextRole.toUpperCase(),
companyId: companyId ? (companyId as Id<"companies">) : undefined,
})
}
if (companyId) {
ensurePayload.companyId = companyId as Id<"companies">
}
if (hasJobTitleField) {
ensurePayload.jobTitle = jobTitle ?? undefined
}
if (hasManagerField) {
ensurePayload.managerId = managerConvexId
}
await convex.mutation(api.users.ensureUser, ensurePayload)
} catch (error) {
console.warn("Falha ao sincronizar usuário no Convex", error)
}
}
const updatedDomain = await prisma.user.findUnique({
where: { email: nextEmail },
select: {
jobTitle: true,
managerId: true,
manager: { select: { name: true, email: true } },
},
})
return NextResponse.json({
user: {
id: updated.id,
@ -218,6 +326,10 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
companyId,
companyName: companyData?.name ?? null,
machinePersona: updated.machinePersona ?? null,
jobTitle: updatedDomain?.jobTitle ?? null,
managerId: updatedDomain?.managerId ?? null,
managerName: updatedDomain?.manager?.name ?? null,
managerEmail: updatedDomain?.manager?.email ?? null,
},
})
}

View file

@ -3,6 +3,7 @@ import { NextResponse } from "next/server"
import { hashPassword } from "better-auth/crypto"
import { ConvexHttpClient } from "convex/browser"
import type { UserRole } from "@prisma/client"
import type { Id } from "@/convex/_generated/dataModel"
import { prisma } from "@/lib/prisma"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
@ -52,6 +53,13 @@ export async function GET() {
name: true,
},
},
manager: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: { createdAt: "desc" },
})
@ -102,6 +110,10 @@ export async function GET() {
role: normalizeRole(user.role),
companyId: user.companyId,
companyName: user.company?.name ?? null,
jobTitle: user.jobTitle ?? null,
managerId: user.managerId,
managerName: user.manager?.name ?? null,
managerEmail: user.manager?.email ?? null,
tenantId: user.tenantId,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
@ -124,6 +136,8 @@ export async function POST(request: Request) {
email?: string
role?: string
tenantId?: string
jobTitle?: string | null
managerId?: string | null
} | null
if (!body || typeof body !== "object") {
@ -141,6 +155,25 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 })
}
const rawJobTitle = typeof body.jobTitle === "string" ? body.jobTitle.trim() : null
const jobTitle = rawJobTitle ? rawJobTitle : null
const rawManagerId = typeof body.managerId === "string" ? body.managerId.trim() : ""
const managerId = rawManagerId.length > 0 ? rawManagerId : null
let managerRecord: { id: string; email: string; tenantId: string; name: string } | null = null
if (managerId) {
managerRecord = await prisma.user.findUnique({
where: { id: managerId },
select: { id: true, email: true, tenantId: true, name: true },
})
if (!managerRecord) {
return NextResponse.json({ error: "Gestor informado não foi encontrado." }, { status: 400 })
}
if (managerRecord.tenantId !== tenantId) {
return NextResponse.json({ error: "Gestor pertence a outro cliente." }, { status: 400 })
}
}
const normalizedRole = normalizeRole(body.role)
const authRole = normalizedRole.toLowerCase()
const userRole = normalizedRole as UserRole
@ -176,12 +209,16 @@ export async function POST(request: Request) {
name,
role: userRole,
tenantId,
jobTitle,
managerId: managerRecord?.id ?? null,
},
create: {
name,
email,
role: userRole,
tenantId,
jobTitle,
managerId: managerRecord?.id ?? null,
},
})
@ -192,12 +229,28 @@ export async function POST(request: Request) {
if (convexUrl) {
try {
const convex = new ConvexHttpClient(convexUrl)
let managerConvexId: Id<"users"> | undefined
if (managerRecord?.email) {
try {
const convexManager = await convex.query(api.users.findByEmail, {
tenantId,
email: managerRecord.email,
})
if (convexManager?._id) {
managerConvexId = convexManager._id as Id<"users">
}
} catch (error) {
console.warn("[admin/users] Falha ao localizar gestor no Convex", error)
}
}
await convex.mutation(api.users.ensureUser, {
tenantId,
email,
name,
avatarUrl: authUser.avatarUrl ?? undefined,
role: userRole,
jobTitle: jobTitle ?? undefined,
managerId: managerConvexId,
})
} catch (error) {
console.error("[admin/users] ensureUser failed", error)
@ -213,6 +266,10 @@ export async function POST(request: Request) {
role: authRole,
tenantId: domainUser.tenantId,
createdAt: domainUser.createdAt.toISOString(),
jobTitle,
managerId: managerRecord?.id ?? null,
managerName: managerRecord?.name ?? null,
managerEmail: managerRecord?.email ?? null,
},
temporaryPassword,
})