feat: expand admin companies and users modules

This commit is contained in:
Esdras Renan 2025-10-22 01:27:43 -03:00
parent a043b1203c
commit 2e3b46a7b5
31 changed files with 5626 additions and 2003 deletions

439
src/lib/schemas/company.ts Normal file
View file

@ -0,0 +1,439 @@
import { z } from "zod"
export const BRAZILIAN_UF = [
{ value: "AC", label: "Acre" },
{ value: "AL", label: "Alagoas" },
{ value: "AP", label: "Amapá" },
{ value: "AM", label: "Amazonas" },
{ value: "BA", label: "Bahia" },
{ value: "CE", label: "Ceará" },
{ value: "DF", label: "Distrito Federal" },
{ value: "ES", label: "Espírito Santo" },
{ value: "GO", label: "Goiás" },
{ value: "MA", label: "Maranhão" },
{ value: "MT", label: "Mato Grosso" },
{ value: "MS", label: "Mato Grosso do Sul" },
{ value: "MG", label: "Minas Gerais" },
{ value: "PA", label: "Pará" },
{ value: "PB", label: "Paraíba" },
{ value: "PR", label: "Paraná" },
{ value: "PE", label: "Pernambuco" },
{ value: "PI", label: "Piauí" },
{ value: "RJ", label: "Rio de Janeiro" },
{ value: "RN", label: "Rio Grande do Norte" },
{ value: "RS", label: "Rio Grande do Sul" },
{ value: "RO", label: "Rondônia" },
{ value: "RR", label: "Roraima" },
{ value: "SC", label: "Santa Catarina" },
{ value: "SP", label: "São Paulo" },
{ value: "SE", label: "Sergipe" },
{ value: "TO", label: "Tocantins" },
] as const
export const COMPANY_STATE_REGISTRATION_TYPES = [
{ value: "standard", label: "Inscrição estadual" },
{ value: "exempt", label: "Isento" },
{ value: "simples", label: "Simples Nacional" },
] as const
export const COMPANY_CONTACT_ROLES = [
{ value: "financeiro", label: "Financeiro" },
{ value: "decisor", label: "Decisor" },
{ value: "ti", label: "TI" },
{ value: "juridico", label: "Jurídico" },
{ value: "compras", label: "Compras" },
{ value: "usuario_chave", label: "Usuário-chave" },
{ value: "outro", label: "Outro" },
] as const
export const COMPANY_CONTACT_PREFERENCES = [
{ value: "email", label: "E-mail" },
{ value: "phone", label: "Telefone" },
{ value: "whatsapp", label: "WhatsApp" },
{ value: "business_hours", label: "Horário comercial" },
] as const
export const COMPANY_LOCATION_TYPES = [
{ value: "matrix", label: "Matriz" },
{ value: "branch", label: "Filial" },
{ value: "data_center", label: "Data Center" },
{ value: "home_office", label: "Home Office" },
{ value: "other", label: "Outro" },
] as const
export const COMPANY_CONTRACT_TYPES = [
{ value: "monthly", label: "Mensalidade" },
{ value: "time_bank", label: "Banco de horas" },
{ value: "per_ticket", label: "Por chamado" },
{ value: "project", label: "Projetos" },
] as const
export const COMPANY_CONTRACT_SCOPES = [
"suporte_m365",
"endpoints",
"rede",
"servidores",
"backup",
"email",
"impressoras",
"seguranca",
] as const
export const COMPANY_CRITICALITY_LEVELS = [
{ value: "low", label: "Baixa" },
{ value: "medium", label: "Média" },
{ value: "high", label: "Alta" },
] as const
export const COMPANY_REGULATION_OPTIONS = [
{ value: "lgpd_critical", label: "LGPD crítico" },
{ value: "finance", label: "Financeiro" },
{ value: "health", label: "Saúde" },
{ value: "public", label: "Setor público" },
{ value: "education", label: "Educação" },
{ value: "custom", label: "Outro" },
] as const
export const SLA_SEVERITY_LEVELS = ["P1", "P2", "P3", "P4"] as const
const daySchema = z.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])
const ufSchema = z.enum(BRAZILIAN_UF.map((item) => item.value) as [string, ...string[]])
const stateRegistrationTypeSchema = z.enum(
COMPANY_STATE_REGISTRATION_TYPES.map((item) => item.value) as [string, ...string[]]
)
const contactRoleSchema = z.enum(
COMPANY_CONTACT_ROLES.map((item) => item.value) as [string, ...string[]]
)
const contactPreferenceSchema = z.enum(
COMPANY_CONTACT_PREFERENCES.map((item) => item.value) as [string, ...string[]]
)
const locationTypeSchema = z.enum(
COMPANY_LOCATION_TYPES.map((item) => item.value) as [string, ...string[]]
)
const contractTypeSchema = z.enum(
COMPANY_CONTRACT_TYPES.map((item) => item.value) as [string, ...string[]]
)
const contractScopeSchema = z.enum(COMPANY_CONTRACT_SCOPES)
const criticalitySchema = z.enum(
COMPANY_CRITICALITY_LEVELS.map((item) => item.value) as [string, ...string[]]
)
const regulationSchema = z.enum(
COMPANY_REGULATION_OPTIONS.map((item) => item.value) as [string, ...string[]]
)
const severitySchema = z.enum(SLA_SEVERITY_LEVELS)
const phoneRegex = /^[0-9()+\s-]{8,20}$/
const timeRegex = /^\d{2}:\d{2}$/
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
const cnpjDigitsRegex = /^\d{14}$/
const cepRegex = /^\d{5}-?\d{3}$/
const domainRegex =
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/i
const optionalString = z
.string()
.trim()
.transform((value) => (value.length === 0 ? null : value))
.nullable()
.optional()
const monetarySchema = z
.union([z.number(), z.string()])
.transform((value) => {
if (typeof value === "number") return value
const normalized = value.replace(/\./g, "").replace(",", ".")
const parsed = Number(normalized)
return Number.isFinite(parsed) ? parsed : null
})
.nullable()
const servicePeriodSchema = z.object({
days: z.array(daySchema).min(1, "Informe ao menos um dia"),
start: z
.string()
.regex(timeRegex, "Use o formato HH:MM"),
end: z
.string()
.regex(timeRegex, "Use o formato HH:MM"),
})
export const businessHoursSchema = z
.object({
mode: z.enum(["business", "twentyfour", "custom"]).default("business"),
timezone: z.string().min(3).default("America/Sao_Paulo"),
periods: z.array(servicePeriodSchema).default([]),
})
.refine(
(value) => {
if (value.mode === "twentyfour") return true
return value.periods.length > 0
},
{ message: "Defina pelo menos um período", path: ["periods"] }
)
const addressSchema = z
.object({
street: z.string().min(3, "Informe a rua"),
number: z.string().min(1, "Informe o número"),
complement: optionalString,
district: z.string().min(2, "Informe o bairro"),
city: z.string().min(2, "Informe a cidade"),
state: ufSchema,
zip: z
.string()
.regex(cepRegex, "CEP inválido"),
})
.partial({
complement: true,
})
const contactSchema = z.object({
id: z.string(),
fullName: z.string().min(3, "Nome obrigatório"),
email: z.string().email("E-mail inválido"),
phone: z
.string()
.regex(phoneRegex, "Telefone inválido")
.nullable()
.optional(),
whatsapp: z
.string()
.regex(phoneRegex, "WhatsApp inválido")
.nullable()
.optional(),
role: contactRoleSchema,
title: z.string().trim().nullable().optional(),
preference: z.array(contactPreferenceSchema).default([]),
canAuthorizeTickets: z.boolean().default(false),
canApproveCosts: z.boolean().default(false),
lgpdConsent: z.boolean().default(true),
notes: z.string().trim().nullable().optional(),
})
const locationSchema = z.object({
id: z.string(),
name: z.string().min(2, "Informe o nome da unidade"),
type: locationTypeSchema,
address: addressSchema.nullish(),
responsibleContactId: z.string().nullable().optional(),
serviceWindow: z
.object({
mode: z.enum(["inherit", "custom"]).default("inherit"),
periods: z.array(servicePeriodSchema).default([]),
})
.refine(
(value) => (value.mode === "inherit" ? true : value.periods.length > 0),
{ message: "Defina períodos personalizados", path: ["periods"] }
),
notes: z.string().trim().nullable().optional(),
})
const contractSchema = z.object({
id: z.string(),
contractType: contractTypeSchema,
planSku: z.string().trim().nullable().optional(),
startDate: z
.string()
.regex(dateRegex, "Formato AAAA-MM-DD")
.nullable()
.optional(),
endDate: z
.string()
.regex(dateRegex, "Formato AAAA-MM-DD")
.nullable()
.optional(),
renewalDate: z
.string()
.regex(dateRegex, "Formato AAAA-MM-DD")
.nullable()
.optional(),
scope: z.array(contractScopeSchema).default([]),
price: monetarySchema,
costCenter: z.string().trim().nullable().optional(),
criticality: criticalitySchema.default("medium"),
notes: z.string().trim().nullable().optional(),
})
const severityEntrySchema = z.object({
level: severitySchema,
responseMinutes: z
.number()
.int()
.min(0)
.default(60),
resolutionMinutes: z
.number()
.int()
.min(0)
.default(240),
})
const slaSchema = z.object({
calendar: z.enum(["24x7", "business", "custom"]).default("business"),
validChannels: z.array(z.string().min(3)).default([]),
holidays: z
.array(
z
.string()
.regex(dateRegex, "Formato AAAA-MM-DD")
)
.default([]),
severities: z
.array(severityEntrySchema)
.default([
{ level: "P1", responseMinutes: 30, resolutionMinutes: 240 },
{ level: "P2", responseMinutes: 60, resolutionMinutes: 480 },
{ level: "P3", responseMinutes: 120, resolutionMinutes: 1440 },
{ level: "P4", responseMinutes: 240, resolutionMinutes: 2880 },
]),
serviceWindow: z
.object({
timezone: z.string().default("America/Sao_Paulo"),
periods: z.array(servicePeriodSchema).default([]),
})
.optional(),
})
const communicationChannelsSchema = z.object({
supportEmails: z.array(z.string().email()).default([]),
billingEmails: z.array(z.string().email()).default([]),
whatsappNumbers: z.array(z.string().regex(phoneRegex, "Telefone inválido")).default([]),
phones: z.array(z.string().regex(phoneRegex, "Telefone inválido")).default([]),
portals: z.array(z.string().url("URL inválida")).default([]),
})
const privacyPolicySchema = z.object({
accepted: z.boolean().default(false),
reference: z.string().url("URL inválida").nullable().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
const customFieldSchema = z.object({
id: z.string(),
key: z.string().min(1),
label: z.string().min(1),
type: z.enum(["text", "number", "boolean", "date", "url"]).default("text"),
value: z.union([z.string(), z.number(), z.boolean(), z.null()]).nullable(),
})
const domainSchema = z
.string()
.trim()
.toLowerCase()
.regex(domainRegex, "Domínio inválido")
export const companyFormSchema = z.object({
tenantId: z.string(),
name: z.string().min(2, "Nome obrigatório"),
slug: z
.string()
.min(2, "Informe um apelido")
.regex(/^[a-z0-9-]+$/, "Use apenas letras minúsculas, números ou hífen"),
legalName: z.string().trim().nullable().optional(),
tradeName: z.string().trim().nullable().optional(),
cnpj: z
.string()
.regex(cnpjDigitsRegex, "CNPJ deve ter 14 dígitos")
.nullable()
.optional(),
stateRegistration: z.string().trim().nullable().optional(),
stateRegistrationType: stateRegistrationTypeSchema.nullish(),
primaryCnae: z.string().trim().nullable().optional(),
description: z.string().trim().nullable().optional(),
domain: domainSchema.nullable().optional(),
phone: z
.string()
.regex(phoneRegex, "Telefone inválido")
.nullable()
.optional(),
address: z.string().trim().nullable().optional(),
contractedHoursPerMonth: z
.number()
.min(0)
.nullable()
.optional(),
businessHours: businessHoursSchema.nullish(),
communicationChannels: communicationChannelsSchema.default({
supportEmails: [],
billingEmails: [],
whatsappNumbers: [],
phones: [],
portals: [],
}),
supportEmail: z.string().email().nullable().optional(),
billingEmail: z.string().email().nullable().optional(),
contactPreferences: z
.object({
defaultChannel: contactPreferenceSchema.nullable().optional(),
escalationNotes: z.string().trim().nullable().optional(),
})
.optional(),
clientDomains: z.array(domainSchema).default([]),
fiscalAddress: addressSchema.nullish(),
hasBranches: z.boolean().default(false),
regulatedEnvironments: z.array(regulationSchema).default([]),
privacyPolicy: privacyPolicySchema.default({
accepted: false,
reference: null,
}),
contacts: z.array(contactSchema).default([]),
locations: z.array(locationSchema).default([]),
contracts: z.array(contractSchema).default([]),
sla: slaSchema.nullish(),
tags: z.array(z.string().trim().min(1)).default([]),
customFields: z.array(customFieldSchema).default([]),
notes: z.string().trim().nullable().optional(),
isAvulso: z.boolean().default(false),
})
export type CompanyFormValues = z.infer<typeof companyFormSchema>
export type CompanyContact = z.infer<typeof contactSchema>
export type CompanyLocation = z.infer<typeof locationSchema>
export type CompanyContract = z.infer<typeof contractSchema>
export type CompanySla = z.infer<typeof slaSchema>
export type CompanyBusinessHours = z.infer<typeof businessHoursSchema>
export type CompanyCommunicationChannels = z.infer<typeof communicationChannelsSchema>
export type CompanyStateRegistrationTypeOption =
(typeof COMPANY_STATE_REGISTRATION_TYPES)[number]["value"]
export const defaultBusinessHours: CompanyBusinessHours = {
mode: "business",
timezone: "America/Sao_Paulo",
periods: [
{
days: ["mon", "tue", "wed", "thu", "fri"],
start: "09:00",
end: "18:00",
},
],
}
export const defaultSla: CompanySla = {
calendar: "business",
validChannels: [],
holidays: [],
severities: [
{ level: "P1", responseMinutes: 30, resolutionMinutes: 240 },
{ level: "P2", responseMinutes: 60, resolutionMinutes: 480 },
{ level: "P3", responseMinutes: 120, resolutionMinutes: 1440 },
{ level: "P4", responseMinutes: 240, resolutionMinutes: 2880 },
],
serviceWindow: {
timezone: "America/Sao_Paulo",
periods: [
{
days: ["mon", "tue", "wed", "thu", "fri"],
start: "09:00",
end: "18:00",
},
],
},
}
export type CompanyFormPayload = CompanyFormValues
export const companyInputSchema = companyFormSchema.extend({
tenantId: companyFormSchema.shape.tenantId.optional(),
})
export type CompanyInputPayload = z.infer<typeof companyInputSchema>