Initial SeConectai briefing system
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { decryptCpf } from "@/lib/crypto";
|
||||
|
||||
export async function getClienteConta() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id || session.user.papel !== "CLIENTE") {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const cliente = await db.cliente.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
include: {
|
||||
user: true,
|
||||
briefings: {
|
||||
include: {
|
||||
respostas: true,
|
||||
anexos: {
|
||||
select: {
|
||||
id: true,
|
||||
nomeOriginal: true,
|
||||
mime: true,
|
||||
tamanho: true,
|
||||
criadoEm: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
solicitacoesExclusao: {
|
||||
orderBy: { criadoEm: "desc" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!cliente) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return cliente;
|
||||
}
|
||||
|
||||
export async function getExportacaoDadosCliente() {
|
||||
const cliente = await getClienteConta();
|
||||
|
||||
return {
|
||||
exportadoEm: new Date().toISOString(),
|
||||
cliente: {
|
||||
id: cliente.id,
|
||||
nomeCompleto: cliente.nomeCompleto,
|
||||
cpf: decryptCpf(cliente.cpfCifrado),
|
||||
whatsapp: cliente.whatsapp,
|
||||
email: cliente.user.email,
|
||||
tipo: cliente.tipo,
|
||||
razaoSocial: cliente.razaoSocial,
|
||||
cnpj: cliente.cnpj,
|
||||
endereco: {
|
||||
cep: cliente.cep,
|
||||
logradouro: cliente.logradouro,
|
||||
numero: cliente.numero,
|
||||
complemento: cliente.complemento,
|
||||
bairro: cliente.bairro,
|
||||
cidade: cliente.cidade,
|
||||
uf: cliente.uf
|
||||
},
|
||||
lgpdAceiteEm: cliente.lgpdAceiteEm
|
||||
},
|
||||
briefings: cliente.briefings.map((briefing) => ({
|
||||
id: briefing.id,
|
||||
titulo: briefing.titulo,
|
||||
status: briefing.status,
|
||||
criadoEm: briefing.criadoEm,
|
||||
atualizadoEm: briefing.atualizadoEm,
|
||||
enviadoEm: briefing.enviadoEm,
|
||||
confirmadoEm: briefing.confirmadoEm,
|
||||
respostas: briefing.respostas.map((resposta) => ({
|
||||
perguntaId: resposta.perguntaId,
|
||||
texto: resposta.texto,
|
||||
complemento: resposta.complemento,
|
||||
selecionadas: resposta.selecionadas,
|
||||
atualizadoEm: resposta.atualizadoEm
|
||||
})),
|
||||
anexos: briefing.anexos
|
||||
})),
|
||||
solicitacoesExclusao: cliente.solicitacoesExclusao
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { StatusBriefing, TipoPessoa } from "@prisma/client";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { decryptCpf } from "@/lib/crypto";
|
||||
|
||||
export async function requireAdmin() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (session.user.papel !== "ADMIN") {
|
||||
redirect("/briefings");
|
||||
}
|
||||
|
||||
return session.user;
|
||||
}
|
||||
|
||||
export function maskCpf(cpf: string) {
|
||||
const digits = cpf.replace(/\D/g, "");
|
||||
|
||||
if (digits.length !== 11) {
|
||||
return "***.***.***-**";
|
||||
}
|
||||
|
||||
return `***.${digits.slice(3, 6)}.${digits.slice(6, 9)}-**`;
|
||||
}
|
||||
|
||||
export function whatsappLink(value: string) {
|
||||
const digits = value.replace(/\D/g, "");
|
||||
return `https://wa.me/${digits}`;
|
||||
}
|
||||
|
||||
export async function getAdminDashboard() {
|
||||
await requireAdmin();
|
||||
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const [porStatus, novosUltimos7Dias, ultimosEnvios] = await Promise.all([
|
||||
db.briefing.groupBy({
|
||||
by: ["status"],
|
||||
_count: { _all: true }
|
||||
}),
|
||||
db.briefing.count({
|
||||
where: {
|
||||
criadoEm: {
|
||||
gte: since
|
||||
}
|
||||
}
|
||||
}),
|
||||
db.briefing.findMany({
|
||||
where: {
|
||||
enviadoEm: {
|
||||
not: null
|
||||
}
|
||||
},
|
||||
orderBy: { enviadoEm: "desc" },
|
||||
take: 8,
|
||||
include: {
|
||||
cliente: {
|
||||
include: { user: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
return { porStatus, novosUltimos7Dias, ultimosEnvios };
|
||||
}
|
||||
|
||||
export async function listarClientesAdmin(filters: {
|
||||
busca?: string;
|
||||
tipo?: TipoPessoa;
|
||||
}) {
|
||||
await requireAdmin();
|
||||
const busca = filters.busca?.trim();
|
||||
|
||||
return db.cliente.findMany({
|
||||
where: {
|
||||
tipo: filters.tipo,
|
||||
OR: busca
|
||||
? [
|
||||
{ nomeCompleto: { contains: busca, mode: "insensitive" } },
|
||||
{ razaoSocial: { contains: busca, mode: "insensitive" } },
|
||||
{ cnpj: { contains: busca.replace(/\D/g, "") } },
|
||||
{ whatsapp: { contains: busca.replace(/\D/g, "") || busca } },
|
||||
{ user: { email: { contains: busca, mode: "insensitive" } } }
|
||||
]
|
||||
: undefined
|
||||
},
|
||||
orderBy: { criadoEm: "desc" },
|
||||
include: {
|
||||
user: true,
|
||||
briefings: {
|
||||
orderBy: { atualizadoEm: "desc" }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function getClienteAdmin(clienteId: string) {
|
||||
await requireAdmin();
|
||||
const cliente = await db.cliente.findUnique({
|
||||
where: { id: clienteId },
|
||||
include: {
|
||||
user: true,
|
||||
briefings: {
|
||||
orderBy: { atualizadoEm: "desc" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!cliente) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return cliente;
|
||||
}
|
||||
|
||||
export async function revelarCpfCliente(clienteId: string) {
|
||||
const admin = await requireAdmin();
|
||||
const cliente = await db.cliente.findUnique({
|
||||
where: { id: clienteId }
|
||||
});
|
||||
|
||||
if (!cliente) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
await db.auditoriaAcesso.create({
|
||||
data: {
|
||||
adminId: admin.id,
|
||||
clienteId,
|
||||
acao: "REVELOU_CPF"
|
||||
}
|
||||
});
|
||||
|
||||
return decryptCpf(cliente.cpfCifrado);
|
||||
}
|
||||
|
||||
export async function getBriefingAdmin(briefingId: string) {
|
||||
await requireAdmin();
|
||||
const briefing = await db.briefing.findUnique({
|
||||
where: { id: briefingId },
|
||||
include: {
|
||||
cliente: {
|
||||
include: { user: true }
|
||||
},
|
||||
notasInternas: {
|
||||
orderBy: { criadoEm: "desc" },
|
||||
include: { admin: true }
|
||||
},
|
||||
anexos: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!briefing) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const secoes = await db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
respostas: {
|
||||
where: { briefingId }
|
||||
},
|
||||
anexos: {
|
||||
where: { briefingId },
|
||||
orderBy: { criadoEm: "desc" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { briefing, secoes };
|
||||
}
|
||||
|
||||
export async function listarPerguntasAdmin() {
|
||||
await requireAdmin();
|
||||
|
||||
return db.secao.findMany({
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
orderBy: { ordem: "asc" },
|
||||
include: { ajuda: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function atualizarStatusBriefing(briefingId: string, status: StatusBriefing) {
|
||||
await requireAdmin();
|
||||
|
||||
return db.briefing.update({
|
||||
where: { id: briefingId },
|
||||
data: { status }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function getAnexoAutorizado(anexoId: string) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anexo = await db.anexo.findUnique({
|
||||
where: { id: anexoId },
|
||||
include: {
|
||||
briefing: {
|
||||
include: {
|
||||
cliente: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!anexo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.user.papel === "ADMIN") {
|
||||
return anexo;
|
||||
}
|
||||
|
||||
if (session.user.papel === "CLIENTE" && anexo.briefing.cliente.userId === session.user.id) {
|
||||
return anexo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getBriefingPerguntaParaUpload(briefingId: string, perguntaId: string) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id || session.user.papel !== "CLIENTE") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const briefing = await db.briefing.findFirst({
|
||||
where: {
|
||||
id: briefingId,
|
||||
cliente: {
|
||||
userId: session.user.id
|
||||
}
|
||||
},
|
||||
include: {
|
||||
cliente: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!briefing) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const pergunta = await db.pergunta.findFirst({
|
||||
where: {
|
||||
id: perguntaId,
|
||||
tipo: "ANEXO",
|
||||
ativo: true
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!pergunta) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return { briefing, pergunta };
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
pdf
|
||||
} from "@react-pdf/renderer";
|
||||
import type { getBriefingPdfData } from "@/lib/pdf-data";
|
||||
|
||||
type PdfData = Awaited<ReturnType<typeof getBriefingPdfData>>;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
padding: 32,
|
||||
fontSize: 10,
|
||||
color: "#1f2933",
|
||||
fontFamily: "Helvetica"
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
marginBottom: 6,
|
||||
fontWeight: 700
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 10,
|
||||
color: "#52606d",
|
||||
marginBottom: 16
|
||||
},
|
||||
section: {
|
||||
marginTop: 16,
|
||||
paddingTop: 10,
|
||||
borderTop: "1 solid #d9e2ec"
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
marginBottom: 8
|
||||
},
|
||||
question: {
|
||||
marginBottom: 10
|
||||
},
|
||||
label: {
|
||||
fontWeight: 700,
|
||||
marginBottom: 3
|
||||
},
|
||||
answer: {
|
||||
color: "#52606d",
|
||||
lineHeight: 1.5
|
||||
},
|
||||
meta: {
|
||||
marginBottom: 4
|
||||
}
|
||||
});
|
||||
|
||||
function selectedText(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string").join(", ")
|
||||
: "";
|
||||
}
|
||||
|
||||
function opcoesTexto(opcoes: unknown, selecionadas: unknown) {
|
||||
const options = Array.isArray(opcoes)
|
||||
? opcoes.filter((item): item is string => typeof item === "string")
|
||||
: [];
|
||||
const selected = Array.isArray(selecionadas)
|
||||
? selecionadas.filter((item): item is string => typeof item === "string")
|
||||
: [];
|
||||
|
||||
if (options.length === 0) {
|
||||
return selected.join(", ");
|
||||
}
|
||||
|
||||
return options
|
||||
.map((opcao) => `${selected.includes(opcao) ? "☒" : "☐"} ${opcao}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function respostaTexto(resposta: {
|
||||
texto: string | null;
|
||||
complemento: string | null;
|
||||
selecionadas: unknown;
|
||||
}) {
|
||||
const partes = [
|
||||
resposta.texto?.trim(),
|
||||
selectedText(resposta.selecionadas),
|
||||
resposta.complemento?.trim() ? `Complemento: ${resposta.complemento.trim()}` : ""
|
||||
].filter(Boolean);
|
||||
|
||||
return partes.join(" | ");
|
||||
}
|
||||
|
||||
function BriefingPdfDocument({ data }: { data: PdfData }) {
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
<Text style={styles.title}>{data.briefing.titulo}</Text>
|
||||
<Text style={styles.subtitle}>Briefing de Site | SeConectai</Text>
|
||||
|
||||
<View>
|
||||
<Text style={styles.meta}>Cliente: {data.briefing.cliente.nomeCompleto}</Text>
|
||||
<Text style={styles.meta}>E-mail: {data.briefing.cliente.user.email}</Text>
|
||||
<Text style={styles.meta}>WhatsApp: {data.briefing.cliente.whatsapp}</Text>
|
||||
<Text style={styles.meta}>CPF: {data.cpfMascarado}</Text>
|
||||
<Text style={styles.meta}>Status: {data.briefing.status}</Text>
|
||||
<Text style={styles.meta}>
|
||||
Enviado em: {data.briefing.enviadoEm ? data.briefing.enviadoEm.toLocaleString("pt-BR") : "Nao enviado"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{data.secoes.map((secao) => (
|
||||
<View key={secao.id} style={styles.section} wrap={false}>
|
||||
<Text style={styles.sectionTitle}>{secao.ordem}. {secao.titulo}</Text>
|
||||
{secao.perguntas.map((pergunta) => {
|
||||
const resposta = pergunta.respostas[0];
|
||||
const texto =
|
||||
resposta && (pergunta.tipo === "RADIO" || pergunta.tipo === "CHECKBOX")
|
||||
? [
|
||||
opcoesTexto(pergunta.opcoes, resposta.selecionadas),
|
||||
resposta.complemento?.trim() ? `Complemento: ${resposta.complemento.trim()}` : ""
|
||||
].filter(Boolean).join(" | ")
|
||||
: resposta
|
||||
? respostaTexto(resposta)
|
||||
: "";
|
||||
|
||||
return (
|
||||
<View key={pergunta.id} style={styles.question}>
|
||||
<Text style={styles.label}>{pergunta.rotulo}</Text>
|
||||
<Text style={styles.answer}>{texto || "Sem resposta informada."}</Text>
|
||||
{pergunta.anexos.length > 0 ? (
|
||||
<Text style={styles.answer}>
|
||||
Anexos: {pergunta.anexos.map((anexo) => anexo.nomeOriginal).join(", ")}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Confirmacao do cliente</Text>
|
||||
<Text style={styles.answer}>
|
||||
Confirmado em: {data.briefing.confirmadoEm ? data.briefing.confirmadoEm.toLocaleString("pt-BR") : "Nao confirmado"}
|
||||
</Text>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
export async function gerarBriefingPdfBuffer(data: PdfData) {
|
||||
return pdf(<BriefingPdfDocument data={data} />).toBuffer();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function getEstruturaBriefing() {
|
||||
return db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
ajuda: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
async function getClienteAtual() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id || session.user.papel !== "CLIENTE") {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const cliente = await db.cliente.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
include: { user: true }
|
||||
});
|
||||
|
||||
if (!cliente) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return cliente;
|
||||
}
|
||||
|
||||
function isRespondida(resposta: {
|
||||
texto: string | null;
|
||||
selecionadas: unknown;
|
||||
complemento?: string | null;
|
||||
}) {
|
||||
const selecionadas = Array.isArray(resposta.selecionadas) ? resposta.selecionadas : [];
|
||||
return Boolean(
|
||||
resposta.texto?.trim() ||
|
||||
resposta.complemento?.trim() ||
|
||||
selecionadas.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function listarBriefingsDoCliente() {
|
||||
const cliente = await getClienteAtual();
|
||||
|
||||
return db.briefing.findMany({
|
||||
where: { clienteId: cliente.id },
|
||||
orderBy: { atualizadoEm: "desc" },
|
||||
include: {
|
||||
respostas: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function criarBriefingParaCliente() {
|
||||
const cliente = await getClienteAtual();
|
||||
const titulo = cliente.razaoSocial ?? cliente.nomeCompleto;
|
||||
|
||||
const briefing = await db.briefing.create({
|
||||
data: {
|
||||
clienteId: cliente.id,
|
||||
titulo: `Briefing - ${titulo}`
|
||||
}
|
||||
});
|
||||
|
||||
return briefing;
|
||||
}
|
||||
|
||||
export async function getBriefingDoCliente(briefingId: string) {
|
||||
const cliente = await getClienteAtual();
|
||||
|
||||
const briefing = await db.briefing.findFirst({
|
||||
where: {
|
||||
id: briefingId,
|
||||
clienteId: cliente.id
|
||||
},
|
||||
include: {
|
||||
respostas: true,
|
||||
cliente: {
|
||||
include: { user: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!briefing) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return briefing;
|
||||
}
|
||||
|
||||
export async function getAberturaBriefing(briefingId: string) {
|
||||
const briefing = await getBriefingDoCliente(briefingId);
|
||||
const [secoes, configuracoes] = await Promise.all([
|
||||
db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
anexos: {
|
||||
where: { briefingId: briefing.id },
|
||||
orderBy: { criadoEm: "desc" }
|
||||
},
|
||||
respostas: {
|
||||
where: { briefingId: briefing.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
db.configuracao.findMany({
|
||||
where: {
|
||||
chave: {
|
||||
in: ["comoPreencher", "avisoSenhas"]
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const progresso = secoes.map((secao) => {
|
||||
const total = secao.perguntas.length;
|
||||
const respondidas = secao.perguntas.filter((pergunta) =>
|
||||
pergunta.respostas.some((resposta) => isRespondida(resposta))
|
||||
).length;
|
||||
|
||||
return {
|
||||
id: secao.id,
|
||||
ordem: secao.ordem,
|
||||
titulo: secao.titulo,
|
||||
descricao: secao.descricao,
|
||||
total,
|
||||
respondidas
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
briefing,
|
||||
progresso,
|
||||
comoPreencher:
|
||||
configuracoes.find((configuracao) => configuracao.chave === "comoPreencher")?.valor ?? [],
|
||||
avisoSenhas:
|
||||
configuracoes.find((configuracao) => configuracao.chave === "avisoSenhas")?.valor ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSecaoBriefing(briefingId: string, ordemSecao: number) {
|
||||
const briefing = await getBriefingDoCliente(briefingId);
|
||||
const secoes = await db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
ordem: true,
|
||||
titulo: true,
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
select: {
|
||||
id: true,
|
||||
respostas: {
|
||||
where: { briefingId: briefing.id },
|
||||
select: {
|
||||
texto: true,
|
||||
selecionadas: true,
|
||||
complemento: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const secao = await db.secao.findFirst({
|
||||
where: {
|
||||
ordem: ordemSecao,
|
||||
ativo: true
|
||||
},
|
||||
include: {
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
ajuda: true,
|
||||
anexos: {
|
||||
where: { briefingId: briefing.id },
|
||||
orderBy: { criadoEm: "desc" }
|
||||
},
|
||||
respostas: {
|
||||
where: { briefingId: briefing.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!secao) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const progressoGeral = secoes.reduce(
|
||||
(acc, item) => {
|
||||
const respondidas = item.perguntas.filter((pergunta) =>
|
||||
pergunta.respostas.some((resposta) => isRespondida(resposta))
|
||||
).length;
|
||||
|
||||
return {
|
||||
total: acc.total + item.perguntas.length,
|
||||
respondidas: acc.respondidas + respondidas
|
||||
};
|
||||
},
|
||||
{ total: 0, respondidas: 0 }
|
||||
);
|
||||
|
||||
return {
|
||||
briefing,
|
||||
secao,
|
||||
secoes: secoes.map((item) => ({
|
||||
id: item.id,
|
||||
ordem: item.ordem,
|
||||
titulo: item.titulo,
|
||||
total: item.perguntas.length,
|
||||
respondidas: item.perguntas.filter((pergunta) =>
|
||||
pergunta.respostas.some((resposta) => isRespondida(resposta))
|
||||
).length
|
||||
})),
|
||||
progressoGeral,
|
||||
anterior: secoes.find((item) => item.ordem === ordemSecao - 1) ?? null,
|
||||
proxima: secoes.find((item) => item.ordem === ordemSecao + 1) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRevisaoBriefing(briefingId: string) {
|
||||
const briefing = await getBriefingDoCliente(briefingId);
|
||||
const [secoes, confirmacao] = await Promise.all([
|
||||
db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
anexos: {
|
||||
where: { briefingId: briefing.id },
|
||||
orderBy: { criadoEm: "desc" }
|
||||
},
|
||||
respostas: {
|
||||
where: { briefingId: briefing.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
db.configuracao.findUnique({
|
||||
where: { chave: "confirmacaoCliente" }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
briefing,
|
||||
secoes,
|
||||
confirmacaoCliente: confirmacao?.valor ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export async function reabrirBriefing(briefingId: string) {
|
||||
return db.briefing.update({
|
||||
where: { id: briefingId },
|
||||
data: {
|
||||
status: "RASCUNHO",
|
||||
confirmadoEm: null,
|
||||
enviadoEm: null
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
||||
|
||||
function getEncryptionKey() {
|
||||
const rawKey = process.env.CPF_ENC_KEY;
|
||||
|
||||
if (!rawKey) {
|
||||
throw new Error("CPF_ENC_KEY nao configurada.");
|
||||
}
|
||||
|
||||
const key = Buffer.from(rawKey, "base64");
|
||||
|
||||
if (key.length !== 32) {
|
||||
throw new Error("CPF_ENC_KEY deve ter 32 bytes em base64.");
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function getHashKey() {
|
||||
const key = process.env.CPF_HASH_KEY;
|
||||
|
||||
if (!key) {
|
||||
throw new Error("CPF_HASH_KEY nao configurada.");
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
export function onlyDigits(value: string) {
|
||||
return value.replace(/\D/g, "");
|
||||
}
|
||||
|
||||
export function encryptCpf(cpf: string) {
|
||||
const iv = randomBytes(12);
|
||||
const cipher = createCipheriv("aes-256-gcm", getEncryptionKey(), iv);
|
||||
const encrypted = Buffer.concat([cipher.update(onlyDigits(cpf), "utf8"), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return `${iv.toString("base64")}.${tag.toString("base64")}.${encrypted.toString("base64")}`;
|
||||
}
|
||||
|
||||
export function decryptCpf(payload: string) {
|
||||
const [ivValue, tagValue, encryptedValue] = payload.split(".");
|
||||
|
||||
if (!ivValue || !tagValue || !encryptedValue) {
|
||||
throw new Error("CPF cifrado invalido.");
|
||||
}
|
||||
|
||||
const decipher = createDecipheriv("aes-256-gcm", getEncryptionKey(), Buffer.from(ivValue, "base64"));
|
||||
decipher.setAuthTag(Buffer.from(tagValue, "base64"));
|
||||
|
||||
return Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedValue, "base64")),
|
||||
decipher.final()
|
||||
]).toString("utf8");
|
||||
}
|
||||
|
||||
export function hashCpf(cpf: string) {
|
||||
return createHmac("sha256", getHashKey()).update(onlyDigits(cpf)).digest("hex");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma?: PrismaClient;
|
||||
};
|
||||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error"]
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = db;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function formatFileSize(bytes: number) {
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
type MailOptions = {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export async function sendMail({ to, subject, text }: MailOptions) {
|
||||
const host = process.env.SMTP_HOST;
|
||||
const port = Number(process.env.SMTP_PORT ?? 587);
|
||||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
const from = process.env.SMTP_FROM;
|
||||
|
||||
if (!host || !from) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
auth: user && pass ? { user, pass } : undefined
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { db } from "@/lib/db";
|
||||
import { sendMail } from "@/lib/mail";
|
||||
|
||||
async function registrarEmail(params: {
|
||||
briefingId: string;
|
||||
destino: string;
|
||||
assunto: string;
|
||||
texto: string;
|
||||
}) {
|
||||
const registro = await db.envioNotificacao.create({
|
||||
data: {
|
||||
briefingId: params.briefingId,
|
||||
canal: "EMAIL",
|
||||
destino: params.destino,
|
||||
assunto: params.assunto,
|
||||
status: "PENDENTE"
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await sendMail({
|
||||
to: params.destino,
|
||||
subject: params.assunto,
|
||||
text: params.texto
|
||||
});
|
||||
|
||||
await db.envioNotificacao.update({
|
||||
where: { id: registro.id },
|
||||
data: {
|
||||
status: "ENVIADO",
|
||||
tentativas: { increment: 1 },
|
||||
enviadoEm: new Date()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await db.envioNotificacao.update({
|
||||
where: { id: registro.id },
|
||||
data: {
|
||||
status: "ERRO",
|
||||
tentativas: { increment: 1 },
|
||||
erro: error instanceof Error ? error.message : "Erro desconhecido"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificarBriefingEnviado(briefingId: string) {
|
||||
const briefing = await db.briefing.findUnique({
|
||||
where: { id: briefingId },
|
||||
include: {
|
||||
cliente: {
|
||||
include: { user: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!briefing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
|
||||
const pdfUrl = `${appUrl}/api/briefings/${briefing.id}/pdf`;
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
|
||||
await registrarEmail({
|
||||
briefingId,
|
||||
destino: briefing.cliente.user.email,
|
||||
assunto: "Briefing enviado - SeConectai",
|
||||
texto: [
|
||||
`O briefing "${briefing.titulo}" foi enviado com sucesso.`,
|
||||
"",
|
||||
`Voce pode baixar o PDF em: ${pdfUrl}`,
|
||||
"",
|
||||
"Este e-mail nao inclui CPF completo nem senhas."
|
||||
].join("\n")
|
||||
});
|
||||
|
||||
if (adminEmail) {
|
||||
await registrarEmail({
|
||||
briefingId,
|
||||
destino: adminEmail,
|
||||
assunto: "Novo briefing enviado",
|
||||
texto: [
|
||||
`Um novo briefing foi enviado por ${briefing.cliente.nomeCompleto}.`,
|
||||
`Titulo: ${briefing.titulo}`,
|
||||
`Painel: ${appUrl}/admin/briefings/${briefing.id}`,
|
||||
"",
|
||||
"Este e-mail nao inclui CPF completo nem senhas."
|
||||
].join("\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.EVOLUTION_API_URL &&
|
||||
process.env.EVOLUTION_API_KEY &&
|
||||
process.env.EVOLUTION_API_INSTANCE &&
|
||||
process.env.ADMIN_WHATSAPP
|
||||
) {
|
||||
await db.envioNotificacao.create({
|
||||
data: {
|
||||
briefingId,
|
||||
canal: "WHATSAPP",
|
||||
destino: process.env.ADMIN_WHATSAPP,
|
||||
assunto: "Novo briefing enviado",
|
||||
status: "PENDENTE",
|
||||
erro: "Pendente de consulta da versao/formato de payload da Evolution API antes do envio."
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { maskCpf } from "@/lib/admin-service";
|
||||
import { decryptCpf } from "@/lib/crypto";
|
||||
|
||||
export async function getBriefingPdfData(briefingId: string) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const briefing = await db.briefing.findUnique({
|
||||
where: { id: briefingId },
|
||||
include: {
|
||||
cliente: {
|
||||
include: { user: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!briefing) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isAdmin = session.user.papel === "ADMIN";
|
||||
const isOwner = session.user.papel === "CLIENTE" && briefing.cliente.userId === session.user.id;
|
||||
|
||||
if (!isAdmin && !isOwner) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const secoes = await db.secao.findMany({
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
perguntas: {
|
||||
where: { ativo: true },
|
||||
orderBy: { ordem: "asc" },
|
||||
include: {
|
||||
respostas: {
|
||||
where: { briefingId }
|
||||
},
|
||||
anexos: {
|
||||
where: { briefingId },
|
||||
orderBy: { criadoEm: "asc" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
briefing,
|
||||
secoes,
|
||||
cpfMascarado: maskCpf(decryptCpf(briefing.cliente.cpfCifrado))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
type Bucket = {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
};
|
||||
|
||||
const buckets = new Map<string, Bucket>();
|
||||
|
||||
export function checkRateLimit(key: string, limit = 5, windowMs = 15 * 60 * 1000) {
|
||||
const now = Date.now();
|
||||
const bucket = buckets.get(key);
|
||||
|
||||
if (!bucket || bucket.resetAt <= now) {
|
||||
buckets.set(key, { count: 1, resetAt: now + windowMs });
|
||||
return { allowed: true, remaining: limit - 1 };
|
||||
}
|
||||
|
||||
if (bucket.count >= limit) {
|
||||
return { allowed: false, remaining: 0 };
|
||||
}
|
||||
|
||||
bucket.count += 1;
|
||||
return { allowed: true, remaining: limit - bucket.count };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export function assertSameOrigin(request: Request) {
|
||||
const origin = request.headers.get("origin");
|
||||
const host = request.headers.get("host");
|
||||
|
||||
if (!origin || !host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const originUrl = new URL(origin);
|
||||
|
||||
if (originUrl.host !== host) {
|
||||
return NextResponse.json({ message: "Origem nao autorizada." }, { status: 403 });
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ message: "Origem invalida." }, { status: 403 });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function rateLimitResponse(key: string, limit = 10, windowMs = 15 * 60 * 1000) {
|
||||
const result = checkRateLimit(key, limit, windowMs);
|
||||
|
||||
if (!result.allowed) {
|
||||
return NextResponse.json({ message: "Muitas tentativas. Tente novamente mais tarde." }, { status: 429 });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { mkdir, unlink, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const allowedExtensions = new Set([
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".webp",
|
||||
".svg",
|
||||
".pdf",
|
||||
".docx",
|
||||
".doc",
|
||||
".xlsx",
|
||||
".xls",
|
||||
".pptx",
|
||||
".zip",
|
||||
".mp4",
|
||||
".mov"
|
||||
]);
|
||||
|
||||
const allowedMimes = new Set([
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/zip",
|
||||
"video/mp4",
|
||||
"video/quicktime"
|
||||
]);
|
||||
|
||||
export function getUploadDir() {
|
||||
return process.env.UPLOAD_DIR ?? path.join(process.cwd(), "storage", "uploads");
|
||||
}
|
||||
|
||||
export function getUploadMaxBytes() {
|
||||
const maxMb = Number(process.env.UPLOAD_MAX_MB ?? 25);
|
||||
return maxMb * 1024 * 1024;
|
||||
}
|
||||
|
||||
function hasMagicBytes(buffer: Buffer, mime: string, extension: string) {
|
||||
if (mime === "image/png") {
|
||||
return buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
}
|
||||
|
||||
if (mime === "image/jpeg") {
|
||||
return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
||||
}
|
||||
|
||||
if (mime === "image/webp") {
|
||||
return buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP";
|
||||
}
|
||||
|
||||
if (mime === "application/pdf") {
|
||||
return buffer.subarray(0, 4).toString("ascii") === "%PDF";
|
||||
}
|
||||
|
||||
if (mime === "video/mp4" || mime === "video/quicktime") {
|
||||
return buffer.subarray(4, 8).toString("ascii") === "ftyp";
|
||||
}
|
||||
|
||||
if (extension === ".zip" || extension === ".docx" || extension === ".xlsx" || extension === ".pptx") {
|
||||
return buffer[0] === 0x50 && buffer[1] === 0x4b;
|
||||
}
|
||||
|
||||
if (extension === ".doc" || extension === ".xls") {
|
||||
return buffer[0] === 0xd0 && buffer[1] === 0xcf && buffer[2] === 0x11 && buffer[3] === 0xe0;
|
||||
}
|
||||
|
||||
if (mime === "image/svg+xml") {
|
||||
const start = buffer.subarray(0, 512).toString("utf8").trimStart().toLowerCase();
|
||||
return start.startsWith("<svg") || start.startsWith("<?xml");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function validateAndStoreUpload(file: File) {
|
||||
const originalName = file.name;
|
||||
const extension = path.extname(originalName).toLowerCase();
|
||||
const mime = file.type;
|
||||
const maxBytes = getUploadMaxBytes();
|
||||
|
||||
if (!allowedExtensions.has(extension)) {
|
||||
throw new Error("Tipo de arquivo nao permitido.");
|
||||
}
|
||||
|
||||
if (!allowedMimes.has(mime)) {
|
||||
throw new Error("MIME do arquivo nao permitido.");
|
||||
}
|
||||
|
||||
if (file.size <= 0) {
|
||||
throw new Error("Arquivo vazio.");
|
||||
}
|
||||
|
||||
if (file.size > maxBytes) {
|
||||
throw new Error(`Arquivo maior que o limite de ${Math.round(maxBytes / 1024 / 1024)} MB.`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (!hasMagicBytes(buffer, mime, extension)) {
|
||||
throw new Error("Conteudo do arquivo nao confere com o tipo informado.");
|
||||
}
|
||||
|
||||
const uploadDir = getUploadDir();
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
|
||||
const storedName = `${randomUUID()}${extension}`;
|
||||
const storedPath = path.join(uploadDir, storedName);
|
||||
|
||||
await writeFile(storedPath, buffer);
|
||||
|
||||
return {
|
||||
originalName,
|
||||
storedPath,
|
||||
mime,
|
||||
size: file.size
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeStoredFile(filePath: string) {
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch {
|
||||
// A ausencia do arquivo nao deve impedir a remocao do registro.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { z } from "zod";
|
||||
import { onlyDigits } from "@/lib/crypto";
|
||||
|
||||
function hasSameDigits(value: string) {
|
||||
return /^(\d)\1+$/.test(value);
|
||||
}
|
||||
|
||||
export function isValidCpf(value: string) {
|
||||
const cpf = onlyDigits(value);
|
||||
|
||||
if (cpf.length !== 11 || hasSameDigits(cpf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const calcDigit = (base: string, factor: number) => {
|
||||
let total = 0;
|
||||
|
||||
for (const digit of base) {
|
||||
total += Number(digit) * factor;
|
||||
factor -= 1;
|
||||
}
|
||||
|
||||
const rest = (total * 10) % 11;
|
||||
return rest === 10 ? 0 : rest;
|
||||
};
|
||||
|
||||
const firstDigit = calcDigit(cpf.slice(0, 9), 10);
|
||||
const secondDigit = calcDigit(cpf.slice(0, 10), 11);
|
||||
|
||||
return firstDigit === Number(cpf[9]) && secondDigit === Number(cpf[10]);
|
||||
}
|
||||
|
||||
export function isValidCnpj(value: string) {
|
||||
const cnpj = onlyDigits(value);
|
||||
|
||||
if (cnpj.length !== 14 || hasSameDigits(cnpj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const calcDigit = (base: string, weights: number[]) => {
|
||||
const total = base
|
||||
.split("")
|
||||
.reduce((sum, digit, index) => sum + Number(digit) * weights[index], 0);
|
||||
const rest = total % 11;
|
||||
return rest < 2 ? 0 : 11 - rest;
|
||||
};
|
||||
|
||||
const firstDigit = calcDigit(cnpj.slice(0, 12), [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
||||
const secondDigit = calcDigit(cnpj.slice(0, 13), [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
||||
|
||||
return firstDigit === Number(cnpj[12]) && secondDigit === Number(cnpj[13]);
|
||||
}
|
||||
|
||||
export function normalizeWhatsapp(value: string) {
|
||||
const digits = onlyDigits(value);
|
||||
const withoutCountry = digits.startsWith("55") && digits.length === 13 ? digits.slice(2) : digits;
|
||||
|
||||
if (withoutCountry.length !== 11) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `+55${withoutCountry}`;
|
||||
}
|
||||
|
||||
export const cadastroSchema = z
|
||||
.object({
|
||||
nomeCompleto: z.string().trim().min(3, "Informe seu nome completo."),
|
||||
cpf: z.string().refine(isValidCpf, "Informe um CPF valido."),
|
||||
whatsapp: z.string().refine((value) => normalizeWhatsapp(value) !== null, "Informe um WhatsApp com DDD."),
|
||||
email: z.string().trim().email("Informe um e-mail valido.").toLowerCase(),
|
||||
senha: z.string().min(8, "A senha precisa ter pelo menos 8 caracteres."),
|
||||
confirmarSenha: z.string(),
|
||||
tipo: z.enum(["PF", "PJ"]),
|
||||
razaoSocial: z.string().trim().optional(),
|
||||
cnpj: z.string().optional(),
|
||||
cep: z.string().optional(),
|
||||
logradouro: z.string().optional(),
|
||||
numero: z.string().optional(),
|
||||
complemento: z.string().optional(),
|
||||
bairro: z.string().optional(),
|
||||
cidade: z.string().optional(),
|
||||
uf: z.string().optional(),
|
||||
lgpdAceite: z.literal("on", {
|
||||
errorMap: () => ({ message: "Aceite a Politica de Privacidade para continuar." })
|
||||
})
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.senha !== data.confirmarSenha) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["confirmarSenha"],
|
||||
message: "As senhas nao conferem."
|
||||
});
|
||||
}
|
||||
|
||||
if (data.tipo === "PJ") {
|
||||
if (!data.razaoSocial) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["razaoSocial"],
|
||||
message: "Informe a razao social."
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.cnpj || !isValidCnpj(data.cnpj)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["cnpj"],
|
||||
message: "Informe um CNPJ valido."
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type CadastroInput = z.infer<typeof cadastroSchema>;
|
||||
Reference in New Issue
Block a user