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
+87
View File
@@ -0,0 +1,87 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { getClienteConta } from "@/lib/account-service";
import { db } from "@/lib/db";
import { normalizeWhatsapp } from "@/lib/validators";
export type ContaState = {
ok: boolean;
message?: string;
};
const atualizarContaSchema = z.object({
nomeCompleto: z.string().trim().min(3),
whatsapp: z.string().trim().min(10),
razaoSocial: z.string().trim().optional(),
cep: z.string().trim().optional(),
logradouro: z.string().trim().optional(),
numero: z.string().trim().optional(),
complemento: z.string().trim().optional(),
bairro: z.string().trim().optional(),
cidade: z.string().trim().optional(),
uf: z.string().trim().max(2).optional()
});
export async function atualizarConta(
_state: ContaState,
formData: FormData
): Promise<ContaState> {
const cliente = await getClienteConta();
const parsed = atualizarContaSchema.safeParse({
nomeCompleto: formData.get("nomeCompleto"),
whatsapp: formData.get("whatsapp"),
razaoSocial: formData.get("razaoSocial") || undefined,
cep: formData.get("cep") || undefined,
logradouro: formData.get("logradouro") || undefined,
numero: formData.get("numero") || undefined,
complemento: formData.get("complemento") || undefined,
bairro: formData.get("bairro") || undefined,
cidade: formData.get("cidade") || undefined,
uf: formData.get("uf") || undefined
});
if (!parsed.success) {
return { ok: false, message: "Revise os dados informados." };
}
const whatsapp = normalizeWhatsapp(parsed.data.whatsapp);
if (!whatsapp) {
return { ok: false, message: "Informe um WhatsApp com DDD." };
}
await db.cliente.update({
where: { id: cliente.id },
data: {
nomeCompleto: parsed.data.nomeCompleto,
whatsapp,
razaoSocial: parsed.data.razaoSocial ?? null,
cep: parsed.data.cep ?? null,
logradouro: parsed.data.logradouro ?? null,
numero: parsed.data.numero ?? null,
complemento: parsed.data.complemento ?? null,
bairro: parsed.data.bairro ?? null,
cidade: parsed.data.cidade ?? null,
uf: parsed.data.uf?.toUpperCase() ?? null
}
});
revalidatePath("/minha-conta");
return { ok: true, message: "Dados atualizados." };
}
export async function solicitarExclusao(formData: FormData) {
const cliente = await getClienteConta();
const motivo = String(formData.get("motivo") ?? "").trim();
await db.solicitacaoExclusao.create({
data: {
clienteId: cliente.id,
motivo: motivo || null
}
});
revalidatePath("/minha-conta");
}
+87
View File
@@ -0,0 +1,87 @@
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { atualizarConta, type ContaState } from "./actions";
type Props = {
cliente: {
nomeCompleto: string;
whatsapp: string;
razaoSocial: string | null;
cep: string | null;
logradouro: string | null;
numero: string | null;
complemento: string | null;
bairro: string | null;
cidade: string | null;
uf: string | null;
};
};
const initialState: ContaState = { ok: false };
function SubmitButton() {
const { pending } = useFormStatus();
return <Button type="submit" disabled={pending}>{pending ? "Salvando..." : "Salvar dados"}</Button>;
}
export function ContaForm({ cliente }: Props) {
const [state, action] = useFormState(atualizarConta, initialState);
return (
<form action={action} className="space-y-4">
{state.message ? (
<Alert variant={state.ok ? "default" : "destructive"}>
<AlertDescription>{state.message}</AlertDescription>
</Alert>
) : null}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="nomeCompleto">Nome completo</Label>
<Input id="nomeCompleto" name="nomeCompleto" defaultValue={cliente.nomeCompleto} />
</div>
<div className="space-y-2">
<Label htmlFor="whatsapp">WhatsApp</Label>
<Input id="whatsapp" name="whatsapp" defaultValue={cliente.whatsapp} />
</div>
<div className="space-y-2">
<Label htmlFor="razaoSocial">Razao social</Label>
<Input id="razaoSocial" name="razaoSocial" defaultValue={cliente.razaoSocial ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="cep">CEP</Label>
<Input id="cep" name="cep" defaultValue={cliente.cep ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="logradouro">Logradouro</Label>
<Input id="logradouro" name="logradouro" defaultValue={cliente.logradouro ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="numero">Numero</Label>
<Input id="numero" name="numero" defaultValue={cliente.numero ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="complemento">Complemento</Label>
<Input id="complemento" name="complemento" defaultValue={cliente.complemento ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="bairro">Bairro</Label>
<Input id="bairro" name="bairro" defaultValue={cliente.bairro ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label>
<Input id="cidade" name="cidade" defaultValue={cliente.cidade ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="uf">UF</Label>
<Input id="uf" name="uf" defaultValue={cliente.uf ?? ""} maxLength={2} />
</div>
</div>
<SubmitButton />
</form>
);
}
+77
View File
@@ -0,0 +1,77 @@
import Link from "next/link";
import { solicitarExclusao } from "@/app/(cliente)/minha-conta/actions";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { getClienteConta } from "@/lib/account-service";
import { ContaForm } from "./conta-form";
export default async function MinhaContaPage() {
const cliente = await getClienteConta();
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>
<h1 className="text-3xl font-semibold">Minha conta</h1>
<p className="mt-2 text-sm text-muted-foreground">Gerencie seus dados e direitos LGPD.</p>
</div>
<Button asChild variant="outline">
<Link href="/briefings">Voltar</Link>
</Button>
</div>
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
<Card>
<CardHeader>
<CardTitle>Dados cadastrais</CardTitle>
</CardHeader>
<CardContent>
<ContaForm cliente={cliente} />
</CardContent>
</Card>
<aside className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Meus dados</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm leading-6 text-muted-foreground">
Baixe uma copia em JSON dos dados associados a sua conta.
</p>
<Button asChild>
<a href="/api/minha-conta/exportar">Baixar JSON</a>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Solicitar exclusao</CardTitle>
</CardHeader>
<CardContent>
<form action={solicitarExclusao} className="space-y-3">
<div className="space-y-2">
<Label htmlFor="motivo">Motivo opcional</Label>
<Textarea id="motivo" name="motivo" />
</div>
<Button type="submit" variant="outline">Solicitar exclusao</Button>
</form>
{cliente.solicitacoesExclusao.length > 0 ? (
<div className="mt-4 space-y-2">
{cliente.solicitacoesExclusao.map((solicitacao) => (
<p key={solicitacao.id} className="text-sm text-muted-foreground">
{solicitacao.status} em {solicitacao.criadoEm.toLocaleDateString("pt-BR")}
</p>
))}
</div>
) : null}
</CardContent>
</Card>
</aside>
</div>
</main>
);
}