diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ae66af..dfefb44 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,42 +4,42 @@ generator client { // and container runtime (Debian bullseye, OpenSSL 1.1.x) binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] } - -datasource db { + +datasource db { provider = "sqlite" - url = env("DATABASE_URL") -} - + url = env("DATABASE_URL") +} + enum UserRole { ADMIN MANAGER AGENT COLLABORATOR } - + enum TicketStatus { PENDING AWAITING_ATTENDANCE PAUSED RESOLVED } - -enum TicketPriority { - LOW - MEDIUM - HIGH - URGENT -} - -enum TicketChannel { - EMAIL - WHATSAPP - CHAT - PHONE - API - MANUAL -} - + +enum TicketPriority { + LOW + MEDIUM + HIGH + URGENT +} + +enum TicketChannel { + EMAIL + WHATSAPP + CHAT + PHONE + API + MANUAL +} + enum CommentVisibility { PUBLIC INTERNAL @@ -56,66 +56,66 @@ model Team { tenantId String name String description String? - members TeamMember[] - queues Queue[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([tenantId, name]) -} - -model TeamMember { - teamId String - userId String - isLead Boolean @default(false) - assignedAt DateTime @default(now()) - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@id([teamId, userId]) -} - + members TeamMember[] + queues Queue[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([tenantId, name]) +} + +model TeamMember { + teamId String + userId String + isLead Boolean @default(false) + assignedAt DateTime @default(now()) + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([teamId, userId]) +} + model Company { - id String @id @default(cuid()) - tenantId String - name String - slug String - provisioningCode String @unique - isAvulso Boolean @default(false) + id String @id @default(cuid()) + tenantId String + name String + slug String + provisioningCode String @unique + isAvulso Boolean @default(false) contractedHoursPerMonth Float? - cnpj String? - domain String? - phone String? - description String? - address String? - legalName String? - tradeName String? - stateRegistration String? - stateRegistrationType CompanyStateRegistrationType? - primaryCnae String? - timezone String? - businessHours Json? - supportEmail String? - billingEmail String? - contactPreferences Json? - clientDomains Json? - communicationChannels Json? - fiscalAddress Json? - hasBranches Boolean @default(false) - regulatedEnvironments Json? - privacyPolicyAccepted Boolean @default(false) - privacyPolicyReference String? - privacyPolicyMetadata Json? - contacts Json? - locations Json? - contracts Json? - sla Json? - tags Json? - customFields Json? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + cnpj String? + domain String? + phone String? + description String? + address String? + legalName String? + tradeName String? + stateRegistration String? + stateRegistrationType CompanyStateRegistrationType? + primaryCnae String? + timezone String? + businessHours Json? + supportEmail String? + billingEmail String? + contactPreferences Json? + clientDomains Json? + communicationChannels Json? + fiscalAddress Json? + hasBranches Boolean @default(false) + regulatedEnvironments Json? + privacyPolicyAccepted Boolean @default(false) + privacyPolicyReference String? + privacyPolicyMetadata Json? + contacts Json? + locations Json? + contracts Json? + sla Json? + tags Json? + customFields Json? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt users User[] tickets Ticket[] @@ -124,121 +124,121 @@ model Company { @@index([tenantId, name]) } -model User { - id String @id @default(cuid()) - tenantId String - name String - email String @unique - role UserRole - timezone String @default("America/Sao_Paulo") - avatarUrl String? - companyId String? - teams TeamMember[] - requestedTickets Ticket[] @relation("TicketRequester") - assignedTickets Ticket[] @relation("TicketAssignee") - comments TicketComment[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - +model User { + id String @id @default(cuid()) + tenantId String + name String + email String @unique + role UserRole + timezone String @default("America/Sao_Paulo") + avatarUrl String? + companyId String? + teams TeamMember[] + requestedTickets Ticket[] @relation("TicketRequester") + assignedTickets Ticket[] @relation("TicketAssignee") + comments TicketComment[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + company Company? @relation(fields: [companyId], references: [id]) - @@index([tenantId, role]) + @@index([tenantId, role]) @@index([tenantId, companyId]) -} - -model Queue { - id String @id @default(cuid()) - tenantId String - name String - slug String - teamId String? - tickets Ticket[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team? @relation(fields: [teamId], references: [id]) - - @@unique([tenantId, slug]) -} - -model Ticket { - id String @id @default(cuid()) - tenantId String - reference Int @default(0) - subject String - summary String? - status TicketStatus @default(PENDING) - priority TicketPriority @default(MEDIUM) - channel TicketChannel @default(EMAIL) - queueId String? - requesterId String - assigneeId String? - slaPolicyId String? - companyId String? - dueAt DateTime? - firstResponseAt DateTime? - resolvedAt DateTime? - closedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - requester User @relation("TicketRequester", fields: [requesterId], references: [id]) - assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id]) - queue Queue? @relation(fields: [queueId], references: [id]) - slaPolicy SlaPolicy? @relation(fields: [slaPolicyId], references: [id]) - company Company? @relation(fields: [companyId], references: [id]) - events TicketEvent[] - comments TicketComment[] - - @@index([tenantId, status]) - @@index([tenantId, queueId]) - @@index([tenantId, assigneeId]) +} + +model Queue { + id String @id @default(cuid()) + tenantId String + name String + slug String + teamId String? + tickets Ticket[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team? @relation(fields: [teamId], references: [id]) + + @@unique([tenantId, slug]) +} + +model Ticket { + id String @id @default(cuid()) + tenantId String + reference Int @default(0) + subject String + summary String? + status TicketStatus @default(PENDING) + priority TicketPriority @default(MEDIUM) + channel TicketChannel @default(EMAIL) + queueId String? + requesterId String + assigneeId String? + slaPolicyId String? + companyId String? + dueAt DateTime? + firstResponseAt DateTime? + resolvedAt DateTime? + closedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + requester User @relation("TicketRequester", fields: [requesterId], references: [id]) + assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id]) + queue Queue? @relation(fields: [queueId], references: [id]) + slaPolicy SlaPolicy? @relation(fields: [slaPolicyId], references: [id]) + company Company? @relation(fields: [companyId], references: [id]) + events TicketEvent[] + comments TicketComment[] + + @@index([tenantId, status]) + @@index([tenantId, queueId]) + @@index([tenantId, assigneeId]) @@index([tenantId, companyId]) -} - -model TicketEvent { - id String @id @default(cuid()) - ticketId String - type String - payload Json - createdAt DateTime @default(now()) - - ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) - - @@index([ticketId, createdAt]) -} - -model TicketComment { - id String @id @default(cuid()) - ticketId String - authorId String - visibility CommentVisibility @default(INTERNAL) - body String - attachments Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) - author User @relation(fields: [authorId], references: [id]) - - @@index([ticketId, visibility]) -} - -model SlaPolicy { - id String @id @default(cuid()) - tenantId String - name String - description String? - timeToFirstResponse Int? - timeToResolution Int? - calendar Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - tickets Ticket[] - - @@unique([tenantId, name]) -} +} + +model TicketEvent { + id String @id @default(cuid()) + ticketId String + type String + payload Json + createdAt DateTime @default(now()) + + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + + @@index([ticketId, createdAt]) +} + +model TicketComment { + id String @id @default(cuid()) + ticketId String + authorId String + visibility CommentVisibility @default(INTERNAL) + body String + attachments Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [id]) + + @@index([ticketId, visibility]) +} + +model SlaPolicy { + id String @id @default(cuid()) + tenantId String + name String + description String? + timeToFirstResponse Int? + timeToResolution Int? + calendar Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tickets Ticket[] + + @@unique([tenantId, name]) +} model AuthUser { id String @id @default(cuid()) @@ -257,14 +257,14 @@ model AuthUser { } model AuthSession { - id String @id @default(cuid()) - userId String - token String @unique - expiresAt DateTime - ipAddress String? - userAgent String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + userId String + token String @unique + expiresAt DateTime + ipAddress String? + userAgent String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt user AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -273,7 +273,7 @@ model AuthSession { } model AuthAccount { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String accountId String providerId String @@ -284,8 +284,8 @@ model AuthAccount { scope String? idToken String? password String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt user AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -294,24 +294,24 @@ model AuthAccount { } model AuthInvite { - id String @id @default(cuid()) - email String - name String? - role String @default("agent") - tenantId String - token String @unique - status String @default("pending") - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - createdById String? - acceptedAt DateTime? - acceptedById String? - revokedAt DateTime? - revokedById String? + id String @id @default(cuid()) + email String + name String? + role String @default("agent") + tenantId String + token String @unique + status String @default("pending") + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdById String? + acceptedAt DateTime? + acceptedById String? + revokedAt DateTime? + revokedById String? revokedReason String? - events AuthInviteEvent[] + events AuthInviteEvent[] @@index([tenantId, status]) @@index([tenantId, email]) diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 309f32c..897384a 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -4,7 +4,7 @@ import { prisma } from "@/lib/prisma" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { requireStaffSession } from "@/lib/auth-server" import { AdminUsersWorkspace, type AdminAccount } from "@/components/admin/users/admin-users-workspace" -import { normalizeCompany } from "@/server/company-service" +import { fetchCompaniesByTenant, normalizeCompany } from "@/server/company-service" export const runtime = "nodejs" export const dynamic = "force-dynamic" @@ -79,10 +79,7 @@ export default async function AdminUsersPage() { } }) - const companiesRaw = await prisma.company.findMany({ - where: { tenantId }, - orderBy: { name: "asc" }, - }) + const companiesRaw = await fetchCompaniesByTenant(tenantId) const companies = companiesRaw.map(normalizeCompany) return ( diff --git a/src/app/api/machines/companies/route.ts b/src/app/api/machines/companies/route.ts index f7531f6..c21252f 100644 --- a/src/app/api/machines/companies/route.ts +++ b/src/app/api/machines/companies/route.ts @@ -7,6 +7,7 @@ import { prisma } from "@/lib/prisma" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { ConvexConfigurationError } from "@/server/convex-client" import { syncConvexCompany } from "@/server/companies-sync" +import { safeCompanyFindMany } from "@/server/company-service" export const runtime = "nodejs" @@ -75,7 +76,7 @@ export async function GET(request: Request) { ...(orFilters.length > 0 ? { OR: orFilters } : {}), } - const companies = await prisma.company.findMany({ + const companies = await safeCompanyFindMany({ where, orderBy: { name: "asc" }, take: 20, diff --git a/src/app/tickets/tickets-page-client.tsx b/src/app/tickets/tickets-page-client.tsx index 713fbe1..e9fd057 100644 --- a/src/app/tickets/tickets-page-client.tsx +++ b/src/app/tickets/tickets-page-client.tsx @@ -36,8 +36,8 @@ export function TicketsPageClient() { } - secondaryAction={Exportar CSV} + primaryAction={Exportar CSV} + secondaryAction={} /> } > diff --git a/src/components/tickets/priority-select.tsx b/src/components/tickets/priority-select.tsx index e4e9f2c..8383ae6 100644 --- a/src/components/tickets/priority-select.tsx +++ b/src/components/tickets/priority-select.tsx @@ -40,10 +40,12 @@ export function PrioritySelect({ ticketId, value, className, + badgeClassName, }: { ticketId: string value: TicketPriority className?: string + badgeClassName?: string }) { const updatePriority = useMutation(api.tickets.updatePriority) const [priority, setPriority] = useState(value) @@ -69,7 +71,9 @@ export function PrioritySelect({ > - + {priorityStyles[priority]?.label ?? priority} diff --git a/src/components/tickets/tickets-board.tsx b/src/components/tickets/tickets-board.tsx index 8ded0bd..349954e 100644 --- a/src/components/tickets/tickets-board.tsx +++ b/src/components/tickets/tickets-board.tsx @@ -10,7 +10,7 @@ import type { Ticket } from "@/lib/schemas/ticket" import { Badge } from "@/components/ui/badge" import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty" import { cn } from "@/lib/utils" -import { TicketPriorityPill } from "@/components/tickets/priority-pill" +import { PrioritySelect } from "@/components/tickets/priority-select" import { getTicketStatusChipClass, getTicketStatusLabel } from "@/lib/ticket-status-style" type TicketsBoardProps = { @@ -57,6 +57,17 @@ function formatUpdated(date: Date | number | string, now: number) { return formatDistanceStrict(timestamp, now, { addSuffix: true, locale: ptBR }) } +function formatQueueLabel(queue?: string | null) { + if (!queue) { + return { label: "Sem fila", title: "Sem fila" } + } + const normalized = queue.trim().toLowerCase() + if (normalized.startsWith("laboratorio")) { + return { label: "Lab", title: queue } + } + return { label: queue, title: queue } +} + export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) { const [now, setNow] = useState(() => Date.now()) @@ -103,73 +114,118 @@ export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) { } return ( -
+
{tickets.map((ticket) => { const isEntering = enteringIds?.has(ticket.id) + const queueDisplay = formatQueueLabel(ticket.queue) + const updatedTimestamp = getTimestamp(ticket.updatedAt) + const updatedAbsoluteLabel = updatedTimestamp + ? new Date(updatedTimestamp).toLocaleString("pt-BR", { + dateStyle: "short", + timeStyle: "short", + }) + : "—" + return ( -
-
+
+
#{ticket.reference} + + {queueDisplay.label} + +
+
{getTicketStatusLabel(ticket.status)} +
event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + +
- - {formatUpdated(ticket.updatedAt, now)} -
-

- {ticket.subject || "Sem assunto"} -

-
- Fila: - - {ticket.queue ?? "Sem fila"} - - Prioridade: - + +
+

+ {ticket.subject || "Sem assunto"} +

-
-
-
Empresa
-
- {ticket.company?.name ?? "Sem empresa"} -
+ +
+
+
+
+ Empresa +
+
+ {ticket.company?.name ?? "Sem empresa"} +
+
+
+
+ Responsável +
+
+ {ticket.assignee?.name ?? "Sem responsável"} +
+
+
+
+ Solicitante +
+
+ {ticket.requester?.name ?? ticket.requester?.email ?? "—"} +
+
+
+
+ Criado em +
+
+ {(() => { + const createdTimestamp = getTimestamp(ticket.createdAt) + return createdTimestamp + ? new Date(createdTimestamp).toLocaleDateString("pt-BR") + : "—" + })()} +
+
+
+ +
+ + Categoria:{" "} + {ticket.category?.name ?? "Sem categoria"} +
-
-
Responsável
-
- {ticket.assignee?.name ?? "Sem responsável"} -
-
-
-
Solicitante
-
- {ticket.requester?.name ?? ticket.requester?.email ?? "—"} -
-
-
+
) })} diff --git a/src/server/company-service.ts b/src/server/company-service.ts index 4ddab88..622278a 100644 --- a/src/server/company-service.ts +++ b/src/server/company-service.ts @@ -72,6 +72,99 @@ const STATE_REGISTRATION_TYPE_FROM_PRISMA: Record< SIMPLES: "simples", } +const COMPANY_LEGACY_SELECT = { + id: true, + tenantId: true, + name: true, + slug: true, + isAvulso: true, + createdAt: true, + updatedAt: true, +} satisfies Prisma.CompanySelect + +type LegacyCompanyRow = Prisma.CompanyGetPayload<{ select: typeof COMPANY_LEGACY_SELECT }> + +function isMissingProvisioningCodeColumn(error: unknown): boolean { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2022") { + const meta = error.meta as Record | undefined + const metaColumn = + typeof meta?.column === "string" + ? meta.column + : typeof meta?.column_name === "string" + ? meta.column_name + : typeof meta?.model === "string" + ? meta.model + : null + if (metaColumn && metaColumn.toLowerCase().includes("provisioningcode")) return true + if (error.message.toLowerCase().includes("provisioningcode")) return true + } + return false +} + +export async function safeCompanyFindMany(args: Prisma.CompanyFindManyArgs): Promise { + try { + return await prisma.company.findMany(args) + } catch (error) { + if (!isMissingProvisioningCodeColumn(error)) { + throw error + } + + if (args.select || args.include) { + throw error + } + + const legacyRows = (await prisma.company.findMany({ + ...args, + select: COMPANY_LEGACY_SELECT, + })) as LegacyCompanyRow[] + + return legacyRows.map( + (row) => + ({ + id: row.id, + tenantId: row.tenantId, + name: row.name, + slug: row.slug, + provisioningCode: "", + isAvulso: Boolean(row.isAvulso), + contractedHoursPerMonth: null, + cnpj: null, + domain: null, + phone: null, + description: null, + address: null, + legalName: null, + tradeName: null, + stateRegistration: null, + stateRegistrationType: null, + primaryCnae: null, + timezone: null, + businessHours: Prisma.JsonNull, + supportEmail: null, + billingEmail: null, + contactPreferences: Prisma.JsonNull, + clientDomains: Prisma.JsonNull, + communicationChannels: Prisma.JsonNull, + fiscalAddress: Prisma.JsonNull, + hasBranches: false, + regulatedEnvironments: Prisma.JsonNull, + privacyPolicyAccepted: false, + privacyPolicyReference: null, + privacyPolicyMetadata: Prisma.JsonNull, + contacts: Prisma.JsonNull, + locations: Prisma.JsonNull, + contracts: Prisma.JsonNull, + sla: Prisma.JsonNull, + tags: Prisma.JsonNull, + customFields: Prisma.JsonNull, + notes: null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }) as Company + ) + } +} + export function formatZodError(error: ZodError) { return error.issues.map((issue) => ({ path: issue.path.join("."), @@ -328,171 +421,25 @@ export function normalizeCompany(company: Company): NormalizedCompany { return { ...payload, id: company.id, - provisioningCode: company.provisioningCode ?? null, + provisioningCode: company.provisioningCode && company.provisioningCode.trim().length > 0 + ? company.provisioningCode + : null, createdAt: company.createdAt.toISOString(), updatedAt: company.updatedAt.toISOString(), } } -type RawCompanyRow = { - id: string - tenantId: string - name: string - slug: string - provisioningCode: string | null - isAvulso: number | boolean | null - contractedHoursPerMonth: number | null - cnpj: string | null - domain: string | null - phone: string | null - description: string | null - address: string | null - legalName: string | null - tradeName: string | null - stateRegistration: string | null - stateRegistrationType: string | null - primaryCnae: string | null - timezone: string | null - businessHours: string | null - supportEmail: string | null - billingEmail: string | null - contactPreferences: string | null - clientDomains: string | null - communicationChannels: string | null - fiscalAddress: string | null - hasBranches: number | null - regulatedEnvironments: string | null - privacyPolicyAccepted: number | null - privacyPolicyReference: string | null - privacyPolicyMetadata: string | null - contacts: string | null - locations: string | null - contracts: string | null - sla: string | null - tags: string | null - customFields: string | null - notes: string | null - createdAt: string - updatedAt: string -} - -function parseJsonValue(value: string | null): Prisma.JsonValue | null { - if (value === null || value === undefined) return null - const trimmed = value.trim() - if (!trimmed || trimmed.toLowerCase() === "null") return null - try { - return JSON.parse(trimmed) as Prisma.JsonValue - } catch (error) { - console.warn("[company-service] Invalid JSON detected; coercing to null.", { value, error }) - return null - } -} - -function mapRawRowToCompany(row: RawCompanyRow): Company { - return { - id: row.id, - tenantId: row.tenantId, - name: row.name, - slug: row.slug, - provisioningCode: row.provisioningCode ?? "", - isAvulso: Boolean(row.isAvulso), - contractedHoursPerMonth: row.contractedHoursPerMonth, - cnpj: row.cnpj, - domain: row.domain, - phone: row.phone, - description: row.description, - address: row.address, - legalName: row.legalName, - tradeName: row.tradeName, - stateRegistration: row.stateRegistration, - stateRegistrationType: row.stateRegistrationType - ? (row.stateRegistrationType as DbCompanyStateRegistrationType) - : null, - primaryCnae: row.primaryCnae, - timezone: row.timezone, - businessHours: parseJsonValue(row.businessHours), - supportEmail: row.supportEmail, - billingEmail: row.billingEmail, - contactPreferences: parseJsonValue(row.contactPreferences), - clientDomains: parseJsonValue(row.clientDomains), - communicationChannels: parseJsonValue(row.communicationChannels), - fiscalAddress: parseJsonValue(row.fiscalAddress), - hasBranches: Boolean(row.hasBranches), - regulatedEnvironments: parseJsonValue(row.regulatedEnvironments), - privacyPolicyAccepted: Boolean(row.privacyPolicyAccepted), - privacyPolicyReference: row.privacyPolicyReference, - privacyPolicyMetadata: parseJsonValue(row.privacyPolicyMetadata), - contacts: parseJsonValue(row.contacts), - locations: parseJsonValue(row.locations), - contracts: parseJsonValue(row.contracts), - sla: parseJsonValue(row.sla), - tags: parseJsonValue(row.tags), - customFields: parseJsonValue(row.customFields), - notes: row.notes, - createdAt: new Date(row.createdAt), - updatedAt: new Date(row.updatedAt), - } -} - -const COMPANY_BASE_SELECT = Prisma.sql` - SELECT - id, - tenantId, - name, - slug, - provisioningCode, - isAvulso, - contractedHoursPerMonth, - cnpj, - domain, - phone, - description, - address, - legalName, - tradeName, - stateRegistration, - stateRegistrationType, - primaryCnae, - timezone, - CAST(businessHours AS TEXT) AS businessHours, - supportEmail, - billingEmail, - CAST(contactPreferences AS TEXT) AS contactPreferences, - CAST(clientDomains AS TEXT) AS clientDomains, - CAST(communicationChannels AS TEXT) AS communicationChannels, - CAST(fiscalAddress AS TEXT) AS fiscalAddress, - hasBranches, - CAST(regulatedEnvironments AS TEXT) AS regulatedEnvironments, - privacyPolicyAccepted, - privacyPolicyReference, - CAST(privacyPolicyMetadata AS TEXT) AS privacyPolicyMetadata, - CAST(contacts AS TEXT) AS contacts, - CAST(locations AS TEXT) AS locations, - CAST(contracts AS TEXT) AS contracts, - CAST(sla AS TEXT) AS sla, - CAST(tags AS TEXT) AS tags, - CAST(customFields AS TEXT) AS customFields, - notes, - createdAt, - updatedAt - FROM "Company" -` - export async function fetchCompaniesByTenant(tenantId: string): Promise { - const rows = await prisma.$queryRaw(Prisma.sql` - ${COMPANY_BASE_SELECT} - WHERE tenantId = ${tenantId} - ORDER BY name ASC - `) - return rows.map(mapRawRowToCompany) + return safeCompanyFindMany({ + where: { tenantId }, + orderBy: { name: "asc" }, + }) } export async function fetchCompanyById(id: string): Promise { - const rows = await prisma.$queryRaw(Prisma.sql` - ${COMPANY_BASE_SELECT} - WHERE id = ${id} - LIMIT 1 - `) - const row = rows[0] - return row ? mapRawRowToCompany(row) : null + const rows = await safeCompanyFindMany({ + where: { id }, + take: 1, + }) + return rows[0] ?? null }