Initial SeConectai briefing system
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import type { StatusBriefing } from "@prisma/client";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
requireAdmin,
|
||||
revelarCpfCliente,
|
||||
atualizarStatusBriefing
|
||||
} from "@/lib/admin-service";
|
||||
import { reabrirBriefing } from "@/lib/briefing-service";
|
||||
|
||||
export type AdminActionState = {
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export async function revelarCpfAction(
|
||||
clienteId: string,
|
||||
_state: AdminActionState
|
||||
): Promise<AdminActionState> {
|
||||
const cpf = await revelarCpfCliente(clienteId);
|
||||
return { ok: true, value: cpf };
|
||||
}
|
||||
|
||||
export async function mudarStatusAction(formData: FormData) {
|
||||
await requireAdmin();
|
||||
const briefingId = String(formData.get("briefingId") ?? "");
|
||||
const status = String(formData.get("status") ?? "") as StatusBriefing;
|
||||
|
||||
if (!["ENVIADO", "EM_ANALISE", "CONCLUIDO"].includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await atualizarStatusBriefing(briefingId, status);
|
||||
revalidatePath(`/admin/briefings/${briefingId}`);
|
||||
}
|
||||
|
||||
export async function reabrirBriefingAction(formData: FormData) {
|
||||
await requireAdmin();
|
||||
const briefingId = String(formData.get("briefingId") ?? "");
|
||||
await reabrirBriefing(briefingId);
|
||||
revalidatePath(`/admin/briefings/${briefingId}`);
|
||||
}
|
||||
|
||||
export async function adicionarNotaInternaAction(formData: FormData) {
|
||||
const admin = await requireAdmin();
|
||||
const briefingId = String(formData.get("briefingId") ?? "");
|
||||
const texto = String(formData.get("texto") ?? "").trim();
|
||||
|
||||
if (!texto) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.notaInterna.create({
|
||||
data: {
|
||||
briefingId,
|
||||
adminId: admin.id,
|
||||
texto
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath(`/admin/briefings/${briefingId}`);
|
||||
}
|
||||
|
||||
const perguntaSchema = z.object({
|
||||
perguntaId: z.string().min(1),
|
||||
rotulo: z.string().trim().min(1),
|
||||
dica: z.string().trim().optional(),
|
||||
ordem: z.coerce.number().int().positive(),
|
||||
ativo: z.boolean(),
|
||||
obrigatoria: z.boolean(),
|
||||
oQueResponder: z.string().trim().min(1),
|
||||
porQuePerguntamos: z.string().trim().min(1),
|
||||
miniExemplo: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export async function salvarPerguntaAction(formData: FormData) {
|
||||
await requireAdmin();
|
||||
const parsed = perguntaSchema.safeParse({
|
||||
perguntaId: formData.get("perguntaId"),
|
||||
rotulo: formData.get("rotulo"),
|
||||
dica: formData.get("dica") || undefined,
|
||||
ordem: formData.get("ordem"),
|
||||
ativo: formData.get("ativo") === "on",
|
||||
obrigatoria: formData.get("obrigatoria") === "on",
|
||||
oQueResponder: formData.get("oQueResponder"),
|
||||
porQuePerguntamos: formData.get("porQuePerguntamos"),
|
||||
miniExemplo: formData.get("miniExemplo")
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.pergunta.update({
|
||||
where: { id: parsed.data.perguntaId },
|
||||
data: {
|
||||
rotulo: parsed.data.rotulo,
|
||||
dica: parsed.data.dica ?? null,
|
||||
ordem: parsed.data.ordem,
|
||||
ativo: parsed.data.ativo,
|
||||
obrigatoria: parsed.data.obrigatoria,
|
||||
ajuda: {
|
||||
upsert: {
|
||||
update: {
|
||||
oQueResponder: parsed.data.oQueResponder,
|
||||
porQuePerguntamos: parsed.data.porQuePerguntamos,
|
||||
miniExemplo: parsed.data.miniExemplo
|
||||
},
|
||||
create: {
|
||||
oQueResponder: parsed.data.oQueResponder,
|
||||
porQuePerguntamos: parsed.data.porQuePerguntamos,
|
||||
miniExemplo: parsed.data.miniExemplo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/admin/perguntas");
|
||||
}
|
||||
|
||||
export async function irParaClientes(formData: FormData) {
|
||||
const busca = String(formData.get("busca") ?? "").trim();
|
||||
const tipo = String(formData.get("tipo") ?? "").trim();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (busca) params.set("busca", busca);
|
||||
if (tipo) params.set("tipo", tipo);
|
||||
|
||||
redirect(`/admin/clientes${params.size ? `?${params.toString()}` : ""}`);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import Link from "next/link";
|
||||
import { adicionarNotaInternaAction, mudarStatusAction, reabrirBriefingAction } 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 { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getBriefingAdmin } from "@/lib/admin-service";
|
||||
import { formatFileSize } from "@/lib/format";
|
||||
|
||||
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 BriefingAdminPage({ params }: { params: { id: string } }) {
|
||||
const { briefing, secoes } = await getBriefingAdmin(params.id);
|
||||
|
||||
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}</Badge>
|
||||
<h1 className="mt-3 text-3xl font-semibold">{briefing.titulo}</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{briefing.cliente.nomeCompleto} | {briefing.cliente.user.email}
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/admin">Voltar</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a href={`/api/briefings/${briefing.id}/pdf`}>Baixar PDF</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
|
||||
<div className="space-y-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) : "";
|
||||
|
||||
return (
|
||||
<div key={pergunta.id} className="rounded-md border p-3">
|
||||
<p className="text-sm font-medium">{pergunta.rotulo}</p>
|
||||
<p className="mt-2 whitespace-pre-wrap text-sm leading-6 text-muted-foreground">
|
||||
{texto || "Sem resposta informada."}
|
||||
</p>
|
||||
{pergunta.anexos.length > 0 ? (
|
||||
<ul className="mt-3 space-y-1 text-sm">
|
||||
{pergunta.anexos.map((anexo) => (
|
||||
<li key={anexo.id}>
|
||||
<a className="text-primary hover:underline" href={`/api/anexos/${anexo.id}`}>
|
||||
{anexo.nomeOriginal} ({formatFileSize(anexo.tamanho)})
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<aside className="space-y-4 lg:sticky lg:top-6 lg:self-start">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Acoes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<form action={mudarStatusAction} className="grid gap-3">
|
||||
<input type="hidden" name="briefingId" value={briefing.id} />
|
||||
<select name="status" defaultValue={briefing.status} className="h-10 rounded-md border bg-background px-3 text-sm">
|
||||
<option value="ENVIADO">ENVIADO</option>
|
||||
<option value="EM_ANALISE">EM_ANALISE</option>
|
||||
<option value="CONCLUIDO">CONCLUIDO</option>
|
||||
</select>
|
||||
<Button type="submit">Atualizar status</Button>
|
||||
</form>
|
||||
<form action={reabrirBriefingAction}>
|
||||
<input type="hidden" name="briefingId" value={briefing.id} />
|
||||
<Button type="submit" variant="outline" className="w-full">Reabrir para edicao</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Nota interna</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<form action={adicionarNotaInternaAction} className="space-y-3">
|
||||
<input type="hidden" name="briefingId" value={briefing.id} />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="texto">Texto</Label>
|
||||
<Textarea id="texto" name="texto" required />
|
||||
</div>
|
||||
<Button type="submit">Adicionar nota</Button>
|
||||
</form>
|
||||
<div className="space-y-3">
|
||||
{briefing.notasInternas.map((nota) => (
|
||||
<div key={nota.id} className="rounded-md border p-3">
|
||||
<p className="text-sm leading-6">{nota.texto}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{nota.admin.email} | {nota.criadoEm.toLocaleString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { auth } from "@/auth";
|
||||
import { sair } from "@/app/(auth)/actions";
|
||||
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 { getAdminDashboard } from "@/lib/admin-service";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await auth();
|
||||
const dashboard = await getAdminDashboard();
|
||||
const countByStatus = Object.fromEntries(
|
||||
dashboard.porStatus.map((item) => [item.status, item._count._all])
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container py-8">
|
||||
<header className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Painel administrativo</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Area restrita para {session?.user.email}.
|
||||
</p>
|
||||
</div>
|
||||
<form action={sair}>
|
||||
<Button variant="outline" type="submit">
|
||||
Sair
|
||||
</Button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<div className="mb-6 flex flex-wrap gap-3">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/admin/clientes">Clientes</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/admin/perguntas">Perguntas</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Briefings enviados</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{countByStatus.ENVIADO ?? 0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Em analise</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{countByStatus.EM_ANALISE ?? 0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Concluidos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{countByStatus.CONCLUIDO ?? 0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Novos em 7 dias</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{dashboard.novosUltimos7Dias}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Ultimos envios</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Briefing</TableHead>
|
||||
<TableHead>Cliente</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Enviado em</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dashboard.ultimosEnvios.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>{briefing.cliente.nomeCompleto}</TableCell>
|
||||
<TableCell><Badge variant="muted">{briefing.status}</Badge></TableCell>
|
||||
<TableCell>{briefing.enviadoEm?.toLocaleString("pt-BR")}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import Link from "next/link";
|
||||
import { salvarPerguntaAction } 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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { listarPerguntasAdmin } from "@/lib/admin-service";
|
||||
|
||||
export default async function PerguntasAdminPage() {
|
||||
const secoes = await listarPerguntasAdmin();
|
||||
|
||||
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">Perguntas e ajudas</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Edite textos do briefing sem alterar codigo.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/admin">Voltar</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{secoes.map((secao) => (
|
||||
<Card key={secao.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{secao.ordem}. {secao.titulo}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{secao.perguntas.map((pergunta) => (
|
||||
<form key={pergunta.id} action={salvarPerguntaAction} className="space-y-4 rounded-lg border p-4">
|
||||
<input type="hidden" name="perguntaId" value={pergunta.id} />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="muted">{pergunta.tipo}</Badge>
|
||||
{!pergunta.ativo ? <Badge variant="outline">Inativa</Badge> : null}
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-[100px_1fr]">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`ordem-${pergunta.id}`}>Ordem</Label>
|
||||
<Input id={`ordem-${pergunta.id}`} name="ordem" type="number" defaultValue={pergunta.ordem} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`rotulo-${pergunta.id}`}>Rotulo</Label>
|
||||
<Input id={`rotulo-${pergunta.id}`} name="rotulo" defaultValue={pergunta.rotulo} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`dica-${pergunta.id}`}>Dica</Label>
|
||||
<Input id={`dica-${pergunta.id}`} name="dica" defaultValue={pergunta.dica ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="flex items-center gap-3 rounded-md border p-3 text-sm">
|
||||
<Checkbox name="ativo" defaultChecked={pergunta.ativo} />
|
||||
Ativa
|
||||
</label>
|
||||
<label className="flex items-center gap-3 rounded-md border p-3 text-sm">
|
||||
<Checkbox name="obrigatoria" defaultChecked={pergunta.obrigatoria} />
|
||||
Obrigatoria
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`oq-${pergunta.id}`}>O que responder</Label>
|
||||
<Textarea id={`oq-${pergunta.id}`} name="oQueResponder" defaultValue={pergunta.ajuda?.oQueResponder ?? ""} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`pq-${pergunta.id}`}>Por que perguntamos</Label>
|
||||
<Textarea id={`pq-${pergunta.id}`} name="porQuePerguntamos" defaultValue={pergunta.ajuda?.porQuePerguntamos ?? ""} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`ex-${pergunta.id}`}>Mini exemplo</Label>
|
||||
<Textarea id={`ex-${pergunta.id}`} name="miniExemplo" defaultValue={pergunta.ajuda?.miniExemplo ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit">Salvar pergunta</Button>
|
||||
</form>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user