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>
134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
"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>
|
|
)
|
|
}
|