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,28 @@
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { revelarCpfAction, type AdminActionState } from "@/app/(admin)/admin/actions";
import { Button } from "@/components/ui/button";
const initialState: AdminActionState = { ok: false };
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" variant="outline" disabled={pending}>
{pending ? "Registrando..." : "Mostrar CPF"}
</Button>
);
}
export function MostrarCpf({ clienteId }: { clienteId: string }) {
const action = revelarCpfAction.bind(null, clienteId);
const [state, formAction] = useFormState(action, initialState);
return (
<form action={formAction} className="space-y-2">
<SubmitButton />
{state.value ? <p className="text-sm font-medium">{state.value}</p> : null}
</form>
);
}
+101
View File
@@ -0,0 +1,101 @@
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { decryptCpf } from "@/lib/crypto";
import { getClienteAdmin, maskCpf, whatsappLink } from "@/lib/admin-service";
import { MostrarCpf } from "./mostrar-cpf";
export default async function ClienteAdminDetalhePage({ params }: { params: { id: string } }) {
const cliente = await getClienteAdmin(params.id);
const cpfMascarado = maskCpf(decryptCpf(cliente.cpfCifrado));
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">{cliente.tipo}</Badge>
<h1 className="mt-3 text-3xl font-semibold">{cliente.nomeCompleto}</h1>
<p className="mt-2 text-sm text-muted-foreground">{cliente.user.email}</p>
</div>
<Button asChild variant="outline">
<Link href="/admin/clientes">Voltar</Link>
</Button>
</div>
<div className="grid gap-6 lg:grid-cols-[360px_1fr]">
<Card>
<CardHeader>
<CardTitle>Dados cadastrais</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div>
<p className="font-medium">CPF</p>
<p className="text-muted-foreground">{cpfMascarado}</p>
<div className="mt-2">
<MostrarCpf clienteId={cliente.id} />
</div>
</div>
<div>
<p className="font-medium">WhatsApp</p>
<a className="text-primary hover:underline" href={whatsappLink(cliente.whatsapp)} target="_blank" rel="noreferrer">
{cliente.whatsapp}
</a>
</div>
{cliente.razaoSocial ? (
<div>
<p className="font-medium">Razao social</p>
<p className="text-muted-foreground">{cliente.razaoSocial}</p>
</div>
) : null}
{cliente.cnpj ? (
<div>
<p className="font-medium">CNPJ</p>
<p className="text-muted-foreground">{cliente.cnpj}</p>
</div>
) : null}
<div>
<p className="font-medium">Endereco</p>
<p className="text-muted-foreground">
{[cliente.logradouro, cliente.numero, cliente.bairro, cliente.cidade, cliente.uf]
.filter(Boolean)
.join(", ") || "Nao informado"}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Briefings</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Titulo</TableHead>
<TableHead>Status</TableHead>
<TableHead>Atualizado</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cliente.briefings.map((briefing) => (
<TableRow key={briefing.id}>
<TableCell>
<Link className="font-medium text-primary hover:underline" href={`/admin/briefings/${briefing.id}`}>
{briefing.titulo}
</Link>
</TableCell>
<TableCell><Badge variant="muted">{briefing.status}</Badge></TableCell>
<TableCell>{briefing.atualizadoEm.toLocaleString("pt-BR")}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</main>
);
}
+89
View File
@@ -0,0 +1,89 @@
import Link from "next/link";
import { irParaClientes } from "@/app/(admin)/admin/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { listarClientesAdmin, maskCpf } from "@/lib/admin-service";
import { decryptCpf } from "@/lib/crypto";
export default async function ClientesAdminPage({
searchParams
}: {
searchParams: { busca?: string; tipo?: "PF" | "PJ" };
}) {
const clientes = await listarClientesAdmin({
busca: searchParams.busca,
tipo: searchParams.tipo
});
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">Clientes</h1>
<p className="mt-2 text-sm text-muted-foreground">Busque clientes e acompanhe seus briefings.</p>
</div>
<Button asChild variant="outline">
<Link href="/admin">Voltar</Link>
</Button>
</div>
<Card className="mb-6">
<CardContent className="p-4">
<form action={irParaClientes} className="grid gap-4 sm:grid-cols-[1fr_160px_auto] sm:items-end">
<div className="space-y-2">
<Label htmlFor="busca">Busca</Label>
<Input id="busca" name="busca" defaultValue={searchParams.busca ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="tipo">Tipo</Label>
<select id="tipo" name="tipo" defaultValue={searchParams.tipo ?? ""} className="h-10 w-full rounded-md border bg-background px-3 text-sm">
<option value="">Todos</option>
<option value="PF">PF</option>
<option value="PJ">PJ</option>
</select>
</div>
<Button type="submit">Filtrar</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{clientes.length} cliente(s)</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>E-mail</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>CPF</TableHead>
<TableHead>Briefings</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{clientes.map((cliente) => (
<TableRow key={cliente.id}>
<TableCell>
<Link href={`/admin/clientes/${cliente.id}`} className="font-medium text-primary hover:underline">
{cliente.nomeCompleto}
</Link>
</TableCell>
<TableCell>{cliente.user.email}</TableCell>
<TableCell><Badge variant="muted">{cliente.tipo}</Badge></TableCell>
<TableCell>{maskCpf(decryptCpf(cliente.cpfCifrado))}</TableCell>
<TableCell>{cliente.briefings.length}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</main>
);
}