feat: initial commit — asaas-checkout template white-label

Template genérico de checkout com ASAAS, parametrizado via env vars.
Inclui fluxo completo: checkout → pedido → polling → webhook → admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 06:40:41 +02:00
commit 038ce3f556
103 changed files with 20709 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { useToast } from "@/components/ui/use-toast"
import { Calendar, CheckCircle2, XCircle, Clock } from "lucide-react"
const statusConfig: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
pendente: { label: "Pendente", color: "bg-yellow-100 text-yellow-800", icon: <Clock className="h-3 w-3" /> },
confirmado: { label: "Confirmado", color: "bg-blue-100 text-blue-800", icon: <Calendar className="h-3 w-3" /> },
realizado: { label: "Realizado", color: "bg-green-100 text-green-800", icon: <CheckCircle2 className="h-3 w-3" /> },
cancelado: { label: "Cancelado", color: "bg-red-100 text-red-800", icon: <XCircle className="h-3 w-3" /> },
}
type Agendamento = {
id: string
data_hora: string
status: string
observacoes: string | null
created_at: string
clientes: { nome: string; email: string } | null
produtos: { nome: string } | null
}
export function AgendamentosTable({ agendamentos: inicial }: { agendamentos: Agendamento[] }) {
const { toast } = useToast()
const [itens, setItens] = useState(inicial)
const [atualizando, setAtualizando] = useState<string | null>(null)
async function atualizarStatus(id: string, novoStatus: string) {
setAtualizando(id)
try {
const res = await fetch(`/api/agendamento/${id}/status`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: novoStatus }),
})
if (!res.ok) throw new Error("Erro ao atualizar")
setItens((prev) =>
prev.map((a) => (a.id === id ? { ...a, status: novoStatus } : a))
)
toast({ title: `Status atualizado para "${novoStatus}"` })
} catch {
toast({ title: "Erro ao atualizar status", variant: "destructive" })
} finally {
setAtualizando(null)
}
}
if (itens.length === 0) {
return (
<div className="rounded-md border p-8 text-center text-muted-foreground">
Nenhum agendamento encontrado.
</div>
)
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Cliente</TableHead>
<TableHead>Produto</TableHead>
<TableHead>Data / Hora</TableHead>
<TableHead>Status</TableHead>
<TableHead>Observações</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{itens.map((a) => {
const cfg = statusConfig[a.status] ?? { label: a.status, color: "bg-gray-100 text-gray-800", icon: null }
const isLoading = atualizando === a.id
return (
<TableRow key={a.id}>
<TableCell>
<p className="font-medium text-sm">{a.clientes?.nome ?? "—"}</p>
<p className="text-xs text-muted-foreground">{a.clientes?.email ?? ""}</p>
</TableCell>
<TableCell className="text-sm">{a.produtos?.nome ?? "—"}</TableCell>
<TableCell className="text-sm whitespace-nowrap">
{new Date(a.data_hora).toLocaleString("pt-BR", {
day: "2-digit", month: "2-digit", year: "numeric",
hour: "2-digit", minute: "2-digit",
})}
</TableCell>
<TableCell>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${cfg.color}`}>
{cfg.icon}
{cfg.label}
</span>
</TableCell>
<TableCell className="text-sm text-muted-foreground max-w-[180px] truncate">
{a.observacoes ?? "—"}
</TableCell>
<TableCell>
<div className="flex gap-1">
{a.status !== "realizado" && (
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
disabled={isLoading}
onClick={() => atualizarStatus(a.id, "realizado")}
>
<CheckCircle2 className="mr-1 h-3 w-3" />
Realizado
</Button>
)}
{a.status !== "cancelado" && a.status !== "realizado" && (
<Button
size="sm"
variant="outline"
className="h-7 text-xs text-red-600 hover:text-red-700"
disabled={isLoading}
onClick={() => atualizarStatus(a.id, "cancelado")}
>
<XCircle className="mr-1 h-3 w-3" />
Cancelar
</Button>
)}
</div>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)
}