Initial SeConectai briefing system

This commit is contained in:
2026-10-04 10:42:23 -03:00
commit 45cd8d2f03
163 changed files with 12659 additions and 0 deletions
@@ -0,0 +1,67 @@
"use server";
import { redirect } from "next/navigation";
import { getBriefingDoCliente } from "@/lib/briefing-service";
import { db } from "@/lib/db";
import { notificarBriefingEnviado } from "@/lib/notifications";
export type EnvioBriefingState = {
ok: boolean;
message?: string;
};
export async function enviarBriefing(
briefingId: string,
_state: EnvioBriefingState,
formData: FormData
): Promise<EnvioBriefingState> {
const briefing = await getBriefingDoCliente(briefingId);
if (briefing.status !== "RASCUNHO") {
return { ok: false, message: "Este briefing ja foi enviado." };
}
if (formData.get("confirmacaoCliente") !== "on") {
return { ok: false, message: "Confirme a declaracao do cliente para enviar." };
}
const totalPerguntas = await db.pergunta.count({
where: { ativo: true }
});
const respostas = await db.resposta.findMany({
where: { briefingId: briefing.id },
select: {
texto: true,
complemento: true,
selecionadas: true
}
});
const respondidas = respostas.filter((resposta) => {
const selecionadas = Array.isArray(resposta.selecionadas) ? resposta.selecionadas : [];
return Boolean(resposta.texto?.trim() || resposta.complemento?.trim() || selecionadas.length > 0);
}).length;
if (respondidas < totalPerguntas && formData.get("confirmarBrancos") !== "on") {
return {
ok: false,
message: "Confirme que deseja enviar mesmo com perguntas em branco."
};
}
await db.briefing.update({
where: { id: briefing.id },
data: {
status: "ENVIADO",
confirmadoEm: new Date(),
enviadoEm: new Date()
}
});
try {
await notificarBriefingEnviado(briefing.id);
} catch {
// Falha de notificacao nao pode impedir o envio do briefing.
}
redirect(`/briefings/${briefing.id}/revisao`);
}
@@ -0,0 +1,66 @@
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { enviarBriefing, type EnvioBriefingState } from "./actions";
const initialState: EnvioBriefingState = { ok: false };
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending ? "Enviando..." : "Enviar briefing"}
</Button>
);
}
export function EnvioBriefingForm({
briefingId,
confirmacaoCliente,
perguntasEmBranco
}: {
briefingId: string;
confirmacaoCliente: string;
perguntasEmBranco: number;
}) {
const enviarComId = enviarBriefing.bind(null, briefingId);
const [state, action] = useFormState(enviarComId, initialState);
return (
<form action={action} className="space-y-4 rounded-lg border bg-card p-4">
{state.message ? (
<Alert variant="destructive">
<AlertDescription>{state.message}</AlertDescription>
</Alert>
) : null}
{perguntasEmBranco > 0 ? (
<Alert>
<AlertDescription>
Existem {perguntasEmBranco} pergunta(s) em branco. Voce pode enviar mesmo assim se estiver tudo certo.
</AlertDescription>
</Alert>
) : null}
<p className="text-sm leading-6 text-muted-foreground">{confirmacaoCliente}</p>
<label className="flex items-start gap-3 rounded-md border p-3 text-sm leading-6">
<Checkbox name="confirmacaoCliente" required />
<span>Confirmo a declaracao acima e desejo enviar o briefing para analise.</span>
</label>
{perguntasEmBranco > 0 ? (
<label className="flex items-start gap-3 rounded-md border p-3 text-sm leading-6">
<Checkbox name="confirmarBrancos" required />
<span>Estou ciente de que ha perguntas em branco e quero enviar mesmo assim.</span>
</label>
) : null}
<SubmitButton />
</form>
);
}
@@ -0,0 +1,131 @@
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getRevisaoBriefing } from "@/lib/briefing-service";
import { EnvioBriefingForm } from "./envio-briefing-form";
function respostaTexto(resposta: {
texto: string | null;
complemento: string | null;
selecionadas: unknown;
}) {
const partes: string[] = [];
const selecionadas = Array.isArray(resposta.selecionadas)
? resposta.selecionadas.filter((item): item is string => typeof item === "string")
: [];
if (resposta.texto?.trim()) {
partes.push(resposta.texto.trim());
}
if (selecionadas.length > 0) {
partes.push(selecionadas.join(", "));
}
if (resposta.complemento?.trim()) {
partes.push(`Complemento: ${resposta.complemento.trim()}`);
}
return partes.join(" | ");
}
export default async function RevisaoBriefingPage({ params }: { params: { id: string } }) {
const { briefing, secoes, confirmacaoCliente } = await getRevisaoBriefing(params.id);
const perguntas = secoes.flatMap((secao) => secao.perguntas);
const perguntasEmBranco = perguntas.filter((pergunta) => {
const resposta = pergunta.respostas[0];
const anexos = "anexos" in pergunta && Array.isArray(pergunta.anexos) ? pergunta.anexos : [];
return (!resposta || !respostaTexto(resposta)) && anexos.length === 0;
}).length;
return (
<main className="container py-8">
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<Badge variant="muted">{briefing.status.replace("_", " ")}</Badge>
<h1 className="mt-3 text-3xl font-semibold">Revisao do briefing</h1>
<p className="mt-2 text-sm text-muted-foreground">
Confira as respostas antes de enviar para analise.
</p>
</div>
<Button asChild variant="outline">
<Link href={`/briefings/${briefing.id}`}>Voltar para abertura</Link>
</Button>
{briefing.status !== "RASCUNHO" ? (
<Button asChild>
<a href={`/api/briefings/${briefing.id}/pdf`}>Baixar PDF</a>
</Button>
) : null}
</div>
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
<div className="grid gap-4">
{secoes.map((secao) => (
<Card key={secao.id}>
<CardHeader>
<CardTitle className="text-lg">{secao.ordem}. {secao.titulo}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{secao.perguntas.map((pergunta) => {
const resposta = pergunta.respostas[0];
const texto = resposta ? respostaTexto(resposta) : "";
const anexos = "anexos" in pergunta && Array.isArray(pergunta.anexos) ? pergunta.anexos : [];
return (
<div key={pergunta.id} className="rounded-md border p-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<p className="text-sm font-medium">{pergunta.rotulo}</p>
{!texto && anexos.length === 0 ? <Badge variant="outline">Em branco</Badge> : null}
</div>
<p className="mt-2 whitespace-pre-wrap text-sm leading-6 text-muted-foreground">
{texto || "Sem resposta informada."}
</p>
{anexos.length > 0 ? (
<ul className="mt-3 space-y-1 text-sm text-muted-foreground">
{anexos.map((anexo) => (
<li key={anexo.id}>
<a className="text-primary underline-offset-4 hover:underline" href={`/api/anexos/${anexo.id}`}>
{anexo.nomeOriginal}
</a>
</li>
))}
</ul>
) : null}
</div>
);
})}
{briefing.status === "RASCUNHO" ? (
<Button asChild variant="outline">
<Link href={`/briefings/${briefing.id}/${secao.ordem}`}>Editar secao</Link>
</Button>
) : null}
</CardContent>
</Card>
))}
</div>
<aside className="lg:sticky lg:top-6 lg:self-start">
{briefing.status === "RASCUNHO" ? (
<EnvioBriefingForm
briefingId={briefing.id}
confirmacaoCliente={String(confirmacaoCliente ?? "")}
perguntasEmBranco={perguntasEmBranco}
/>
) : (
<Card>
<CardHeader>
<CardTitle className="text-lg">Briefing enviado</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-6 text-muted-foreground">
Este briefing foi enviado e esta bloqueado para edicao.
</p>
</CardContent>
</Card>
)}
</aside>
</div>
</main>
);
}