412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { Coins, DollarSign, Loader2, RefreshCw } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { useSession } from "@/lib/hooks/use-session";
|
|
|
|
interface Listing {
|
|
listing_id: number;
|
|
item_name: string;
|
|
quantity: number;
|
|
price_pix: number | string | null;
|
|
price_tibia_coins: number | string | null;
|
|
price_gold_coins: number | string | null;
|
|
trade_item_id?: number | null;
|
|
trade_item_name?: string | null;
|
|
trade_item_quantity?: number | null;
|
|
highest_custom_offer_amount?: number | string | null;
|
|
highest_custom_offer_currency?: string | null;
|
|
}
|
|
|
|
interface PurchaseModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
listing: Listing | null;
|
|
onSuccess?: () => void;
|
|
}
|
|
|
|
export function PurchaseModal({
|
|
open,
|
|
onOpenChange,
|
|
listing,
|
|
onSuccess,
|
|
}: PurchaseModalProps) {
|
|
const { user } = useSession();
|
|
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [customOfferValue, setCustomOfferValue] = useState<string>("");
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setSelectedMethod(null);
|
|
setCustomOfferValue("");
|
|
setError(null);
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
selectedMethod &&
|
|
!["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)
|
|
) {
|
|
setCustomOfferValue("");
|
|
}
|
|
}, [selectedMethod]);
|
|
|
|
if (!listing) return null;
|
|
|
|
// Determinar métodos de pagamento disponíveis
|
|
const availableMethods: Array<{
|
|
type: string;
|
|
label: string;
|
|
value: string;
|
|
icon: any;
|
|
}> = [];
|
|
|
|
if (listing.price_pix) {
|
|
const price =
|
|
typeof listing.price_pix === "string"
|
|
? parseFloat(listing.price_pix)
|
|
: listing.price_pix;
|
|
if (!Number.isNaN(price)) {
|
|
availableMethods.push({
|
|
type: "pix",
|
|
label: "PIX",
|
|
value: `R$ ${price.toFixed(2)}`,
|
|
icon: DollarSign,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (listing.price_tibia_coins) {
|
|
const price =
|
|
typeof listing.price_tibia_coins === "string"
|
|
? parseInt(listing.price_tibia_coins, 10)
|
|
: listing.price_tibia_coins;
|
|
if (!Number.isNaN(price)) {
|
|
availableMethods.push({
|
|
type: "tibia_coins",
|
|
label: "Tibia Coins",
|
|
value: `${price.toLocaleString()} TC`,
|
|
icon: Coins,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (listing.price_gold_coins) {
|
|
const price =
|
|
typeof listing.price_gold_coins === "string"
|
|
? parseInt(listing.price_gold_coins, 10)
|
|
: listing.price_gold_coins;
|
|
if (!Number.isNaN(price)) {
|
|
availableMethods.push({
|
|
type: "gold_coins",
|
|
label: "Gold Coins",
|
|
value: `${price.toLocaleString()} GP`,
|
|
icon: Coins,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (
|
|
listing.trade_item_id &&
|
|
listing.trade_item_name &&
|
|
listing.trade_item_quantity
|
|
) {
|
|
availableMethods.push({
|
|
type: "trade",
|
|
label: "Troca",
|
|
value: `Troca por ${listing.trade_item_quantity}x ${listing.trade_item_name}`,
|
|
icon: RefreshCw,
|
|
});
|
|
}
|
|
|
|
const formatOffer = (
|
|
amount: number | string | null,
|
|
currency: string | null,
|
|
) => {
|
|
if (amount === null || amount === undefined || !currency) {
|
|
return null;
|
|
}
|
|
const numeric = typeof amount === "string" ? Number(amount) : amount;
|
|
if (Number.isNaN(numeric)) {
|
|
return null;
|
|
}
|
|
switch (currency) {
|
|
case "pix":
|
|
return `R$ ${numeric.toFixed(2)}`;
|
|
case "tibia_coins":
|
|
return `${Math.round(numeric).toLocaleString()} TC`;
|
|
case "gold_coins":
|
|
return `${Math.round(numeric).toLocaleString()} GP`;
|
|
default:
|
|
return `${numeric}`;
|
|
}
|
|
};
|
|
|
|
const highestOfferLabel = formatOffer(
|
|
listing.highest_custom_offer_amount ?? null,
|
|
listing.highest_custom_offer_currency ?? null,
|
|
);
|
|
|
|
// Se houver apenas um método, selecionar automaticamente
|
|
if (availableMethods.length === 1 && !selectedMethod) {
|
|
setSelectedMethod(availableMethods[0].type);
|
|
}
|
|
|
|
const handlePurchase = async () => {
|
|
if (!selectedMethod) {
|
|
setError("Selecione um método de pagamento");
|
|
return;
|
|
}
|
|
|
|
const hasCustomOffer = customOfferValue.trim().length > 0;
|
|
if (hasCustomOffer) {
|
|
const offerNumber = Number(customOfferValue);
|
|
if (Number.isNaN(offerNumber) || offerNumber <= 0) {
|
|
setError("Informe um valor válido para a oferta personalizada.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const payload: Record<string, unknown> = {
|
|
payment_method: selectedMethod,
|
|
};
|
|
|
|
if (
|
|
hasCustomOffer &&
|
|
["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)
|
|
) {
|
|
const offerNumber = Number(customOfferValue);
|
|
payload.custom_offer_amount =
|
|
selectedMethod === "pix"
|
|
? Number(offerNumber.toFixed(2))
|
|
: Math.floor(offerNumber);
|
|
payload.custom_offer_currency = selectedMethod;
|
|
}
|
|
|
|
const response = await fetch(
|
|
`/api/listings/${listing.listing_id}/purchase`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
},
|
|
);
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok || !data.success) {
|
|
throw new Error(data.error || "Erro ao processar compra");
|
|
}
|
|
|
|
// Sucesso
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
}
|
|
onOpenChange(false);
|
|
setCustomOfferValue("");
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Erro ao processar compra");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getMethodClassName = (type: string) => {
|
|
switch (type) {
|
|
case "pix":
|
|
return "bg-green-500 hover:bg-green-600 text-white";
|
|
case "tibia_coins":
|
|
return "bg-blue-500 hover:bg-blue-600 text-white";
|
|
case "gold_coins":
|
|
return "bg-yellow-500 hover:bg-yellow-600 text-white";
|
|
case "trade":
|
|
return "bg-purple-500 hover:bg-purple-600 text-white";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Confirmar Compra</DialogTitle>
|
|
<DialogDescription>
|
|
Escolha o método de pagamento para finalizar a compra
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Resumo do item */}
|
|
<div className="bg-muted p-4 rounded-lg">
|
|
<h3 className="font-semibold mb-2">{listing.item_name}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Quantidade: {listing.quantity}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium">
|
|
Valores anunciados pelo vendedor
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{availableMethods.map((method) => (
|
|
<Badge
|
|
key={`original-${method.type}`}
|
|
variant="secondary"
|
|
className="gap-1"
|
|
>
|
|
{method.label}: {method.value}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{highestOfferLabel && (
|
|
<div className="rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-sm">
|
|
Maior oferta registrada:{" "}
|
|
<span className="font-semibold">{highestOfferLabel}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
{/* Seleção de método de pagamento */}
|
|
{availableMethods.length > 1 ? (
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">Método de Pagamento</Label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{availableMethods.map((method) => {
|
|
const Icon = method.icon;
|
|
const isSelected = selectedMethod === method.type;
|
|
return (
|
|
<button
|
|
key={method.type}
|
|
type="button"
|
|
onClick={() => setSelectedMethod(method.type)}
|
|
className={`p-4 rounded-lg border-2 transition-all text-left ${
|
|
isSelected
|
|
? `${getMethodClassName(method.type)} border-primary`
|
|
: "border-muted hover:border-primary/50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Icon className="h-4 w-4" />
|
|
<span className="font-medium">{method.label}</span>
|
|
</div>
|
|
<p className="text-sm opacity-90">{method.value}</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : availableMethods.length === 1 ? (
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">Método de Pagamento</Label>
|
|
<div
|
|
className={`p-4 rounded-lg border-2 ${getMethodClassName(availableMethods[0].type)}`}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
{(() => {
|
|
const Icon = availableMethods[0].icon;
|
|
return <Icon className="h-4 w-4" />;
|
|
})()}
|
|
<span className="font-medium">
|
|
{availableMethods[0].label}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm opacity-90">
|
|
{availableMethods[0].value}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Nenhum método de pagamento disponível
|
|
</p>
|
|
)}
|
|
|
|
{selectedMethod &&
|
|
["pix", "tibia_coins", "gold_coins"].includes(selectedMethod) && (
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">
|
|
Propor valor personalizado (
|
|
{selectedMethod === "pix" ? "R$" : "quantidade"})
|
|
</Label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
step={selectedMethod === "pix" ? "0.01" : "1"}
|
|
value={customOfferValue}
|
|
onChange={(event) => setCustomOfferValue(event.target.value)}
|
|
placeholder={
|
|
selectedMethod === "pix" ? "Ex.: 199.90" : "Ex.: 1200"
|
|
}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Esse valor será enviado ao vendedor como sua proposta. Ele
|
|
pode aceitar, negar ou responder.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-destructive/10 text-destructive p-3 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
{/* Botões */}
|
|
<div className="flex gap-2 justify-end">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={loading}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
onClick={handlePurchase}
|
|
disabled={
|
|
loading || !selectedMethod || availableMethods.length === 0
|
|
}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Processando...
|
|
</>
|
|
) : (
|
|
"Confirmar Compra"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|