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>
240 lines
8.2 KiB
TypeScript
240 lines
8.2 KiB
TypeScript
"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>
|
|
)
|
|
}
|