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
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

551 changes: 390 additions & 161 deletions src/components/compare/SupplierComparisonModal.tsx

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions src/components/products/PriceFreshnessBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ describe('PriceFreshnessBadge Component', () => {
return render(<TooltipProvider>{ui}</TooltipProvider>);
};

it("renders 'Atualizado (há 0 dias)' for fresh updates in inline variant", () => {
it("renders 'Atualizado hoje' for fresh updates in inline variant", () => {
const today = new Date('2026-05-03T09:00:00Z').toISOString();
renderWithProvider(<PriceFreshnessBadge priceUpdatedAt={today} variant="inline" />);

expect(screen.getByText(/Atualizado \(há 0 dias\)/i)).toBeInTheDocument();
expect(screen.getByText(/Atualizado hoje/i)).toBeInTheDocument();
});

it('renders nothing for fresh updates in compact variant (unless alwaysShow is true)', () => {
Expand All @@ -42,19 +42,19 @@ describe('PriceFreshnessBadge Component', () => {
expect(screen.getByText(/há 4m/i)).toBeInTheDocument();
});

it("renders 'Próximo do limite (há 45 dias)' for aging updates in inline variant", () => {
it("renders 'Atualizado há 45 dias' for aging updates in inline variant", () => {
// 2026-05-03 - 45 days ago
const fortyFiveDaysAgo = new Date('2026-03-19T12:00:00Z').toISOString();
renderWithProvider(<PriceFreshnessBadge priceUpdatedAt={fortyFiveDaysAgo} variant="inline" />);

expect(screen.getByText(/Próximo do limite \(há 45 dias\)/i)).toBeInTheDocument();
expect(screen.getByText(/Atualizado há 45 dias/i)).toBeInTheDocument();
});

it('renders PDP variant with warning box for stale updates', () => {
const monthsAgo = new Date('2026-01-03T12:00:00Z').toISOString();
renderWithProvider(<PriceFreshnessBadge priceUpdatedAt={monthsAgo} variant="pdp" />);

expect(screen.getByText(/Possivelmente defasado/i)).toBeInTheDocument();
expect(screen.getByText(/Preço pode estar defasado/i)).toBeInTheDocument();
expect(screen.getByText(/\(há 120 dias\)/i)).toBeInTheDocument();
expect(screen.getByText(/Confirme com o fornecedor/i)).toBeInTheDocument();
});
Expand Down
22 changes: 12 additions & 10 deletions src/components/products/PriceFreshnessBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ function FreshnessTooltipBody({ freshness, priceUpdatedAt }: FreshnessTooltipPro
// Padrão único pt-BR: "em DD/MM/AAAA". A hora local e a forma por extenso
// ficam como detalhamento auxiliar, sem repetir a data curta.
const shortDate = isValidDate ? formatPriceDateShort(dateValue) : null;
const longDate = isValidDate ? formatPriceDateLong(dateValue) : null;
const _exactDateTime = isValidDate ? formatExactDateTime(dateValue) : null;
const statusLabel = STATUS_LABELS[freshness.status];
const rule = buildClassificationRule(freshness.thresholdDays);
Expand All @@ -128,12 +127,15 @@ function FreshnessTooltipBody({ freshness, priceUpdatedAt }: FreshnessTooltipPro
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1.5">
<span className="font-semibold">{statusLabel}</span>
{freshness.status !== 'unknown' && (() => {
const stripped = freshness.label.match(/\(([^)]+)\)/)?.[1] || freshness.label.replace(/^(Atualizado|Próximo do limite|Possivelmente defasado)\s+/i, '').trim();
return (
<span className="text-muted-foreground">({stripped})</span>
);
})()}
{freshness.status !== 'unknown' &&
(() => {
const stripped =
freshness.label.match(/\(([^)]+)\)/)?.[1] ||
freshness.label
.replace(/^(Atualizado|Próximo do limite|Possivelmente defasado)\s+/i, '')
.trim();
return <span className="text-muted-foreground">({stripped})</span>;
})()}
</div>
{shortDate && (
<div className="leading-snug">
Expand Down Expand Up @@ -395,7 +397,7 @@ export function PriceFreshnessBadge({
>
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
<div className="flex flex-col gap-0.5 leading-tight">
<span className="font-display text-sm font-semibold">Possivelmente defasado</span>
<span className="font-display text-sm font-semibold">Preço pode estar defasado</span>
{absolute && (
<span className="text-xs tabular-nums text-amber-800/90 dark:text-amber-200/80">
Última atualização em {absolute} ({relative})
Expand All @@ -420,7 +422,7 @@ export function PriceFreshnessBadge({
<Clock className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="tabular-nums">
Atualizado em {absolute}
<span className="text-amber-700/70 dark:text-amber-300/70"> ({relative})</span>
<span className="text-amber-700/70 dark:text-amber-300/70"> · {relative}</span>
{limitSuffix}
</span>
</span>
Expand All @@ -438,7 +440,7 @@ export function PriceFreshnessBadge({
<CheckCircle2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="tabular-nums">
Atualizado em {absolute}
<span className="text-emerald-700/70 dark:text-emerald-400/70"> ({relative})</span>
<span className="text-emerald-700/70 dark:text-emerald-400/70"> · {relative}</span>
{limitSuffix}
</span>
</span>
Expand Down
63 changes: 42 additions & 21 deletions src/components/products/SmartRecommendationsMock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import { useRef } from 'react';
import { Sparkles, ChevronLeft, ChevronRight, Trophy, TrendingUp, Star } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { QuickAddToQuote } from './QuickAddToQuote';
Expand Down Expand Up @@ -91,18 +90,28 @@ const MOCK_RECS: MockRec[] = [
},
];

function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestChoice?: boolean; badgeLabel?: string }) {
function MockMiniCard({
rec,
isBestChoice,
badgeLabel,
}: {
rec: MockRec;
isBestChoice?: boolean;
badgeLabel?: string;
}) {
const scorePct = Math.round(rec.score * 100);
const isHighMatch = scorePct >= 95;

return (
<Card
className={cn(
'group/card min-w-[240px] max-w-[240px] shrink-0 overflow-hidden rounded-xl border-[1.5px] border-border',
'animate-fade-in relative transition-all duration-300',
'relative animate-fade-in transition-all duration-300',
'cursor-pointer hover:-translate-y-1 hover:border-primary/50 hover:shadow-lg',
isBestChoice && 'border-amber-400/60 shadow-[0_0_20px_-5px_rgba(251,191,36,0.3)]',
isHighMatch && !isBestChoice && 'border-primary/40 shadow-[0_0_15px_-5px_rgba(59,130,246,0.2)]',
isHighMatch &&
!isBestChoice &&
'border-primary/40 shadow-[0_0_15px_-5px_rgba(59,130,246,0.2)]',
)}
>
<div className="relative aspect-video w-full overflow-hidden bg-muted/30">
Expand All @@ -113,11 +122,11 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC
loading="lazy"
/>
{(isHighMatch || isBestChoice) && (
<div
<div
className={cn(
"absolute -inset-px opacity-0 transition-opacity duration-300 group-hover/card:opacity-100",
isBestChoice ? "bg-amber-400/10" : "bg-primary/5"
)}
'absolute -inset-px opacity-0 transition-opacity duration-300 group-hover/card:opacity-100',
isBestChoice ? 'bg-amber-400/10' : 'bg-primary/5',
)}
/>
)}
</div>
Expand All @@ -131,25 +140,31 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC
</div>
) : badgeLabel ? (
<div className="flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[9px] font-bold text-primary">
{badgeLabel === 'Mais Pedido' ? <TrendingUp className="h-2.5 w-2.5" /> : <Star className="h-2.5 w-2.5" />}
{badgeLabel === 'Mais Pedido' ? (
<TrendingUp className="h-2.5 w-2.5" />
) : (
<Star className="h-2.5 w-2.5" />
)}
{badgeLabel.toUpperCase()}
</div>
) : <div className="h-4" />}
) : (
<div className="h-4" />
)}

<span
className={cn(
"shrink-0 rounded-full border-[1.5px] px-1.5 py-0.5 font-display text-[10px] font-bold",
isBestChoice
? "border-amber-400/50 bg-amber-50 text-amber-600"
: "border-primary/30 bg-primary/5 text-primary"
'shrink-0 rounded-full border-[1.5px] px-1.5 py-0.5 font-display text-[10px] font-bold',
isBestChoice
? 'border-amber-400/50 bg-amber-50 text-amber-600'
: 'border-primary/30 bg-primary/5 text-primary',
)}
>
{scorePct}%
</span>
</div>

<div className="space-y-1">
<h4 className="line-clamp-2 min-h-[2.5rem] font-display text-sm font-semibold leading-tight group-hover/card:text-primary transition-colors">
<h4 className="line-clamp-2 min-h-[2.5rem] font-display text-sm font-semibold leading-tight transition-colors group-hover/card:text-primary">
{rec.name}
</h4>
<p className="line-clamp-2 font-display text-[11px] leading-relaxed text-muted-foreground">
Expand All @@ -176,7 +191,10 @@ export function SmartRecommendationsMock() {
scrollerRef.current?.scrollBy({ left: delta, behavior: 'smooth' });

return (
<section className="animate-fade-in space-y-3 font-display" aria-label="Recomendações inteligentes (preview)">
<section
className="animate-fade-in space-y-3 font-display"
aria-label="Recomendações inteligentes (preview)"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
Expand All @@ -187,7 +205,8 @@ export function SmartRecommendationsMock() {
Recomendações inteligentes para este produto
</h3>
<p className="text-xs text-muted-foreground">
Sugestões geradas por IA com base em similaridade, margem e perfil do cliente · <span className="font-medium text-primary">preview com dados mockados</span>
Sugestões geradas por IA com base em similaridade, margem e perfil do cliente ·{' '}
<span className="font-medium text-primary">preview com dados mockados</span>
</p>
</div>
</div>
Expand Down Expand Up @@ -222,10 +241,12 @@ export function SmartRecommendationsMock() {
>
{MOCK_RECS.map((rec, i) => (
<div key={rec.id} className="snap-start" role="listitem">
<MockMiniCard
rec={rec}
isBestChoice={i === 0}
badgeLabel={rec.score > 0.9 ? 'Mais Pedido' : rec.score > 0.7 ? 'Tendência' : undefined}
<MockMiniCard
rec={rec}
isBestChoice={i === 0}
badgeLabel={
rec.score > 0.9 ? 'Mais Pedido' : rec.score > 0.7 ? 'Tendência' : undefined
}
/>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-muted-foreground"
>
(há 0 dias)
(hoje)
</span>
</div>
<div
Expand Down Expand Up @@ -608,7 +608,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-muted-foreground"
>
(há 0 dias)
(hoje)
</span>
</div>
<div
Expand Down Expand Up @@ -832,7 +832,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
/>
</svg>
<span>
Próximo do limite (há 31 dias)
Atualizado há 31 dias
<span
class="tabular-nums text-muted-foreground"
>
Expand Down Expand Up @@ -927,7 +927,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
/>
</svg>
<span>
Atualizado (há 0 dias)
Atualizado hoje
<span
class="tabular-nums text-muted-foreground"
>
Expand All @@ -953,7 +953,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-muted-foreground"
>
(há 0 dias)
(hoje)
</span>
</div>
<div
Expand Down Expand Up @@ -1018,7 +1018,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
/>
</svg>
<span>
Possivelmente defasado (há 61 dias)
Preço pode estar defasado (há 61 dias)
<span
class="tabular-nums text-muted-foreground"
>
Expand Down Expand Up @@ -1191,7 +1191,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-amber-700/70 dark:text-amber-300/70"
>
(há 31 dias)
· há 31 dias
</span>
</span>
</span>
Expand Down Expand Up @@ -1287,7 +1287,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-emerald-700/70 dark:text-emerald-400/70"
>
(há 0 dias)
· há 0 dias
</span>
</span>
</span>
Expand All @@ -1309,7 +1309,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="text-muted-foreground"
>
(há 0 dias)
(hoje)
</span>
</div>
<div
Expand Down Expand Up @@ -1378,7 +1378,7 @@ exports[`PriceFreshnessBadge Snapshots and A11y > Snapshots > matches snapshot f
<span
class="font-display text-sm font-semibold"
>
Possivelmente defasado
Preço pode estar defasado
</span>
<span
class="text-xs tabular-nums text-amber-800/90 dark:text-amber-200/80"
Expand Down
Loading
Loading