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:
133
components/agendamentos-table.tsx
Normal file
133
components/agendamentos-table.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user