Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions src/components/cart/BundleSuggestionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* BundleSuggestionCard — sugere produtos comumente orçados juntos com o produto-âncora.
* Consulta histórico de quote_items via RPC `get_bundle_suggestions(_product_id)`.
*/
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Sparkles, Plus } from "lucide-react";
import { motion } from "framer-motion";
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Sparkles, Plus } from 'lucide-react';
import { motion } from 'framer-motion';

interface BundleSuggestion {
product_id: string;
Expand All @@ -26,15 +26,15 @@ interface BundleSuggestionCardProps {

export function BundleSuggestionCard({ productId, onAdd, className }: BundleSuggestionCardProps) {
const { data, isLoading } = useQuery({
queryKey: ["bundle-suggestions", productId],
queryKey: ['bundle-suggestions', productId],
enabled: !!productId,
queryFn: async (): Promise<BundleSuggestion[]> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error } = await (supabase.rpc as any)("get_bundle_suggestions", {
const { data, error } = await (supabase.rpc as any)('get_bundle_suggestions', {
_product_id: productId,
});
if (error) {
console.warn("get_bundle_suggestions error:", error);
console.warn('get_bundle_suggestions error:', error);
return [];
}
return (data ?? []) as BundleSuggestion[];
Expand All @@ -43,12 +43,15 @@ export function BundleSuggestionCard({ productId, onAdd, className }: BundleSugg
});

if (!isLoading && !data?.length) return null;
const suggestions = data ?? [];

return (
<Card className={`border-primary/20 shadow-sm hover:shadow-md transition-shadow animate-in zoom-in-95 duration-300 ${className ?? ""}`}>
<Card
className={`border-primary/20 shadow-sm transition-shadow duration-300 animate-in zoom-in-95 hover:shadow-md ${className ?? ''}`}
>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<div className="p-1 rounded-md bg-primary/10">
<CardTitle className="flex items-center gap-2 text-sm">
<div className="rounded-md bg-primary/10 p-1">
<Sparkles className="h-4 w-4 text-primary" />
</div>
Frequentemente orçado em conjunto
Expand All @@ -57,12 +60,12 @@ export function BundleSuggestionCard({ productId, onAdd, className }: BundleSugg
Vendedores que orçaram este produto também incluíram:
</CardDescription>
</CardHeader>
<CardContent className="p-3 pt-0 space-y-2">
<CardContent className="space-y-2 p-3 pt-0">
{isLoading ? (
<div className="space-y-3 animate-pulse">
<div className="animate-pulse space-y-3">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center gap-2.5 p-2.5 rounded-md">
<Skeleton className="w-10 h-10 rounded-md shrink-0 opacity-20" />
<div key={i} className="flex items-center gap-2.5 rounded-md p-2.5">
<Skeleton className="h-10 w-10 shrink-0 rounded-md opacity-20" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3 w-3/4 opacity-15" />
<Skeleton className="h-2 w-1/2 opacity-10" />
Expand All @@ -72,26 +75,26 @@ export function BundleSuggestionCard({ productId, onAdd, className }: BundleSugg
))}
</div>
) : (
data!.map(item => (
suggestions.map((item) => (
<motion.div
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
key={item.product_id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-muted/50 transition-colors"
className="flex items-center gap-2 rounded-md p-2 transition-colors hover:bg-muted/50"
>
{item.product_image_url ? (
<img
src={item.product_image_url}
alt={item.product_name}
className="w-10 h-10 rounded object-cover bg-muted"
className="h-10 w-10 rounded bg-muted object-cover"
loading="lazy"
/>
) : (
<div className="w-10 h-10 rounded bg-muted shrink-0" />
<div className="h-10 w-10 shrink-0 rounded bg-muted" />
)}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{item.product_name}</p>
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium">{item.product_name}</p>
<p className="text-[10px] text-muted-foreground">
{item.frequency_percent}% das vezes · {item.cooccurrence_count}x
</p>
Expand All @@ -100,7 +103,7 @@ export function BundleSuggestionCard({ productId, onAdd, className }: BundleSugg
<Button
size="sm"
variant="outline"
className="h-7 text-[10px] gap-1 shrink-0"
className="h-7 shrink-0 gap-1 text-[10px]"
onClick={() => onAdd(item)}
aria-label={`Adicionar ${item.product_name}`}
>
Expand Down
62 changes: 30 additions & 32 deletions src/components/compare/ComparisonScoreCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@
* ComparisonScoreCard — Card com score ponderado + popover para ajustar pesos.
* Mostra o vencedor recomendado com badge Crown.
*/
import { useState } from "react";
import { Crown, Sliders, Sparkles } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { useState } from 'react';
import { Crown, Sliders, Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import {
useComparisonScore,
DEFAULT_SCORE_WEIGHTS,
type ComparisonScoreWeights,
} from "@/hooks/comparison";
} from '@/hooks/comparison';

interface ComparisonScoreCardProps {
products: Record<string, unknown>[];
className?: string;
}

const WEIGHT_LABELS: Record<keyof ComparisonScoreWeights, string> = {
price: "Preço",
stock: "Estoque",
minQuantity: "Qtd. mínima",
colorVariety: "Variedade de cores",
verifiedSupplier: "Fornecedor verificado",
leadTime: "Lead time",
price: 'Preço',
stock: 'Estoque',
minQuantity: 'Qtd. mínima',
colorVariety: 'Variedade de cores',
verifiedSupplier: 'Fornecedor verificado',
leadTime: 'Lead time',
};

export function ComparisonScoreCard({ products, className }: ComparisonScoreCardProps) {
const [weights, setWeights] = useState<ComparisonScoreWeights>(DEFAULT_SCORE_WEIGHTS);
const scores = useComparisonScore(products, weights);
const winner = scores.find(s => s.isWinner);
const winnerProduct = winner ? products.find(p => String(p.id) === winner.productId) : null;
const winner = scores.find((s) => s.isWinner);
const winnerProduct = winner ? products.find((p) => String(p.id) === winner.productId) : null;

if (!winnerProduct || products.length < 2) return null;
if (!winner || !winnerProduct || products.length < 2) return null;

return (
<div
className={cn(
"relative rounded-2xl border-[1.5px] border-primary/30 bg-gradient-to-br from-primary/5 via-background to-background p-4 shadow-md",
className
'relative rounded-2xl border-[1.5px] border-primary/30 bg-gradient-to-br from-primary/5 via-background to-background p-4 shadow-md',
className,
)}
>
<div className="flex flex-wrap items-start justify-between gap-3">
Expand All @@ -52,13 +52,13 @@ export function ComparisonScoreCard({ products, className }: ComparisonScoreCard
</div>
<div>
<div className="flex items-center gap-2">
<Badge className="bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/40 gap-1">
<Badge className="gap-1 border-amber-500/40 bg-amber-500/20 text-amber-700 dark:text-amber-300">
<Sparkles className="h-3 w-3" /> Recomendado
</Badge>
<span className="text-2xl font-bold text-foreground">{winner!.total}</span>
<span className="text-2xl font-bold text-foreground">{winner.total}</span>
<span className="text-sm text-muted-foreground">/ 100</span>
</div>
<p className="text-sm font-medium text-foreground line-clamp-1 mt-0.5">
<p className="mt-0.5 line-clamp-1 text-sm font-medium text-foreground">
{winnerProduct.name}
</p>
</div>
Expand All @@ -74,18 +74,16 @@ export function ComparisonScoreCard({ products, className }: ComparisonScoreCard
<PopoverContent className="w-80" align="end">
<div className="space-y-4">
<div>
<h4 className="font-semibold text-sm">Pesos do score</h4>
<h4 className="text-sm font-semibold">Pesos do score</h4>
<p className="text-xs text-muted-foreground">
Ajuste para refletir suas prioridades.
</p>
</div>
{(Object.keys(weights) as Array<keyof ComparisonScoreWeights>).map(key => (
{(Object.keys(weights) as Array<keyof ComparisonScoreWeights>).map((key) => (
<div key={key} className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-xs">{WEIGHT_LABELS[key]}</Label>
<span className="text-xs font-mono text-muted-foreground">
{weights[key]}
</span>
<span className="font-mono text-xs text-muted-foreground">{weights[key]}</span>
</div>
<Slider
value={[weights[key]]}
Expand Down Expand Up @@ -115,16 +113,16 @@ export function ComparisonScoreCard({ products, className }: ComparisonScoreCard
.slice()
.sort((a, b) => a.rank - b.rank)
.map((s) => {
const p = products.find(x => String(x.id) === s.productId);
const p = products.find((x) => String(x.id) === s.productId);
if (!p) return null;
return (
<div
key={s.productId}
className={cn(
"flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs",
'flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs',
s.isWinner
? "border-primary/40 bg-primary/5 font-medium"
: "border-border bg-muted/30"
? 'border-primary/40 bg-primary/5 font-medium'
: 'border-border bg-muted/30',
)}
>
<span className="font-mono text-muted-foreground">#{s.rank}</span>
Expand Down
Loading
Loading