feat: expand admin companies and users modules
This commit is contained in:
parent
a043b1203c
commit
2e3b46a7b5
31 changed files with 5626 additions and 2003 deletions
439
src/lib/schemas/company.ts
Normal file
439
src/lib/schemas/company.ts
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue