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:
239
components/cupom-table.tsx
Normal file
239
components/cupom-table.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { criarCupom, atualizarCupom, deletarCupom } from "@/lib/actions"
|
||||
import type { Cupom } from "@/lib/supabase"
|
||||
import { Plus, Trash2, Star, Tag } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface CupomTableProps {
|
||||
cupons: Cupom[]
|
||||
}
|
||||
|
||||
function NovoCupomDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
const [form, setForm] = useState({
|
||||
codigo: "",
|
||||
descricao: "",
|
||||
percentual: "15",
|
||||
validade: "",
|
||||
destaque: false,
|
||||
})
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
try {
|
||||
await criarCupom({
|
||||
codigo: form.codigo,
|
||||
descricao: form.descricao,
|
||||
percentual: Number(form.percentual),
|
||||
validade: form.validade,
|
||||
destaque: form.destaque,
|
||||
})
|
||||
toast({ title: "Cupom criado!" })
|
||||
setOpen(false)
|
||||
setForm({ codigo: "", descricao: "", percentual: "15", validade: "", destaque: false })
|
||||
router.refresh()
|
||||
} catch (err) {
|
||||
toast({ title: "Erro", description: err instanceof Error ? err.message : "Erro ao criar cupom", variant: "destructive" })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2"><Plus className="w-4 h-4" /> Novo Cupom</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar cupom de desconto</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
|
||||
<div className="space-y-1">
|
||||
<Label>Código</Label>
|
||||
<Input
|
||||
required
|
||||
placeholder="VIXCERT20"
|
||||
value={form.codigo}
|
||||
onChange={(e) => setForm({ ...form, codigo: e.target.value.toUpperCase() })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Descrição</Label>
|
||||
<Input
|
||||
required
|
||||
placeholder="Ex: Renovação de Certificado"
|
||||
value={form.descricao}
|
||||
onChange={(e) => setForm({ ...form, descricao: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label>Desconto (%)</Label>
|
||||
<Input
|
||||
required
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={form.percentual}
|
||||
onChange={(e) => setForm({ ...form, percentual: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Válido até</Label>
|
||||
<Input
|
||||
required
|
||||
type="date"
|
||||
value={form.validade}
|
||||
onChange={(e) => setForm({ ...form, validade: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="destaque"
|
||||
checked={form.destaque}
|
||||
onCheckedChange={(v) => setForm({ ...form, destaque: v })}
|
||||
/>
|
||||
<Label htmlFor="destaque" className="cursor-pointer flex items-center gap-1">
|
||||
<Star className="w-4 h-4 text-yellow-500" /> Exibir em destaque no site
|
||||
</Label>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Criando..." : "Criar cupom"}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function CupomTable({ cupons }: CupomTableProps) {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const handleToggleAtivo = async (cupom: Cupom) => {
|
||||
try {
|
||||
await atualizarCupom(cupom.id, { ativo: !cupom.ativo })
|
||||
router.refresh()
|
||||
} catch {
|
||||
toast({ title: "Erro ao atualizar", variant: "destructive" })
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleDestaque = async (cupom: Cupom) => {
|
||||
try {
|
||||
await atualizarCupom(cupom.id, { destaque: !cupom.destaque })
|
||||
router.refresh()
|
||||
} catch {
|
||||
toast({ title: "Erro ao atualizar", variant: "destructive" })
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletar = async (id: string) => {
|
||||
if (!confirm("Deletar este cupom?")) return
|
||||
try {
|
||||
await deletarCupom(id)
|
||||
toast({ title: "Cupom deletado" })
|
||||
router.refresh()
|
||||
} catch {
|
||||
toast({ title: "Erro ao deletar", variant: "destructive" })
|
||||
}
|
||||
}
|
||||
|
||||
const hoje = new Date().toISOString().slice(0, 10)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Tag className="w-5 h-5" /> Cupons de Desconto
|
||||
</CardTitle>
|
||||
<NovoCupomDialog />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{cupons.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-8">Nenhum cupom cadastrado.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{cupons.map((cupom) => {
|
||||
const vencido = cupom.validade < hoje
|
||||
const validadeFormatada = new Date(cupom.validade + "T12:00:00").toLocaleDateString("pt-BR")
|
||||
return (
|
||||
<div
|
||||
key={cupom.id}
|
||||
className={`flex items-center justify-between p-4 rounded-lg border ${
|
||||
cupom.destaque ? "border-secondary/50 bg-secondary/5" : "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-mono font-bold text-gray-800">{cupom.codigo}</span>
|
||||
<Badge variant="secondary">{cupom.percentual}% OFF</Badge>
|
||||
{cupom.destaque && (
|
||||
<Badge className="bg-yellow-100 text-yellow-800 border-yellow-300">
|
||||
<Star className="w-3 h-3 mr-1" /> Destaque
|
||||
</Badge>
|
||||
)}
|
||||
{vencido && <Badge variant="destructive">Vencido</Badge>}
|
||||
{!cupom.ativo && <Badge variant="outline">Inativo</Badge>}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{cupom.descricao} · Até {validadeFormatada}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<Switch
|
||||
checked={cupom.destaque}
|
||||
onCheckedChange={() => handleToggleDestaque(cupom)}
|
||||
title="Exibir em destaque"
|
||||
/>
|
||||
<span>Destaque</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<Switch
|
||||
checked={cupom.ativo}
|
||||
onCheckedChange={() => handleToggleAtivo(cupom)}
|
||||
title="Ativo/Inativo"
|
||||
/>
|
||||
<span>Ativo</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleDeletar(cupom.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user