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,97 @@
"use server";
import { z } from "zod";
import { getBriefingDoCliente } from "@/lib/briefing-service";
import { db } from "@/lib/db";
const respostaSchema = z.object({
texto: z.string().optional(),
complemento: z.string().optional(),
selecionadas: z.array(z.string()).optional()
});
const salvarRespostasSchema = z.object({
perguntas: z.record(respostaSchema)
});
export type SalvarRespostasResult = {
ok: boolean;
savedAt?: string;
message?: string;
};
function hasValue(resposta: z.infer<typeof respostaSchema>) {
return Boolean(
resposta.texto?.trim() ||
resposta.complemento?.trim() ||
(resposta.selecionadas && resposta.selecionadas.length > 0)
);
}
export async function salvarRespostas(
briefingId: string,
secaoId: string,
valores: unknown
): Promise<SalvarRespostasResult> {
const parsed = salvarRespostasSchema.safeParse(valores);
if (!parsed.success) {
return { ok: false, message: "Dados invalidos para salvar." };
}
const briefing = await getBriefingDoCliente(briefingId);
if (briefing.status !== "RASCUNHO") {
return { ok: false, message: "Este briefing ja foi enviado e nao aceita novas alteracoes." };
}
const perguntasDaSecao = await db.pergunta.findMany({
where: {
secaoId,
ativo: true
},
select: { id: true }
});
const perguntaIds = new Set(perguntasDaSecao.map((pergunta) => pergunta.id));
await db.$transaction(
Object.entries(parsed.data.perguntas)
.filter(([perguntaId]) => perguntaIds.has(perguntaId))
.map(([perguntaId, resposta]) => {
if (!hasValue(resposta)) {
return db.resposta.deleteMany({
where: {
briefingId,
perguntaId
}
});
}
return db.resposta.upsert({
where: {
briefingId_perguntaId: {
briefingId,
perguntaId
}
},
update: {
texto: resposta.texto?.trim() || null,
complemento: resposta.complemento?.trim() || null,
selecionadas: resposta.selecionadas?.length ? resposta.selecionadas : null
},
create: {
briefingId,
perguntaId,
texto: resposta.texto?.trim() || null,
complemento: resposta.complemento?.trim() || null,
selecionadas: resposta.selecionadas?.length ? resposta.selecionadas : null
}
});
})
);
return {
ok: true,
savedAt: new Date().toISOString()
};
}
@@ -0,0 +1,108 @@
import Link from "next/link";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { getSecaoBriefing } from "@/lib/briefing-service";
import { SecaoForm } from "./secao-form";
export default async function SecaoBriefingPage({
params
}: {
params: { id: string; secao: string };
}) {
const ordemSecao = Number(params.secao);
const { briefing, secao, secoes, progressoGeral, anterior, proxima } = await getSecaoBriefing(
params.id,
ordemSecao
);
const percentual =
progressoGeral.total > 0 ? Math.round((progressoGeral.respondidas / progressoGeral.total) * 100) : 0;
return (
<main className="container py-8">
<div className="mb-6">
<Button asChild variant="ghost" className="mb-4 px-0">
<Link href={`/briefings/${briefing.id}`}>
<ChevronLeft className="mr-2 h-4 w-4" />
Voltar para abertura
</Link>
</Button>
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<Badge variant="muted">Secao {secao.ordem} de {secoes.length}</Badge>
<h1 className="mt-3 text-3xl font-semibold">{secao.titulo}</h1>
{secao.descricao ? <p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">{secao.descricao}</p> : null}
</div>
<div className="min-w-56 space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">Progresso geral</span>
<span className="text-muted-foreground">{progressoGeral.respondidas}/{progressoGeral.total}</span>
</div>
<Progress value={percentual} />
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-[280px_1fr]">
<aside className="lg:sticky lg:top-6 lg:self-start">
<Card>
<CardContent className="p-3">
<nav aria-label="Secoes do briefing" className="grid gap-1">
{secoes.map((item) => (
<Link
key={item.id}
href={`/briefings/${briefing.id}/${item.ordem}`}
className={`rounded-md px-3 py-2 text-sm transition ${
item.ordem === secao.ordem
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
}`}
>
<span className="block font-medium">{item.ordem}. {item.titulo}</span>
<span className={item.ordem === secao.ordem ? "text-primary-foreground/80" : "text-muted-foreground"}>
{item.respondidas}/{item.total} respondidas
</span>
</Link>
))}
</nav>
</CardContent>
</Card>
</aside>
<div className="space-y-6">
<SecaoForm
briefingId={briefing.id}
secaoId={secao.id}
perguntas={secao.perguntas}
readOnly={briefing.status !== "RASCUNHO"}
/>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-between">
{anterior ? (
<Button asChild variant="outline">
<Link href={`/briefings/${briefing.id}/${anterior.ordem}`}>
<ChevronLeft className="mr-2 h-4 w-4" />
Secao anterior
</Link>
</Button>
) : <span />}
{proxima ? (
<Button asChild>
<Link href={`/briefings/${briefing.id}/${proxima.ordem}`}>
Proxima secao
<ChevronRight className="ml-2 h-4 w-4" />
</Link>
</Button>
) : (
<Button asChild>
<Link href={`/briefings/${briefing.id}/revisao`}>Ir para revisao</Link>
</Button>
)}
</div>
</div>
</div>
</main>
);
}
@@ -0,0 +1,205 @@
"use client";
import type { Ajuda, Anexo, Pergunta, Resposta } from "@prisma/client";
import { useEffect, useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { PerguntaField } from "@/components/form/PerguntaField";
import { Button } from "@/components/ui/button";
import { salvarRespostas } from "./actions";
type PerguntaComDados = Pergunta & {
ajuda: Ajuda | null;
anexos?: Anexo[];
respostas: Resposta[];
};
type Props = {
briefingId: string;
secaoId: string;
perguntas: PerguntaComDados[];
readOnly?: boolean;
};
type FormValues = {
perguntas: Record<
string,
{
texto?: string;
selecionadas?: string[];
complemento?: string;
}
>;
};
function getDefaultValues(perguntas: PerguntaComDados[]): FormValues {
return {
perguntas: perguntas.reduce<FormValues["perguntas"]>((acc, pergunta) => {
const resposta = pergunta.respostas[0];
const selecionadas = Array.isArray(resposta?.selecionadas)
? resposta.selecionadas.filter((item): item is string => typeof item === "string")
: [];
acc[pergunta.id] = {
texto: resposta?.texto ?? "",
selecionadas,
complemento: resposta?.complemento ?? ""
};
return acc;
}, {})
};
}
type SaveStatus = "idle" | "dirty" | "saving" | "saved" | "error";
function formatTime(value: string) {
return new Intl.DateTimeFormat("pt-BR", {
hour: "2-digit",
minute: "2-digit"
}).format(new Date(value));
}
export function SecaoForm({ briefingId, secaoId, perguntas, readOnly = false }: Props) {
const [status, setStatus] = useState<SaveStatus>("idle");
const [savedAt, setSavedAt] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [, startTransition] = useTransition();
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const router = useRouter();
const form = useForm<FormValues>({
defaultValues: getDefaultValues(perguntas)
});
const watchedValues = form.watch();
async function saveNow(values: FormValues) {
if (readOnly) {
return true;
}
setStatus("saving");
setMessage(null);
const result = await salvarRespostas(briefingId, secaoId, values);
startTransition(() => {
if (!result.ok) {
setStatus("error");
setMessage(result.message ?? "Erro ao salvar. Tente novamente.");
return;
}
form.reset(values);
setStatus("saved");
setSavedAt(result.savedAt ?? new Date().toISOString());
});
return result.ok;
}
useEffect(() => {
if (readOnly || !form.formState.isDirty) {
return;
}
setStatus("dirty");
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
void saveNow(form.getValues());
}, 800);
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [watchedValues, form, readOnly]);
useEffect(() => {
function handleBeforeUnload(event: BeforeUnloadEvent) {
if (form.formState.isDirty && status !== "saved") {
event.preventDefault();
}
}
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [form.formState.isDirty, status]);
useEffect(() => {
async function handleClick(event: MouseEvent) {
const link = (event.target as HTMLElement).closest("a");
if (!link || readOnly || !form.formState.isDirty) {
return;
}
const href = link.getAttribute("href");
if (!href || !href.startsWith(`/briefings/${briefingId}`)) {
return;
}
event.preventDefault();
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
const saved = await saveNow(form.getValues());
if (saved) {
router.push(href);
}
}
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, [briefingId, form, readOnly, router]);
return (
<FormProvider {...form}>
<form className="space-y-4">
<div className="sticky top-0 z-20 rounded-lg border bg-card p-3 text-sm shadow-sm">
{readOnly ? (
<span className="text-muted-foreground">Briefing enviado. Respostas em modo leitura.</span>
) : status === "saving" ? (
<span className="text-muted-foreground">Salvando...</span>
) : status === "saved" && savedAt ? (
<span className="text-muted-foreground">Salvo as {formatTime(savedAt)}</span>
) : status === "error" ? (
<span className="text-destructive">{message ?? "Erro ao salvar — tentar de novo"}</span>
) : status === "dirty" ? (
<span className="text-muted-foreground">Alteracoes pendentes...</span>
) : (
<span className="text-muted-foreground">Rascunho pronto para edicao.</span>
)}
</div>
{perguntas.map((pergunta, index) => (
<fieldset key={pergunta.id} disabled={readOnly} className="disabled:opacity-75">
<PerguntaField
briefingId={briefingId}
pergunta={pergunta}
index={index}
readOnly={readOnly}
/>
</fieldset>
))}
{!readOnly ? (
<div className="flex flex-col gap-3 sm:flex-row">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Descartar alteracoes locais
</Button>
<Button type="button" onClick={() => saveNow(form.getValues())}>
Salvar agora
</Button>
</div>
) : null}
</form>
</FormProvider>
);
}
@@ -0,0 +1,37 @@
"use client";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
type Props = {
empresaMarca: string;
responsavel: string;
contato: string;
data: string;
};
export function CabecalhoBriefingForm({ empresaMarca, responsavel, contato, data }: Props) {
return (
<form className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="empresaMarca">Empresa / marca</Label>
<Input id="empresaMarca" name="empresaMarca" defaultValue={empresaMarca} />
</div>
<div className="space-y-2">
<Label htmlFor="responsavel">Responsavel</Label>
<Input id="responsavel" name="responsavel" defaultValue={responsavel} />
</div>
<div className="space-y-2">
<Label htmlFor="data">Data</Label>
<Input id="data" name="data" type="date" defaultValue={data} />
</div>
<div className="space-y-2">
<Label htmlFor="contato">Contato</Label>
<Input id="contato" name="contato" defaultValue={contato} />
</div>
<p className="text-xs leading-5 text-muted-foreground">
Estes campos serao salvos junto com o rascunho na etapa de autosave.
</p>
</form>
);
}
+110
View File
@@ -0,0 +1,110 @@
import Link from "next/link";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { getAberturaBriefing } from "@/lib/briefing-service";
import { CabecalhoBriefingForm } from "./cabecalho-briefing-form";
function asStringArray(value: unknown) {
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
}
export default async function AberturaBriefingPage({ params }: { params: { id: string } }) {
const { briefing, progresso, comoPreencher, avisoSenhas } = await getAberturaBriefing(params.id);
const totalPerguntas = progresso.reduce((sum, secao) => sum + secao.total, 0);
const totalRespondidas = progresso.reduce((sum, secao) => sum + secao.respondidas, 0);
const percentual = totalPerguntas > 0 ? Math.round((totalRespondidas / totalPerguntas) * 100) : 0;
const primeiraPendente = progresso.find((secao) => secao.respondidas < secao.total) ?? progresso[0];
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">{briefing.titulo}</h1>
<p className="mt-2 text-sm text-muted-foreground">
Revise as orientacoes e continue pela secao indicada.
</p>
</div>
{primeiraPendente ? (
<Button asChild>
<Link href={`/briefings/${briefing.id}/${primeiraPendente.ordem}`}>Continuar de onde parei</Link>
</Button>
) : null}
</div>
<div className="mb-6 space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">Progresso geral</span>
<span className="text-muted-foreground">{totalRespondidas}/{totalPerguntas}</span>
</div>
<Progress value={percentual} />
</div>
<div className="grid gap-6 lg:grid-cols-[360px_1fr]">
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Cabecalho do briefing</CardTitle>
</CardHeader>
<CardContent>
<CabecalhoBriefingForm
empresaMarca={briefing.cliente.razaoSocial ?? briefing.cliente.nomeCompleto}
responsavel={briefing.cliente.nomeCompleto}
contato={`${briefing.cliente.user.email} | ${briefing.cliente.whatsapp}`}
data={new Date().toISOString().slice(0, 10)}
/>
</CardContent>
</Card>
{avisoSenhas ? (
<Alert>
<AlertTitle>Aviso importante</AlertTitle>
<AlertDescription>{String(avisoSenhas)}</AlertDescription>
</Alert>
) : null}
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">Como preencher</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-3 text-sm leading-6 text-muted-foreground">
{asStringArray(comoPreencher).map((item) => (
<li key={item} className="rounded-md bg-muted p-3">{item}</li>
))}
</ul>
</CardContent>
</Card>
<div className="grid gap-3">
{progresso.map((secao) => {
const secaoPercentual = secao.total > 0 ? Math.round((secao.respondidas / secao.total) * 100) : 0;
return (
<Link
key={secao.id}
href={`/briefings/${briefing.id}/${secao.ordem}`}
className="rounded-lg border bg-card p-4 transition hover:border-primary"
>
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<p className="font-medium">{secao.ordem}. {secao.titulo}</p>
{secao.descricao ? <p className="mt-1 text-sm text-muted-foreground">{secao.descricao}</p> : null}
</div>
<Badge variant="muted">{secao.respondidas}/{secao.total}</Badge>
</div>
<Progress value={secaoPercentual} />
</Link>
);
})}
</div>
</div>
</div>
</main>
);
}
@@ -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>
);
}