Files
pagodera/components/marketplace/MyPurchaseIntentionsPanel.tsx
2025-11-11 03:08:36 -03:00

718 lines
24 KiB
TypeScript

"use client";
import {
AlertCircle,
CheckCircle2,
Clock,
Coins,
DollarSign,
ExternalLink,
MapPin,
MessageSquare,
Package,
RefreshCw,
ShoppingCart,
User,
XCircle,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { ChatModal } from "@/components/chat/ChatModal";
import { ChangePaymentMethodButton } from "@/components/purchase-intentions/ChangePaymentMethodButton";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import {
type PurchaseIntention,
type PurchaseIntentionStatus,
useMyPurchaseIntentions,
} from "@/lib/hooks/use-purchase-intentions";
import { useSession } from "@/lib/hooks/use-session";
import { cn } from "@/lib/utils";
function formatTimeAgo(date: Date | string): string {
const dateObj = typeof date === "string" ? new Date(date) : date;
const seconds = Math.floor((Date.now() - dateObj.getTime()) / 1000);
if (seconds < 60) return "há alguns segundos";
const minutes = Math.floor(seconds / 60);
if (minutes < 60)
return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`;
const days = Math.floor(hours / 24);
if (days < 30) return `há ${days} ${days === 1 ? "dia" : "dias"}`;
const months = Math.floor(days / 30);
if (months < 12) return `há ${months} ${months === 1 ? "mês" : "meses"}`;
const years = Math.floor(months / 12);
return `há ${years} ${years === 1 ? "ano" : "anos"}`;
}
function getStatusBadge(status: PurchaseIntentionStatus) {
switch (status) {
case "pending":
return (
<Badge
variant="secondary"
className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400"
>
<Clock className="h-3 w-3 mr-1" />
Pendente
</Badge>
);
case "accepted":
return (
<Badge
variant="secondary"
className="bg-green-500/20 text-green-700 dark:text-green-400"
>
<CheckCircle2 className="h-3 w-3 mr-1" />
Aceita
</Badge>
);
case "rejected":
return (
<Badge
variant="secondary"
className="bg-red-500/20 text-red-700 dark:text-red-400"
>
<XCircle className="h-3 w-3 mr-1" />
Rejeitada
</Badge>
);
case "expired":
return (
<Badge
variant="secondary"
className="bg-gray-500/20 text-gray-700 dark:text-gray-400"
>
<AlertCircle className="h-3 w-3 mr-1" />
Expirada
</Badge>
);
case "completed":
return (
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-700 dark:text-blue-400"
>
<CheckCircle2 className="h-3 w-3 mr-1" />
Concluída
</Badge>
);
default:
return <Badge variant="secondary">{status}</Badge>;
}
}
function getPaymentMethodLabel(method: string): string {
switch (method) {
case "pix":
return "PIX";
case "tibia_coins":
return "Tibia Coins";
case "gold_coins":
return "Gold Coins";
case "trade":
return "Troca";
default:
return method.toUpperCase();
}
}
function formatPrice(price: number | null, method: string): string {
if (price === null) return "N/A";
if (method === "pix") {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(price);
}
if (method === "tibia_coins") {
return `${price.toLocaleString("pt-BR")} TC`;
}
if (method === "gold_coins") {
const gold = price / 1000000;
if (gold >= 1) {
return `${gold.toFixed(2)}kk`;
}
return `${price.toLocaleString("pt-BR")} gp`;
}
return price.toLocaleString("pt-BR");
}
function formatCustomOffer(
amount: number | null | undefined,
currency: string | null | undefined,
): string | null {
if (amount === null || amount === undefined || !currency) {
return null;
}
switch (currency) {
case "pix":
return `R$ ${amount.toFixed(2)}`;
case "tibia_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} TC`;
case "gold_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} GP`;
default:
return amount.toLocaleString("pt-BR");
}
}
interface IntentionCardProps {
intention: PurchaseIntention;
locale?: string;
}
function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
const listing = intention.listing;
const [showListingModal, setShowListingModal] = useState(false);
const [showChatModal, setShowChatModal] = useState(false);
if (!listing) {
return (
<Card className="border-dashed opacity-60">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1 min-w-0">
<div className="relative w-16 h-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
<AlertCircle className="h-8 w-8 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-lg">
Anúncio não disponível
</h3>
{getStatusBadge(intention.status)}
</div>
<p className="text-sm text-muted-foreground">
Este anúncio foi removido ou não está mais disponível
</p>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-xs text-muted-foreground">
Intenção criada {formatTimeAgo(intention.created_at)}
</div>
</CardContent>
</Card>
);
}
const getItemImageUrl = () => {
if (listing.image_url) {
return listing.image_url;
}
if (listing.image_local_path) {
const normalizedPath = listing.image_local_path.replace(/\\/g, "/");
if (normalizedPath.startsWith("data/")) {
return `/api/images?path=${encodeURIComponent(normalizedPath)}`;
}
const category = listing.item_category || "other";
return `/api/images?path=${encodeURIComponent(`data/images/${category}/${normalizedPath}`)}`;
}
if (listing.item_id_unified) {
const params = new URLSearchParams({
item: listing.item_id_unified.toString(),
});
if (listing.item_name) {
params.set("name", listing.item_name);
}
return `/api/images?${params.toString()}`;
}
const params = new URLSearchParams({ item: listing.item_id.toString() });
if (listing.item_name) {
params.set("name", listing.item_name);
}
return `/api/images?${params.toString()}`;
};
const itemImageUrl = getItemImageUrl();
const chatId =
intention.chat_id ??
(listing.seller_id != null
? `${listing.listing_id}_${intention.buyer_id}_${listing.seller_id}`
: null);
const price =
listing.price_pix && intention.payment_method === "pix"
? listing.price_pix
: listing.price_tibia_coins && intention.payment_method === "tibia_coins"
? listing.price_tibia_coins
: listing.price_gold_coins && intention.payment_method === "gold_coins"
? listing.price_gold_coins
: null;
const customOfferLabel = formatCustomOffer(
typeof intention.custom_offer_amount === "number"
? intention.custom_offer_amount
: null,
intention.custom_offer_currency ?? null,
);
return (
<>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1 min-w-0">
<div className="relative w-16 h-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0 overflow-hidden">
{itemImageUrl ? (
<Image
src={itemImageUrl}
alt={listing.item_name}
fill
className="object-cover"
/>
) : (
<Package className="h-8 w-8 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-lg truncate">
{listing.item_name}
</h3>
{getStatusBadge(intention.status)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-2">
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
<span>{listing.server_name}</span>
</div>
<div className="flex items-center gap-1">
<Package className="h-3 w-3" />
<span>Qtd: {listing.quantity}</span>
</div>
<div className="flex items-center gap-1">
<ShoppingCart className="h-3 w-3" />
<span>
{getPaymentMethodLabel(intention.payment_method)}
</span>
{price !== null && (
<span className="font-medium text-foreground ml-1">
{formatPrice(price, intention.payment_method)}
</span>
)}
</div>
{customOfferLabel && (
<div className="flex items-center gap-1 text-foreground">
<Badge
variant="outline"
className="border-primary text-primary"
>
Sua oferta: {customOfferLabel}
</Badge>
</div>
)}
</div>
</div>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Informações do vendedor */}
<div className="flex items-center gap-2 text-sm">
<User className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Vendedor:</span>
<span className="font-medium">
{listing.seller_character_name ||
listing.seller_username ||
"Desconhecido"}
</span>
</div>
{/* Mensagem do comprador */}
{intention.buyer_message && (
<div className="bg-muted/50 rounded-md p-3">
<div className="flex items-start gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground mb-1">
Sua mensagem:
</p>
<p className="text-sm">{intention.buyer_message}</p>
</div>
</div>
</div>
)}
{/* Resposta do vendedor */}
{intention.seller_response && (
<div
className={cn(
"rounded-md p-3 border",
intention.status === "accepted"
? "bg-green-500/10 border-green-500/20"
: intention.status === "rejected"
? "bg-red-500/10 border-red-500/20"
: "bg-blue-500/10 border-blue-500/20",
)}
>
<div className="flex items-start gap-2">
<MessageSquare
className={cn(
"h-4 w-4 mt-0.5 flex-shrink-0",
intention.status === "accepted"
? "text-green-600 dark:text-green-400"
: intention.status === "rejected"
? "text-red-600 dark:text-red-400"
: "text-blue-600 dark:text-blue-400",
)}
/>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium mb-1">
{intention.status === "accepted"
? "Resposta do vendedor (Aceita):"
: intention.status === "rejected"
? "Resposta do vendedor (Rejeitada):"
: "Resposta do vendedor:"}
</p>
<p className="text-sm">{intention.seller_response}</p>
</div>
</div>
</div>
)}
{/* Timestamps */}
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
<span>Criada {formatTimeAgo(intention.created_at)}</span>
</div>
{intention.updated_at !== intention.created_at && (
<span>Atualizada {formatTimeAgo(intention.updated_at)}</span>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-2 flex-wrap">
{intention.status === "pending" &&
intention.payment_method_change_count < 1 && (
<ChangePaymentMethodButton
intentionId={intention.intention_id}
currentPaymentMethod={intention.payment_method}
availableMethods={[
...(listing.price_pix ? ["pix"] : []),
...(listing.price_tibia_coins ? ["tibia_coins"] : []),
...(listing.price_gold_coins ? ["gold_coins"] : []),
...(listing.trade_item_id ? ["trade"] : []),
]}
canChange={true}
/>
)}
<Button
variant="outline"
size="sm"
onClick={() => setShowListingModal(true)}
className="flex-1"
>
<ExternalLink className="h-4 w-4 mr-2" />
Ver Anúncio
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => setShowChatModal(true)}
>
<MessageSquare className="h-4 w-4 mr-2" />
Abrir Chat
</Button>
{intention.status === "accepted" &&
listing.listing_status === "active" && (
<Button
size="sm"
onClick={() => setShowListingModal(true)}
className="flex-1"
>
<ShoppingCart className="h-4 w-4 mr-2" />
Comprar Agora
</Button>
)}
</div>
</CardContent>
</Card>
<Dialog open={showListingModal} onOpenChange={setShowListingModal}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{listing.item_name}</DialogTitle>
<DialogDescription>
Detalhes do anúncio selecionado
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-[220px,1fr] gap-4">
<div className="relative w-full h-52 rounded-lg bg-muted overflow-hidden flex items-center justify-center">
{itemImageUrl ? (
<Image
src={itemImageUrl}
alt={listing.item_name}
fill
className="object-contain p-4"
/>
) : (
<Package className="h-12 w-12 text-muted-foreground" />
)}
</div>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">{listing.item_name}</h3>
<p className="text-sm text-muted-foreground mt-1">
{listing.description ??
"Sem descrição fornecida para este anúncio."}
</p>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="h-4 w-4" />
<span>
Servidor:{" "}
<span className="font-medium text-foreground">
{listing.server_name}
</span>
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Package className="h-4 w-4" />
<span>
Quantidade:{" "}
<span className="font-medium text-foreground">
{listing.quantity}
</span>
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<User className="h-4 w-4" />
<span>
Vendedor:{" "}
<span className="font-medium text-foreground">
{listing.seller_character_name ||
listing.seller_username ||
"Desconhecido"}
</span>
</span>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
Condições de pagamento
</h4>
<div className="flex flex-wrap gap-2">
{listing.price_pix && (
<Badge variant="secondary" className="gap-1">
<DollarSign className="h-3 w-3" />
{formatPrice(listing.price_pix, "pix")}
</Badge>
)}
{listing.price_tibia_coins && (
<Badge variant="secondary" className="gap-1">
<Coins className="h-3 w-3" />
{formatPrice(listing.price_tibia_coins, "tibia_coins")}
</Badge>
)}
{listing.price_gold_coins && (
<Badge variant="secondary" className="gap-1">
<Coins className="h-3 w-3" />
{formatPrice(listing.price_gold_coins, "gold_coins")}
</Badge>
)}
{listing.trade_item_id &&
listing.trade_item_name &&
listing.trade_item_quantity && (
<Badge variant="secondary" className="gap-1">
<RefreshCw className="h-3 w-3" />
{listing.trade_item_quantity}x {listing.trade_item_name}
</Badge>
)}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" asChild>
<Link
href={`/${locale}/market?listing=${listing.listing_id}`}
target="_blank"
>
Abrir no marketplace
</Link>
</Button>
<Button
variant="default"
onClick={() => setShowChatModal(true)}
>
<MessageSquare className="h-4 w-4 mr-2" />
Continuar conversa
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
{listing && (
<ChatModal
open={showChatModal}
onOpenChange={setShowChatModal}
listingId={listing.listing_id}
chatId={chatId || undefined}
/>
)}
</>
);
}
interface MyPurchaseIntentionsPanelProps {
locale?: string;
}
export function MyPurchaseIntentionsPanel({
locale = "pt",
}: MyPurchaseIntentionsPanelProps) {
const { user } = useSession();
const [filter, setFilter] = useState<PurchaseIntentionStatus | "all">("all");
const { intentions, isLoading } = useMyPurchaseIntentions();
// Contar por status
const pendingCount = intentions.filter((i) => i.status === "pending").length;
const acceptedCount = intentions.filter(
(i) => i.status === "accepted",
).length;
const rejectedCount = intentions.filter(
(i) => i.status === "rejected",
).length;
const expiredCount = intentions.filter((i) => i.status === "expired").length;
const completedCount = intentions.filter(
(i) => i.status === "completed",
).length;
const filteredIntentions =
filter === "all"
? intentions
: intentions.filter((intention) => intention.status === filter);
if (!user) {
return (
<div className="text-center py-12">
<p className="text-muted-foreground">
Faça login para ver suas intenções de compra
</p>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold mb-2">Minhas Intenções de Compra</h2>
<p className="text-muted-foreground">
Acompanhe suas intenções de compra e respostas dos vendedores
</p>
</div>
{/* Filtros */}
<div className="flex items-center gap-2 flex-wrap">
<Button
variant={filter === "all" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("all")}
>
Todas ({intentions.length})
</Button>
<Button
variant={filter === "pending" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("pending")}
>
Pendentes ({pendingCount})
</Button>
<Button
variant={filter === "accepted" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("accepted")}
>
Aceitas ({acceptedCount})
</Button>
<Button
variant={filter === "rejected" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("rejected")}
>
Rejeitadas ({rejectedCount})
</Button>
{expiredCount > 0 && (
<Button
variant={filter === "expired" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("expired")}
>
Expiradas ({expiredCount})
</Button>
)}
{completedCount > 0 && (
<Button
variant={filter === "completed" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("completed")}
>
Concluídas ({completedCount})
</Button>
)}
</div>
{/* Lista de intenções */}
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-48 w-full" />
))}
</div>
) : filteredIntentions.length === 0 ? (
<div className="text-center py-12 border rounded-lg">
<ShoppingCart className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<p className="text-lg font-medium mb-2">
Nenhuma intenção de compra encontrada
</p>
<p className="text-sm text-muted-foreground mb-4">
{filter === "all"
? "Quando você manifestar interesse em comprar um item, ele aparecerá aqui."
: `Não há intenções com status "${filter}" no momento.`}
</p>
<Button asChild variant="outline">
<Link href={`/${locale}/market`}>Explorar Marketplace</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{filteredIntentions.map((intention) => (
<IntentionCard
key={intention.intention_id}
intention={intention}
locale={locale}
/>
))}
</div>
)}
</div>
);
}