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:
171
components/product-form-dialog.tsx
Normal file
171
components/product-form-dialog.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { createProduto, updateProduto } from "@/lib/actions"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog"
|
||||
import type { Produto } from "@/lib/supabase"
|
||||
|
||||
type TipoOptions = "PF" | "PJ" | "SSL" | "NFe"
|
||||
|
||||
interface ProductFormDialogProps {
|
||||
product?: Produto | null
|
||||
}
|
||||
|
||||
const parseCurrency = (value: string) => Number(value.replace(/\D/g, ""))
|
||||
const formatCurrency = (value: string) => {
|
||||
const amount = Number(value.replace(/\D/g, "")) / 100
|
||||
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(amount)
|
||||
}
|
||||
|
||||
export function ProductFormDialog({ product }: ProductFormDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState({
|
||||
nome: "",
|
||||
descricao: "",
|
||||
price: "0",
|
||||
tipo: "PF" as TipoOptions,
|
||||
validade: "",
|
||||
midia: "",
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (product) {
|
||||
setFormData({
|
||||
nome: product.nome,
|
||||
descricao: product.descricao ?? "",
|
||||
price: formatCurrency((product.preco_centavos).toString()),
|
||||
tipo: product.tipo,
|
||||
validade: product.validade,
|
||||
midia: product.midia ?? "",
|
||||
})
|
||||
}
|
||||
}, [product])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
const preco_centavos = parseCurrency(formData.price)
|
||||
if (isNaN(preco_centavos) || preco_centavos <= 0) throw new Error("Preço inválido")
|
||||
|
||||
const payload = {
|
||||
nome: formData.nome,
|
||||
descricao: formData.descricao,
|
||||
tipo: formData.tipo,
|
||||
validade: formData.validade,
|
||||
midia: formData.midia || undefined,
|
||||
preco_centavos,
|
||||
}
|
||||
|
||||
if (product) {
|
||||
await updateProduto(product.id, payload)
|
||||
toast({ title: "Produto atualizado", description: "Alterações salvas com sucesso." })
|
||||
} else {
|
||||
await createProduto(payload)
|
||||
toast({ title: "Produto criado", description: "Adicionado com sucesso." })
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: error instanceof Error ? error.message : "Tente novamente.",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setOpen(true)}>{product ? "Editar Produto" : "Novo Produto"}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{product ? "Editar Produto" : "Criar Novo Produto"}</DialogTitle>
|
||||
<DialogDescription>Preencha os dados do certificado.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nome">Nome</Label>
|
||||
<Input id="nome" name="nome" value={formData.nome} onChange={handleChange} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="descricao">Descrição</Label>
|
||||
<Textarea id="descricao" name="descricao" value={formData.descricao} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="price">Preço</Label>
|
||||
<Input
|
||||
id="price"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, price: formatCurrency(e.target.value) }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tipo">Tipo</Label>
|
||||
<select
|
||||
id="tipo"
|
||||
name="tipo"
|
||||
value={formData.tipo}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
required
|
||||
>
|
||||
<option value="PF">PF — Pessoa Física</option>
|
||||
<option value="PJ">PJ — Pessoa Jurídica</option>
|
||||
<option value="SSL">SSL</option>
|
||||
<option value="NFe">NFe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="validade">Validade</Label>
|
||||
<Input id="validade" name="validade" placeholder="ex: 1 ano" value={formData.validade} onChange={handleChange} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="midia">Mídia</Label>
|
||||
<select
|
||||
id="midia"
|
||||
name="midia"
|
||||
value={formData.midia}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Token">Token</option>
|
||||
<option value="Cartão">Cartão</option>
|
||||
<option value="Nuvem">Nuvem</option>
|
||||
<option value="Sem mídia">Sem mídia</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button type="submit">{product ? "Salvar" : "Criar"}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user