+
{attempt.success ? (
-
+
+
+
) : (
-
+
+
+
)}
- {attempt.success ? 'Login bem-sucedido' : 'Tentativa falha'}
- {attempt.success ? 'OK' : 'Falha'}
+
+ {attempt.success ? 'Login bem-sucedido' : 'Tentativa falha'}
+
+
+ {attempt.success ? 'OK' : 'Falha'}
+
-
-
{attempt.ip_address}
-
{format(new Date(attempt.created_at), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
+
+
+
+ {attempt.ip_address}
+
+
+
+ {format(new Date(attempt.created_at), "dd/MM/yyyy 'às' HH:mm", {
+ locale: ptBR,
+ })}
+
- {attempt.failure_reason &&
Motivo: {attempt.failure_reason}
}
+ {attempt.failure_reason && (
+
+ Motivo: {attempt.failure_reason}
+
+ )}
))}
- {loginAttempts.length === 0 &&
Nenhum login registrado ainda
}
+ {loginAttempts.length === 0 && (
+
+ Nenhum login registrado ainda
+
+ )}
diff --git a/src/hooks/favorites/useFavoritesPageState.ts b/src/hooks/favorites/useFavoritesPageState.ts
index a3762070a..5aeaef129 100644
--- a/src/hooks/favorites/useFavoritesPageState.ts
+++ b/src/hooks/favorites/useFavoritesPageState.ts
@@ -1,11 +1,12 @@
import { useState, useMemo, useEffect } from 'react';
import { useFavoritesStore } from '@/stores/useFavoritesStore';
import {
+ useEnrichedFavoriteItems,
useFavoriteLists,
+ useFavoritesGlobalShortcuts,
useFavoriteTrash,
useLegacyFavoritesMigration,
} from '@/hooks/favorites';
-import { useEnrichedFavoriteItems, useFavoritesGlobalShortcuts } from "@/hooks/favorites";
import { useProductsContext } from '@/contexts/ProductsContext';
import { useCatalogSelection } from '@/components/catalog/useCatalogSelection';
import { useUndoStack } from '@/hooks/common';
diff --git a/src/hooks/mockup/useMockupGenerator.ts b/src/hooks/mockup/useMockupGenerator.ts
index 1e8970839..7146c304e 100644
--- a/src/hooks/mockup/useMockupGenerator.ts
+++ b/src/hooks/mockup/useMockupGenerator.ts
@@ -11,14 +11,14 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { toast } from 'sonner';
import { needsConversion, ensureSupportedFormat } from '@/lib/image-converter';
import { useAuth } from '@/contexts/AuthContext';
-import { useMockupDraft } from '@/hooks/mockup';
import {
useFilteredTechniques,
+ useMockupDraft,
useProductCustomizationOptionsForMockup,
- type TechniqueWithLimits,
type CustomizationOption,
+ type TechniqueWithLimits,
} from '@/hooks/mockup';
-import { useLogoColorAnalysis, usePositionHistory } from "@/hooks/simulation";
+import { useLogoColorAnalysis, usePositionHistory } from '@/hooks/simulation';
import { useProductsContext } from '@/contexts/ProductsContext';
import { getMockupWizardStep } from '@/components/mockup/mockupWizardStep';
import { showMockupSuccessToast } from '@/components/mockup/MockupSuccessToast';
@@ -37,7 +37,7 @@ import {
generateMockupApi,
downloadMockupAsPdf,
deleteMockupFromDb,
-} from "@/hooks/mockup/mockupGenerationService";
+} from '@/hooks/mockup/mockupGenerationService';
// Re-export types for consumers
export type { Technique, GeneratedMockup };
diff --git a/src/hooks/quotes/useQuoteBuilderState.ts b/src/hooks/quotes/useQuoteBuilderState.ts
index 8f39fcb87..188b24dab 100644
--- a/src/hooks/quotes/useQuoteBuilderState.ts
+++ b/src/hooks/quotes/useQuoteBuilderState.ts
@@ -5,20 +5,26 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
-import { useAutoSaveQuote, useDiscountApproval, useQuoteItems, useQuotes, useSellerDiscountLimits, type QuoteItem, type QuoteItemPersonalization } from "@/hooks/quotes";
+import {
+ useAutoSaveQuote,
+ useDiscountApproval,
+ useQuoteItems,
+ useQuotes,
+ useQuoteTemplates,
+ useSellerDiscountLimits,
+ type QuoteItem,
+ type QuoteItemPersonalization,
+ type QuoteTemplate,
+ type QuoteTemplateItem,
+} from '@/hooks/quotes';
import { useQuery } from '@tanstack/react-query';
import Fuse from 'fuse.js';
import { format, addDays } from 'date-fns';
import { toast } from 'sonner';
import { formatCurrency as fmtCurrency } from '@/lib/format';
import { validateQuoteForm, QUOTE_FIELD_LABELS } from '@/lib/validations';
-import {
- useQuoteTemplates,
- type QuoteTemplate,
- type QuoteTemplateItem,
-} from '@/hooks/quotes';
import { useAuth } from '@/contexts/AuthContext';
-import { findKnownHex, type ExternalVariantStock } from "@/hooks/products";
+import { findKnownHex, type ExternalVariantStock } from '@/hooks/products';
import { useDebounce } from '@/hooks/common';
import type {
SelectedCompanyInfo,
@@ -170,22 +176,29 @@ export function useQuoteBuilderState() {
}
}, []);
- const handleShippingTypeChange = useCallback((value: string) => {
- setShippingType(value);
- if (value !== 'fob_pre' && shippingCost !== 0) {
- setShippingCost(0);
- }
- setTimeout(() => {
- // Pequeno delay para garantir que o estado foi processado antes de avisar
- toast.success(`Frete alterado para: ${
- value === 'cif' ? 'CIF' :
- value === 'fob' ? 'FOB' :
- 'FOB Pré-negociado'
- }`, {
- description: value === 'fob_pre' ? 'Lembre-se de informar o valor acordado.' : 'O custo será zerado no orçamento.',
- });
- }, 50);
- }, [shippingCost]);
+ const handleShippingTypeChange = useCallback(
+ (value: string) => {
+ setShippingType(value);
+ if (value !== 'fob_pre' && shippingCost !== 0) {
+ setShippingCost(0);
+ }
+ setTimeout(() => {
+ // Pequeno delay para garantir que o estado foi processado antes de avisar
+ toast.success(
+ `Frete alterado para: ${
+ value === 'cif' ? 'CIF' : value === 'fob' ? 'FOB' : 'FOB Pré-negociado'
+ }`,
+ {
+ description:
+ value === 'fob_pre'
+ ? 'Lembre-se de informar o valor acordado.'
+ : 'O custo será zerado no orçamento.',
+ },
+ );
+ }, 50);
+ },
+ [shippingCost],
+ );
const [productSearchOpen, setProductSearchOpen] = useState(false);
const [productSearch, setProductSearch] = useState('');
@@ -205,7 +218,7 @@ export function useQuoteBuilderState() {
const steps: QuoteBuilderStep[] = [];
if (clientId && contactId) steps.push('client');
if (paymentMethod && paymentTerms && deliveryTime && shippingType) {
- if (shippingType !== 'fob_pre' || (shippingCost > 0)) {
+ if (shippingType !== 'fob_pre' || shippingCost > 0) {
steps.push('conditions');
}
}
@@ -214,7 +227,16 @@ export function useQuoteBuilderState() {
const hasAnyPersonalization = items.some((it) => (it.personalizations?.length ?? 0) > 0);
if (items.length > 0 && hasAnyPersonalization) steps.push('personalization');
return steps;
- }, [clientId, contactId, items, paymentMethod, paymentTerms, deliveryTime, shippingType, shippingCost]);
+ }, [
+ clientId,
+ contactId,
+ items,
+ paymentMethod,
+ paymentTerms,
+ deliveryTime,
+ shippingType,
+ shippingCost,
+ ]);
const announce = useCallback((message: string) => {
const announcer = document.getElementById('quote-builder-announcer');
@@ -223,75 +245,94 @@ export function useQuoteBuilderState() {
}
}, []);
- const validateStep = useCallback((step: QuoteBuilderStep): boolean => {
- switch (step) {
- case 'client':
- if (!clientId) {
- toast.error('Selecione um cliente');
- announce('Erro: Selecione um cliente');
- return false;
- }
- if (!contactId) {
- toast.error('Selecione um contato');
- announce('Erro: Selecione um contato');
- return false;
- }
- return true;
- case 'conditions': {
- const errors = validateQuoteForm({
- clientId,
- contactId,
- paymentMethod,
- paymentTerms,
- deliveryTime,
- shippingType,
- shippingCost,
- itemsCount: items.length,
- });
+ const validateStep = useCallback(
+ (step: QuoteBuilderStep): boolean => {
+ switch (step) {
+ case 'client':
+ if (!clientId) {
+ toast.error('Selecione um cliente');
+ announce('Erro: Selecione um cliente');
+ return false;
+ }
+ if (!contactId) {
+ toast.error('Selecione um contato');
+ announce('Erro: Selecione um contato');
+ return false;
+ }
+ return true;
+ case 'conditions': {
+ const errors = validateQuoteForm({
+ clientId,
+ contactId,
+ paymentMethod,
+ paymentTerms,
+ deliveryTime,
+ shippingType,
+ shippingCost,
+ itemsCount: items.length,
+ });
- if (errors.includes('forma_pagamento')) {
- toast.error('Selecione a forma de pagamento');
- return false;
- }
- if (errors.includes('prazo_pagamento')) {
- toast.error('Selecione o prazo de pagamento');
- return false;
- }
- if (errors.includes('prazo_entrega')) {
- toast.error('Defina o prazo de entrega');
- return false;
- }
- if (errors.includes('frete')) {
- toast.error('Selecione a modalidade de frete');
- announce('Erro: Selecione a modalidade de frete');
- return false;
- }
- if (errors.includes('valor_frete')) {
- toast.error('Informe o valor do frete pré-negociado');
- return false;
+ if (errors.includes('forma_pagamento')) {
+ toast.error('Selecione a forma de pagamento');
+ return false;
+ }
+ if (errors.includes('prazo_pagamento')) {
+ toast.error('Selecione o prazo de pagamento');
+ return false;
+ }
+ if (errors.includes('prazo_entrega')) {
+ toast.error('Defina o prazo de entrega');
+ return false;
+ }
+ if (errors.includes('frete')) {
+ toast.error('Selecione a modalidade de frete');
+ announce('Erro: Selecione a modalidade de frete');
+ return false;
+ }
+ if (errors.includes('valor_frete')) {
+ toast.error('Informe o valor do frete pré-negociado');
+ return false;
+ }
+ return true;
}
- return true;
+ case 'items':
+ if (items.length === 0) {
+ toast.error('Adicione pelo menos um item');
+ announce('Erro: Adicione pelo menos um item');
+ return false;
+ }
+ return true;
+ case 'personalization':
+ return true;
+ case 'review':
+ return true;
+ default:
+ return true;
}
- case 'items':
- if (items.length === 0) {
- toast.error('Adicione pelo menos um item');
- announce('Erro: Adicione pelo menos um item');
- return false;
- }
- return true;
- case 'personalization':
- return true;
- case 'review':
- return true;
- default:
- return true;
- }
- }, [clientId, contactId, paymentMethod, paymentTerms, deliveryTime, shippingType, shippingCost, items, announce]);
+ },
+ [
+ clientId,
+ contactId,
+ paymentMethod,
+ paymentTerms,
+ deliveryTime,
+ shippingType,
+ shippingCost,
+ items,
+ announce,
+ ],
+ );
const nextStep = useCallback(() => {
- const steps: QuoteBuilderStep[] = ['client', 'conditions', 'items', 'personalization', 'review'];
+ const steps: QuoteBuilderStep[] = [
+ 'client',
+ 'conditions',
+ 'items',
+ 'personalization',
+ 'review',
+ ];
const currentIndex = steps.indexOf(currentStep);
-
+
if (validateStep(currentStep)) {
if (currentIndex < steps.length - 1) {
setCurrentStep(steps[currentIndex + 1]);
@@ -301,33 +342,48 @@ export function useQuoteBuilderState() {
}, [currentStep, validateStep]);
const prevStep = useCallback(() => {
- const steps: QuoteBuilderStep[] = ['client', 'conditions', 'items', 'personalization', 'review'];
+ const steps: QuoteBuilderStep[] = [
+ 'client',
+ 'conditions',
+ 'items',
+ 'personalization',
+ 'review',
+ ];
const currentIndex = steps.indexOf(currentStep);
-
+
if (currentIndex > 0) {
setCurrentStep(steps[currentIndex - 1]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}, [currentStep]);
-
- const goToStep = useCallback((step: QuoteBuilderStep) => {
- const steps: QuoteBuilderStep[] = ['client', 'conditions', 'items', 'personalization', 'review'];
- const targetIndex = steps.indexOf(step);
- const currentIndex = steps.indexOf(currentStep);
-
- if (targetIndex === currentIndex) return;
- // Se estiver tentando ir para uma etapa posterior, validar as anteriores
- if (targetIndex > currentIndex) {
- // Validar cada etapa entre a atual e a alvo (não inclusiva da alvo, pois a alvo é onde queremos chegar)
- for (let i = currentIndex; i < targetIndex; i++) {
- if (!validateStep(steps[i])) return;
+ const goToStep = useCallback(
+ (step: QuoteBuilderStep) => {
+ const steps: QuoteBuilderStep[] = [
+ 'client',
+ 'conditions',
+ 'items',
+ 'personalization',
+ 'review',
+ ];
+ const targetIndex = steps.indexOf(step);
+ const currentIndex = steps.indexOf(currentStep);
+
+ if (targetIndex === currentIndex) return;
+
+ // Se estiver tentando ir para uma etapa posterior, validar as anteriores
+ if (targetIndex > currentIndex) {
+ // Validar cada etapa entre a atual e a alvo (não inclusiva da alvo, pois a alvo é onde queremos chegar)
+ for (let i = currentIndex; i < targetIndex; i++) {
+ if (!validateStep(steps[i])) return;
+ }
}
- }
- setCurrentStep(step);
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }, [currentStep, validateStep]);
+ setCurrentStep(step);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ },
+ [currentStep, validateStep],
+ );
// ── AutoSave ──
const { clearAutoSave } = useAutoSaveQuote({
enabled: (!!clientId || items.length > 0) && !isEditMode,
@@ -792,7 +848,16 @@ export function useQuoteBuilderState() {
shippingCost,
itemsCount: items.length,
}),
- [clientId, contactId, paymentMethod, paymentTerms, deliveryTime, shippingType, shippingCost, items],
+ [
+ clientId,
+ contactId,
+ paymentMethod,
+ paymentTerms,
+ deliveryTime,
+ shippingType,
+ shippingCost,
+ items,
+ ],
);
const isFormValid = validationErrors.length === 0;
@@ -867,8 +932,7 @@ export function useQuoteBuilderState() {
payment_terms: paymentTerms || undefined,
delivery_time: deliveryTime || undefined,
shipping_type: shippingType || undefined,
- shipping_cost:
- shippingType === 'fob_pre' ? (shippingCost || 0) : 0,
+ shipping_cost: shippingType === 'fob_pre' ? shippingCost || 0 : 0,
};
let result;
if (isEditMode && quoteId) {
diff --git a/src/lib/external-db/product-types.ts b/src/lib/external-db/product-types.ts
index 807cf3ffa..aa7817ac4 100644
--- a/src/lib/external-db/product-types.ts
+++ b/src/lib/external-db/product-types.ts
@@ -74,14 +74,32 @@ export interface PromobrindProduct {
price_updated_at?: string | null;
price_freshness_threshold_days?: number | null;
kit_components?: Array<{
- id: string; component_name: string | null; component_code: string | null;
- component_product_id: string | null; component_sku: string | null;
- quantity: number | null; display_order: number | null;
- is_optional: boolean | null; is_packaging: boolean | null;
- is_replaceable: boolean | null; allows_personalization: boolean | null;
- material: string | null; primary_image_url: string | null;
- height_mm: number | null; width_mm: number | null; length_mm: number | null;
- weight_g: number | null; notes: string | null;
+ id: string;
+ component_name: string | null;
+ component_code: string | null;
+ component_product_id: string | null;
+ component_sku: string | null;
+ quantity: number | null;
+ display_order: number | null;
+ is_optional: boolean | null;
+ is_packaging: boolean | null;
+ is_replaceable: boolean | null;
+ allows_personalization: boolean | null;
+ material: string | null;
+ primary_image_url: string | null;
+ height_mm: number | null;
+ width_mm: number | null;
+ length_mm: number | null;
+ weight_g: number | null;
+ notes: string | null;
+ // Campos provenientes do join `product_kit_components` × `products`
+ // (vide JSON_BUILD_OBJECT em supabase/migrations/20250103070000…).
+ // Podem vir ausentes em produtos mais antigos sem catálogo completo.
+ component_type_code?: string | null;
+ supplier_component_code?: string | null;
+ component_description?: string | null;
+ personalization_notes?: string | null;
+ color?: string | null;
}> | null;
// ------------------------------------------------------------------
@@ -208,5 +226,7 @@ export const PRODUCT_SELECT_FIELDS_DETAIL =
// #2: also trigger fallback when orderBy hits a missing column
export function shouldFallbackSelect(err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
- return /(sale_price|base_price|image_url|supplier_name|category_name|product_videos|selected_images|gender|price_updated_at|price_freshness_threshold_days|does not exist|não existe|undefined column|column .+ does not exist|could not identify an ordering operator|order by)/i.test(msg);
+ return /(sale_price|base_price|image_url|supplier_name|category_name|product_videos|selected_images|gender|price_updated_at|price_freshness_threshold_days|does not exist|não existe|undefined column|column .+ does not exist|could not identify an ordering operator|order by)/i.test(
+ msg,
+ );
}
diff --git a/src/lib/print-area-grouping.ts b/src/lib/print-area-grouping.ts
index 897b3937c..f9074b762 100644
--- a/src/lib/print-area-grouping.ts
+++ b/src/lib/print-area-grouping.ts
@@ -9,7 +9,7 @@
* - Estatísticas e resumos
* - Detecção de área máxima por grupo
*/
-import type { PrintAreaWithTechniques, GroupedPrintArea } from "@/types/gravacao";
+import type { PrintAreaWithTechniques, GroupedPrintArea } from '@/types/gravacao';
// ============================================
// AGRUPAMENTO PRINCIPAL
@@ -19,35 +19,35 @@ import type { PrintAreaWithTechniques, GroupedPrintArea } from "@/types/gravacao
* Agrupa áreas de impressão/personalização por componente → localização → técnicas.
* Áreas sem componente são agrupadas sob "Produto" (default).
*/
-export function groupPrintAreasByComponent(
- areas: PrintAreaWithTechniques[]
-): GroupedPrintArea[] {
+export function groupPrintAreasByComponent(areas: PrintAreaWithTechniques[]): GroupedPrintArea[] {
if (!areas.length) return [];
- const componentMap = new Map
>();
+ const componentMap = new Map<
+ string,
+ Map
+ >();
for (const area of areas) {
- const compName = area.component_name || "Produto";
- const locName = area.location_name || area.area_name || "Padrão";
+ const compName = area.component_name || 'Produto';
+ const locName = area.location_name || area.area_name || 'Padrão';
- if (!componentMap.has(compName)) {
- componentMap.set(compName, new Map());
+ let locMap = componentMap.get(compName);
+ if (!locMap) {
+ locMap = new Map();
+ componentMap.set(compName, locMap);
}
- const locMap = componentMap.get(compName)!;
- if (!locMap.has(locName)) {
- locMap.set(locName, []);
+ let techniques = locMap.get(locName);
+ if (!techniques) {
+ techniques = [];
+ locMap.set(locName, techniques);
}
- const techniques = locMap.get(locName)!;
-
for (const tech of area.techniques) {
const code = tech.codigo;
// Deduplicação: evita técnica duplicada na mesma localização+área
- const isDuplicate = techniques.some(
- (t) => t.techniqueCode === code && t.id === area.area_id
- );
+ const isDuplicate = techniques.some((t) => t.techniqueCode === code && t.id === area.area_id);
if (isDuplicate) continue;
techniques.push({
@@ -71,12 +71,12 @@ export function groupPrintAreasByComponent(
const grouped: GroupedPrintArea[] = [];
for (const [compName, locMap] of componentMap) {
- const locations: GroupedPrintArea["locations"] = [];
+ const locations: GroupedPrintArea['locations'] = [];
for (const [locName, techniques] of locMap) {
locations.push({
locationName: locName,
- locationCode: locName.toLowerCase().replace(/\s+/g, "-"),
+ locationCode: locName.toLowerCase().replace(/\s+/g, '-'),
techniques,
});
}
@@ -92,15 +92,15 @@ export function groupPrintAreasByComponent(
grouped.push({
componentName: compName,
- componentCode: compName.toLowerCase().replace(/\s+/g, "-"),
+ componentCode: compName.toLowerCase().replace(/\s+/g, '-'),
locations,
});
}
// Sort: "Produto" first, then alphabetical
grouped.sort((a, b) => {
- if (a.componentName === "Produto") return -1;
- if (b.componentName === "Produto") return 1;
+ if (a.componentName === 'Produto') return -1;
+ if (b.componentName === 'Produto') return 1;
return a.componentName.localeCompare(b.componentName);
});
@@ -131,7 +131,7 @@ export function getUniqueTechniques(groups: GroupedPrintArea[]): string[] {
*/
export function filterGroupsByTechnique(
groups: GroupedPrintArea[],
- techniqueCode: string
+ techniqueCode: string,
): GroupedPrintArea[] {
return groups
.map((g) => ({
@@ -151,7 +151,7 @@ export function filterGroupsByTechnique(
*/
export function filterGroupsByComponent(
groups: GroupedPrintArea[],
- componentName: string
+ componentName: string,
): GroupedPrintArea[] {
return groups.filter((g) => g.componentName === componentName);
}
@@ -294,7 +294,7 @@ export function summarizeGroups(groups: GroupedPrintArea[]): PrintAreaSummary {
* Encontra a maior área disponível (em cm²) entre todos os grupos.
*/
export function findLargestArea(
- groups: GroupedPrintArea[]
+ groups: GroupedPrintArea[],
): { componentName: string; locationName: string; areaCm2: number } | null {
let largest: { componentName: string; locationName: string; areaCm2: number } | null = null;
diff --git a/src/pages/products/FavoritesPage.tsx b/src/pages/products/FavoritesPage.tsx
index e55380be0..95d13110e 100644
--- a/src/pages/products/FavoritesPage.tsx
+++ b/src/pages/products/FavoritesPage.tsx
@@ -1,58 +1,72 @@
-import { useEffect, useMemo, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { PageSEO } from "@/components/seo/PageSEO";
-import { useFavoritesStore, type FavoriteVariantInfo } from "@/stores/useFavoritesStore";
+import { useEffect, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { PageSEO } from '@/components/seo/PageSEO';
+import { useFavoritesStore, type FavoriteVariantInfo } from '@/stores/useFavoritesStore';
import {
+ useEnrichedFavoriteItems,
useFavoriteLists,
+ useFavoritesGlobalShortcuts,
useFavoriteTrash,
useLegacyFavoritesMigration,
-} from "@/hooks/favorites";
-import { useEnrichedFavoriteItems, useFavoritesGlobalShortcuts } from "@/hooks/favorites";
-import { useProductsContext } from "@/contexts/ProductsContext";
-import { ProductCard } from "@/components/products/ProductCard";
-import { ProductListItem } from "@/components/products/ProductListItem";
-import { ProductTableView } from "@/components/products/ProductTableView";
-import { LayoutPopover } from "@/components/products/LayoutPopover";
-import { getDefaultColumns, type ColumnCount } from "@/components/products/ColumnSelector";
-import { getGridColsClass, getGridGapClass } from "@/components/replenishments/VirtualizedReplenishmentGrid";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Input } from "@/components/ui/input";
-import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
+} from '@/hooks/favorites';
+import { useProductsContext } from '@/contexts/ProductsContext';
+import { ProductCard } from '@/components/products/ProductCard';
+import { ProductListItem } from '@/components/products/ProductListItem';
+import { ProductTableView } from '@/components/products/ProductTableView';
+import { LayoutPopover } from '@/components/products/LayoutPopover';
+import { getDefaultColumns, type ColumnCount } from '@/components/products/ColumnSelector';
import {
- Heart, Trash2, Search, Package, Layers, TrendingDown, TrendingUp,
- CheckSquare, X, FolderOpen,
-} from "lucide-react";
-import { cn } from "@/lib/utils";
-import { motion, AnimatePresence } from "framer-motion";
-import { toast } from "sonner";
-import { DeleteConfirmDialog } from "@/components/ui/ConfirmDialog";
-
-import { useCatalogSelection } from "@/components/catalog/useCatalogSelection";
-import { CatalogBulkModals } from "@/components/catalog/CatalogBulkModals";
-import { FavoriteListsSidebar } from "@/components/favorites/FavoriteListsSidebar";
-import { FavoritesTrashView } from "@/components/favorites/FavoritesTrashView";
-import { FavoritesViewHeader } from "@/components/favorites/FavoritesViewHeader";
-import { ItemNoteEditor } from "@/components/favorites/ItemNoteEditor";
-import { PriceDropBadge } from "@/components/favorites/PriceDropBadge";
-import { FavoritesEmptyStateSmart } from "@/components/favorites/FavoritesEmptyStateSmart";
-import { FavoritePresentationLauncher } from "@/components/favorites/FavoritePresentationLauncher";
-import { useUndoStack } from "@/hooks/common";
-import type { FavoritesSort } from "@/components/favorites/FavoritesSortBar";
-
-type ViewMode = "grid" | "list" | "table";
-const VIEW_MODE_KEY = "favorites-view-mode";
-const GRID_COLS_KEY = "favorites-grid-cols";
-const SELECTED_LIST_KEY = "favorites-selected-list-id";
-const SORT_KEY = "favorites-sort";
-const PRICE_DROP_FILTER_KEY = "favorites-only-drops";
+ getGridColsClass,
+ getGridGapClass,
+} from '@/components/replenishments/VirtualizedReplenishmentGrid';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
+import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
+import {
+ Heart,
+ Trash2,
+ Search,
+ Package,
+ Layers,
+ TrendingDown,
+ TrendingUp,
+ CheckSquare,
+ X,
+ FolderOpen,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { motion, AnimatePresence } from 'framer-motion';
+import { toast } from 'sonner';
+import { DeleteConfirmDialog } from '@/components/ui/ConfirmDialog';
+
+import { useCatalogSelection } from '@/components/catalog/useCatalogSelection';
+import { CatalogBulkModals } from '@/components/catalog/CatalogBulkModals';
+import { FavoriteListsSidebar } from '@/components/favorites/FavoriteListsSidebar';
+import { FavoritesTrashView } from '@/components/favorites/FavoritesTrashView';
+import { FavoritesViewHeader } from '@/components/favorites/FavoritesViewHeader';
+import { ItemNoteEditor } from '@/components/favorites/ItemNoteEditor';
+import { PriceDropBadge } from '@/components/favorites/PriceDropBadge';
+import { FavoritesEmptyStateSmart } from '@/components/favorites/FavoritesEmptyStateSmart';
+import { FavoritePresentationLauncher } from '@/components/favorites/FavoritePresentationLauncher';
+import { useUndoStack } from '@/hooks/common';
+import type { FavoritesSort } from '@/components/favorites/FavoritesSortBar';
+
+type ViewMode = 'grid' | 'list' | 'table';
+const VIEW_MODE_KEY = 'favorites-view-mode';
+const GRID_COLS_KEY = 'favorites-grid-cols';
+const SELECTED_LIST_KEY = 'favorites-selected-list-id';
+const SORT_KEY = 'favorites-sort';
+const PRICE_DROP_FILTER_KEY = 'favorites-only-drops';
function loadViewMode(): ViewMode {
try {
const v = localStorage.getItem(VIEW_MODE_KEY);
- if (v === "grid" || v === "list" || v === "table") return v as ViewMode;
- } catch { /* empty */ }
- return "grid";
+ if (v === 'grid' || v === 'list' || v === 'table') return v as ViewMode;
+ } catch {
+ /* empty */
+ }
+ return 'grid';
}
function loadGridColumns(): ColumnCount {
@@ -62,17 +76,29 @@ function loadGridColumns(): ColumnCount {
const n = Number(v) as ColumnCount;
if ([3, 4, 5, 6, 8].includes(n)) return n as ColumnCount;
}
- } catch { /* empty */ }
+ } catch {
+ /* empty */
+ }
return getDefaultColumns();
}
function loadSort(): FavoritesSort {
try {
const v = localStorage.getItem(SORT_KEY) as FavoritesSort | null;
- const allowed: FavoritesSort[] = ["recent", "oldest", "price-asc", "price-desc", "name-asc", "name-desc", "category"];
+ const allowed: FavoritesSort[] = [
+ 'recent',
+ 'oldest',
+ 'price-asc',
+ 'price-desc',
+ 'name-asc',
+ 'name-desc',
+ 'category',
+ ];
if (v && allowed.includes(v)) return v;
- } catch { /* empty */ }
- return "recent";
+ } catch {
+ /* empty */
+ }
+ return 'recent';
}
export default function FavoritesPage() {
@@ -82,53 +108,85 @@ export default function FavoritesPage() {
useUndoStack();
useLegacyFavoritesMigration();
- const { favorites, clearFavorites, favoriteCount, toggleFavorite, isFavorite } = useFavoritesStore();
+ const { favorites, clearFavorites, favoriteCount, toggleFavorite, isFavorite } =
+ useFavoritesStore();
- const {
- lists,
- createList,
- updateList,
- deleteList,
- generateShareToken,
- revokeShareToken,
- } = useFavoriteLists();
+ const { lists, createList, updateList, deleteList, generateShareToken, revokeShareToken } =
+ useFavoriteLists();
const { items: trashItems } = useFavoriteTrash();
const [selectedListId, setSelectedListId] = useState(() => {
- try { return localStorage.getItem(SELECTED_LIST_KEY); } catch { return null; }
+ try {
+ return localStorage.getItem(SELECTED_LIST_KEY);
+ } catch {
+ return null;
+ }
});
const [showTrash, setShowTrash] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [presenting, setPresenting] = useState(false);
- const [ariaAnnouncement, setAriaAnnouncement] = useState("");
+ const [ariaAnnouncement, setAriaAnnouncement] = useState('');
useEffect(() => {
try {
if (selectedListId) localStorage.setItem(SELECTED_LIST_KEY, selectedListId);
else localStorage.removeItem(SELECTED_LIST_KEY);
- } catch { /* empty */ }
+ } catch {
+ /* empty */
+ }
}, [selectedListId]);
const { enriched, rawItems, removeItem, updateItem } = useEnrichedFavoriteItems(selectedListId);
const isRemoteListView = !!selectedListId && !showTrash;
const { getProductsByIds, products: _cacheSignal } = useProductsContext();
- const [searchQuery, setSearchQuery] = useState("");
+ const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState(() => loadViewMode());
const [gridColumns, setGridColumns] = useState(() => loadGridColumns());
const [sort, setSort] = useState(() => loadSort());
const [selectionMode, setSelectionMode] = useState(false);
const [onlyPriceDrops, setOnlyPriceDrops] = useState(() => {
- try { return localStorage.getItem(PRICE_DROP_FILTER_KEY) === "1"; } catch { return false; }
+ try {
+ return localStorage.getItem(PRICE_DROP_FILTER_KEY) === '1';
+ } catch {
+ return false;
+ }
});
- useEffect(() => { try { localStorage.setItem(VIEW_MODE_KEY, viewMode); } catch { /* empty */ } }, [viewMode]);
- useEffect(() => { try { localStorage.setItem(GRID_COLS_KEY, String(gridColumns)); } catch { /* empty */ } }, [gridColumns]);
- useEffect(() => { try { localStorage.setItem(SORT_KEY, sort); } catch { /* empty */ } }, [sort]);
- useEffect(() => { try { localStorage.setItem(PRICE_DROP_FILTER_KEY, onlyPriceDrops ? "1" : "0"); } catch { /* empty */ } }, [onlyPriceDrops]);
+ useEffect(() => {
+ try {
+ localStorage.setItem(VIEW_MODE_KEY, viewMode);
+ } catch {
+ /* empty */
+ }
+ }, [viewMode]);
+ useEffect(() => {
+ try {
+ localStorage.setItem(GRID_COLS_KEY, String(gridColumns));
+ } catch {
+ /* empty */
+ }
+ }, [gridColumns]);
+ useEffect(() => {
+ try {
+ localStorage.setItem(SORT_KEY, sort);
+ } catch {
+ /* empty */
+ }
+ }, [sort]);
+ useEffect(() => {
+ try {
+ localStorage.setItem(PRICE_DROP_FILTER_KEY, onlyPriceDrops ? '1' : '0');
+ } catch {
+ /* empty */
+ }
+ }, [onlyPriceDrops]);
const enrichedMetaMap = useMemo(() => {
- const m = new Map();
+ const m = new Map<
+ string,
+ { priceDiffPct: number | null; priceAtSave: number | null; savedAt: string }
+ >();
if (isRemoteListView) {
enriched.forEach((e) => {
m.set(e.item.product_id, {
@@ -148,7 +206,7 @@ export default function FavoritesPage() {
const legacyFavoriteProducts = useMemo(
() => getProductsByIds(favorites.map((f) => f.productId)),
- [getProductsByIds, favorites, _cacheSignal]
+ [getProductsByIds, favorites, _cacheSignal],
);
const variantMap = useMemo(() => {
@@ -192,27 +250,46 @@ export default function FavoritesPage() {
let list = productsWithVariant;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
- list = list.filter((p) =>
- p.name.toLowerCase().includes(q) ||
- p.sku?.toLowerCase().includes(q) ||
- p.brand?.toLowerCase().includes(q)
+ list = list.filter(
+ (p) =>
+ p.name.toLowerCase().includes(q) ||
+ p.sku?.toLowerCase().includes(q) ||
+ p.brand?.toLowerCase().includes(q),
);
}
if (onlyPriceDrops && isRemoteListView) {
list = list.filter((p) => {
const meta = enrichedMetaMap.get(p.id);
- return meta?.priceDiffPct !== null && meta?.priceDiffPct !== undefined && meta.priceDiffPct < -2;
+ return (
+ meta?.priceDiffPct !== null && meta?.priceDiffPct !== undefined && meta.priceDiffPct < -2
+ );
});
}
const sorted = [...list];
switch (sort) {
- case "price-asc": sorted.sort((a, b) => (a.price ?? 0) - (b.price ?? 0)); break;
- case "price-desc": sorted.sort((a, b) => (b.price ?? 0) - (a.price ?? 0)); break;
- case "name-asc": sorted.sort((a, b) => a.name.localeCompare(b.name, "pt-BR")); break;
- case "name-desc": sorted.sort((a, b) => b.name.localeCompare(a.name, "pt-BR")); break;
- case "category": sorted.sort((a, b) => (a.category_name ?? "").localeCompare(b.category_name ?? "", "pt-BR")); break;
- case "oldest": sorted.reverse(); break;
- case "recent": default: break;
+ case 'price-asc':
+ sorted.sort((a, b) => (a.price ?? 0) - (b.price ?? 0));
+ break;
+ case 'price-desc':
+ sorted.sort((a, b) => (b.price ?? 0) - (a.price ?? 0));
+ break;
+ case 'name-asc':
+ sorted.sort((a, b) => a.name.localeCompare(b.name, 'pt-BR'));
+ break;
+ case 'name-desc':
+ sorted.sort((a, b) => b.name.localeCompare(a.name, 'pt-BR'));
+ break;
+ case 'category':
+ sorted.sort((a, b) =>
+ (a.category_name ?? '').localeCompare(b.category_name ?? '', 'pt-BR'),
+ );
+ break;
+ case 'oldest':
+ sorted.reverse();
+ break;
+ case 'recent':
+ default:
+ break;
}
return sorted;
}, [productsWithVariant, searchQuery, sort, onlyPriceDrops, isRemoteListView, enrichedMetaMap]);
@@ -232,18 +309,22 @@ export default function FavoritesPage() {
};
}, [productsWithVariant, legacyFavoriteProducts, isRemoteListView]);
- const fmt = (v: number) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v);
+ const fmt = (v: number) =>
+ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v);
- const activeList = useMemo(() => lists.find((l) => l.id === selectedListId) ?? null, [lists, selectedListId]);
+ const activeList = useMemo(
+ () => lists.find((l) => l.id === selectedListId) ?? null,
+ [lists, selectedListId],
+ );
const headerTotalCount = isRemoteListView ? rawItems.length : favoriteCount;
const handleClearAll = () => {
if (isRemoteListView) {
- toast.info("Use a lixeira para remover items individualmente");
+ toast.info('Use a lixeira para remover items individualmente');
return;
}
clearFavorites();
- toast.success("Todos os favoritos foram removidos");
+ toast.success('Todos os favoritos foram removidos');
};
const toggleSelectionMode = () => {
@@ -263,7 +344,7 @@ export default function FavoritesPage() {
} else {
ids.forEach((id) => toggleFavorite(id));
}
- toast.success(`${ids.length} ${ids.length === 1 ? "item removido" : "itens removidos"}`);
+ toast.success(`${ids.length} ${ids.length === 1 ? 'item removido' : 'itens removidos'}`);
sel.clearSelection();
setSelectionMode(false);
};
@@ -280,7 +361,9 @@ export default function FavoritesPage() {
};
const handleToggleFavorite = (productId: string) => {
- const product = (isRemoteListView ? productsWithVariant : legacyFavoriteProducts).find((p) => p.id === productId);
+ const product = (isRemoteListView ? productsWithVariant : legacyFavoriteProducts).find(
+ (p) => p.id === productId,
+ );
if (isRemoteListView) {
const meta = noteMap.get(productId);
if (meta) removeItem.mutate(meta.itemId);
@@ -295,418 +378,496 @@ export default function FavoritesPage() {
const meta = noteMap.get(productId);
if (!meta) return;
await updateItem.mutateAsync({ id: meta.itemId, note });
- toast.success("Nota salva");
+ toast.success('Nota salva');
};
const sidebarNode = (
{ setSelectedListId(id); setShowTrash(false); setSidebarOpen(false); }}
- onCreateList={async (data) => { await createList.mutateAsync(data); }}
- onUpdateList={async (id, patch) => { await updateList.mutateAsync({ id, ...patch }); }}
+ onSelectList={(id) => {
+ setSelectedListId(id);
+ setShowTrash(false);
+ setSidebarOpen(false);
+ }}
+ onCreateList={async (data) => {
+ await createList.mutateAsync(data);
+ }}
+ onUpdateList={async (id, patch) => {
+ await updateList.mutateAsync({ id, ...patch });
+ }}
onDeleteList={async (id) => {
await deleteList.mutateAsync(id);
if (selectedListId === id) setSelectedListId(null);
}}
- onShareList={async (id, days) => generateShareToken.mutateAsync({ listId: id, expiresInDays: days })}
- onRevokeShare={async (id) => { await revokeShareToken.mutateAsync(id); }}
+ onShareList={async (id, days) =>
+ generateShareToken.mutateAsync({ listId: id, expiresInDays: days })
+ }
+ onRevokeShare={async (id) => {
+ await revokeShareToken.mutateAsync(id);
+ }}
trashCount={trashItems.length}
showTrash={showTrash}
- onToggleTrash={(s) => { setShowTrash(s); if (s) setSidebarOpen(false); }}
+ onToggleTrash={(s) => {
+ setShowTrash(s);
+ if (s) setSidebarOpen(false);
+ }}
/>
);
return (
- <>
-
-
-
-
-
-
-
-
-
- Meus Favoritos
-
-
- {headerTotalCount} {" "}
- {headerTotalCount === 1 ? "item" : "itens"}
- {lists.length > 0 && (
- <>
- {" • "}
- {lists.length} {" "}
- {lists.length === 1 ? "lista" : "listas"}
- >
- )}
-
-
+ <>
+
+
+
+
+
+
-
-
-
-
-
-
- Listas
-
-
-
- {sidebarNode}
-
-
-
- {(headerTotalCount > 0 && !showTrash) && (
- <>
- {!isRemoteListView && (
-
-
- Limpar Tudo
-
- }
- title="Limpar todos os favoritos?"
- description={`Esta ação irá remover todos os ${favoriteCount} produtos.`}
- onConfirm={handleClearAll}
- itemName="favoritos"
- />
- )}
-
-
- {selectionMode ? "Cancelar" : "Selecionar"}
-
- {selectionMode && selectedIds.size > 0 && (
-
-
- {selectedIds.size}
-
-
- )}
-
-
-
-
-
- >
- )}
+
+
+ Meus Favoritos
+
+
+ {headerTotalCount} {' '}
+ {headerTotalCount === 1 ? 'item' : 'itens'}
+ {lists.length > 0 && (
+ <>
+ {' • '}
+ {lists.length} {' '}
+ {lists.length === 1 ? 'lista' : 'listas'}
+ >
+ )}
+
-
-
- {sidebarNode}
-
-
-
- {showTrash ? (
- <>
-
-
-
Lixeira
-
-
- >
- ) : (
- <>
-
0 ? () => setPresenting(true) : undefined}
+
+
+
+
+
+ Listas
+
+
+
+ {sidebarNode}
+
+
+
+ {headerTotalCount > 0 && !showTrash && (
+ <>
+ {!isRemoteListView && (
+
+
+ Limpar Tudo
+
+ }
+ title="Limpar todos os favoritos?"
+ description={`Esta ação irá remover todos os ${favoriteCount} produtos.`}
+ onConfirm={handleClearAll}
+ itemName="favoritos"
+ />
+ )}
+
+
+
+ {selectionMode ? 'Cancelar' : 'Selecionar'}
+
+
+ {selectionMode && selectedIds.size > 0 && (
+
+
+ {selectedIds.size}
+
+
+ )}
+
+
+
+
+
+ >
+ )}
+
+
- {stats && (
-
-
-
-
-
{stats.total}
-
Produtos
-
+
+
{sidebarNode}
+
+
+ {showTrash ? (
+ <>
+
+
+
Lixeira
+
+
+ >
+ ) : (
+ <>
+
0 ? () => setPresenting(true) : undefined}
+ />
+
+ {stats && (
+
+
+
-
-
-
-
-
-
{stats.categories}
-
Categorias
-
+
+
+ {stats.total}
+
+
Produtos
-
-
-
-
-
-
{fmt(stats.minPrice)}
-
Menor preço
-
+
+
+
+
-
-
-
-
-
-
{fmt(stats.maxPrice)}
-
Maior preço
-
+
+
+ {stats.categories}
+
+
Categorias
- )}
-
- {productsWithVariant.length > 0 && (
-
-
-
setSearchQuery(e.target.value)}
- className="pl-9"
- />
+
+
+
+
+
+
+ {fmt(stats.minPrice)}
+
+
Menor preço
+
- )}
-
- {selectionMode && productsWithVariant.length > 0 && (
-
-
-
-
- {selectedIds.size} {selectedIds.size === 1 ? "selecionado" : "selecionados"}
-
-
de {filteredProducts.length}
+
+
+
-
-
- Selecionar tudo
-
-
-
- Limpar
-
-
-
- Remover ({selectedIds.size})
-
- }
- title="Remover selecionados?"
- description={`Esta ação irá remover ${selectedIds.size} ${selectedIds.size === 1 ? "item" : "itens"}.`}
- onConfirm={handleRemoveSelected}
- itemName="itens selecionados"
- />
+
+
+ {fmt(stats.maxPrice)}
+
+
Maior preço
- )}
-
- {filteredProducts.length > 0 ? (
- viewMode === "table" ? (
-
navigate(`/produto/${productId}`)}
- isFavorite={isFavorite}
- onToggleFavorite={handleToggleFavorite}
- selectionMode={selectionMode}
- selectedIds={selectedIds}
- onToggleSelect={sel.toggleSelect}
+
+ )}
+
+ {productsWithVariant.length > 0 && (
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+
+ )}
+
+ {selectionMode && productsWithVariant.length > 0 && (
+
+
+
+
+ {selectedIds.size} {selectedIds.size === 1 ? 'selecionado' : 'selecionados'}
+
+ de {filteredProducts.length}
+
+
+
+ Selecionar tudo
+
+
+
+ Limpar
+
+
+
+ Remover ({selectedIds.size})
+
+ }
+ title="Remover selecionados?"
+ description={`Esta ação irá remover ${selectedIds.size} ${selectedIds.size === 1 ? 'item' : 'itens'}.`}
+ onConfirm={handleRemoveSelected}
+ itemName="itens selecionados"
/>
- ) : viewMode === "list" ? (
-
- {filteredProducts.map((product) => {
- const isSelected = selectedIds.has(product.id);
- return (
-
sel.toggleSelect(product.id) : undefined}
- >
-
-
navigate(`/produto/${product.id}`)}
- isFavorited={isFavorite(product.id)}
- onToggleFavorite={handleToggleFavorite}
- />
-
- {selectionMode && (
-
+
+ )}
+
+ {filteredProducts.length > 0 ? (
+ viewMode === 'table' ? (
+
navigate(`/produto/${productId}`)}
+ isFavorite={isFavorite}
+ onToggleFavorite={handleToggleFavorite}
+ selectionMode={selectionMode}
+ selectedIds={selectedIds}
+ onToggleSelect={sel.toggleSelect}
+ />
+ ) : viewMode === 'list' ? (
+
+ {filteredProducts.map((product) => {
+ const isSelected = selectedIds.has(product.id);
+ return (
+
sel.toggleSelect(product.id) : undefined}
+ >
+
+
navigate(`/produto/${product.id}`)}
+ isFavorited={isFavorite(product.id)}
+ onToggleFavorite={handleToggleFavorite}
+ />
+
+ {selectionMode && (
+
+
+ {isSelected && (
+
+ )}
- )}
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
+ {filteredProducts.map((product, index) => {
+ const variant = variantMap.get(product.id);
+ const isSelected = selectedIds.has(product.id);
+ const noteMeta = noteMap.get(product.id);
+ const priceMeta = enrichedMetaMap.get(product.id);
+ return (
+
sel.toggleSelect(product.id) : undefined}
+ >
+
+
navigate(`/produto/${product.id}`)}
+ onFavorite={() => handleRemoveFavorite(product.id, product.name)}
+ />
- );
- })}
-
- ) : (
-
- {filteredProducts.map((product, index) => {
- const variant = variantMap.get(product.id);
- const isSelected = selectedIds.has(product.id);
- const noteMeta = noteMap.get(product.id);
- const priceMeta = enrichedMetaMap.get(product.id);
- return (
-
sel.toggleSelect(product.id) : undefined}
- >
-
-
navigate(`/produto/${product.id}`)}
- onFavorite={() => handleRemoveFavorite(product.id, product.name)}
+ {isRemoteListView && priceMeta && !selectionMode && (
+
- {isRemoteListView && priceMeta && !selectionMode && (
-
- )}
- {selectionMode && (
-
- )}
- {!selectionMode && (
-
-
{ e.stopPropagation(); handleRemoveFavorite(product.id, product.name); }}
- >
-
-
- {isRemoteListView && noteMeta && (
-
handleSaveNote(product.id, note)}
- />
+ )}
+ {selectionMode && (
+
+
- {variant.color_hex && (
-
- )}
- {variant.color_name}
-
+ >
+ {isSelected && (
+
)}
- )}
-
- );
- })}
-
- )
- ) : productsWithVariant.length > 0 && searchQuery ? (
-
-
-
- Nenhum favorito encontrado
-
-
- Nenhum produto corresponde a "{searchQuery}"
-
+
+ )}
+ {!selectionMode && (
+
+ {
+ e.stopPropagation();
+ handleRemoveFavorite(product.id, product.name);
+ }}
+ >
+
+
+ {isRemoteListView && noteMeta && (
+ handleSaveNote(product.id, note)}
+ />
+ )}
+ {variant?.color_name && (
+
+ {variant.color_hex && (
+
+ )}
+
+ {variant.color_name}
+
+
+ )}
+
+ )}
+
+ );
+ })}
- ) : (
-
{
- if (isRemoteListView && activeList) {
- toast.info("Abra o produto e use o coração para adicionar a esta lista");
- navigate(`/produto/${productId}`);
- } else {
- navigate(`/produto/${productId}`);
- }
- }}
- />
- )}
- >
- )}
-
+ )
+ ) : productsWithVariant.length > 0 && searchQuery ? (
+
+
+
+ Nenhum favorito encontrado
+
+
+ Nenhum produto corresponde a "{searchQuery}"
+
+
+ ) : (
+
{
+ if (isRemoteListView && activeList) {
+ toast.info('Abra o produto e use o coração para adicionar a esta lista');
+ navigate(`/produto/${productId}`);
+ } else {
+ navigate(`/produto/${productId}`);
+ }
+ }}
+ />
+ )}
+ >
+ )}
-
-
-
-
- {ariaAnnouncement}
-
-
- {presenting && (
- setPresenting(false)}
- />
- )}
- >
+
+
+
+
+
+ {ariaAnnouncement}
+
+
+ {presenting && (
+
setPresenting(false)}
+ />
+ )}
+ >
);
}
diff --git a/tests/lib/theme-presets.test.ts b/tests/lib/theme-presets.test.ts
index 33d463a66..41c22499a 100644
--- a/tests/lib/theme-presets.test.ts
+++ b/tests/lib/theme-presets.test.ts
@@ -34,17 +34,20 @@ import {
const STORAGE_KEY = 'gifts-store-theme-config';
-// HSL canônico do Zapp Web. Manter sincronizado quando refazer port.
+// HSL canônico — os valores L abaixo foram REDUZIDOS em relação ao Zapp Web
+// original para conformidade WCAG (contraste AA com texto branco). Veja
+// comentários `// Reduzido de XX para YY para contraste WCAG` em
+// src/lib/theme-presets.ts. Manter sincronizado se o catálogo de skins mudar.
const ZAPP_GX_HSL: Record = {
'gx-classic': { h: 347, s: 96, l: 54, gh: 340 },
- 'gx-pink-addiction': { h: 330, s: 95, l: 60, gh: 340 },
+ 'gx-pink-addiction': { h: 330, s: 95, l: 50, gh: 340 },
'gx-purple-haze': { h: 265, s: 65, l: 50, gh: 275 },
- 'gx-rose-quartz': { h: 345, s: 75, l: 68, gh: 355 },
+ 'gx-rose-quartz': { h: 345, s: 75, l: 54, gh: 355 },
'gx-ultraviolet': { h: 271, s: 76, l: 53, gh: 280 },
- 'gx-hackerman': { h: 127, s: 65, l: 46, gh: 135 },
- 'gx-frutti-di-mare': { h: 182, s: 90, l: 42, gh: 190 },
+ 'gx-hackerman': { h: 127, s: 65, l: 40, gh: 135 },
+ 'gx-frutti-di-mare': { h: 182, s: 90, l: 35, gh: 190 },
'gx-cyberpunk': { h: 55, s: 100, l: 51, gh: 180 },
- 'gx-razer': { h: 113, s: 70, l: 51, gh: 120 },
+ 'gx-razer': { h: 113, s: 70, l: 35, gh: 120 },
};
const CLASSIC_IDS = [
@@ -188,7 +191,7 @@ describe('§3 Skins Opera GX (paridade Zapp Web)', () => {
});
it('gx-hackerman é verde Matrix (h=127)', () => {
- expect(findPreset('gx-hackerman').dark.primary).toBe('127 65% 46%');
+ expect(findPreset('gx-hackerman').dark.primary).toBe('127 65% 40%');
});
it('gx-cyberpunk é amarelo neon (h=55)', () => {
@@ -604,8 +607,8 @@ describe('§11 Fluxo: reload da página com skin GX salva (ThemeInitializer)', (
expect(document.documentElement.style.getPropertyValue('--font-sans')).toContain('Inter');
// Radius 10px (GX friendly)
expect(document.documentElement.style.getPropertyValue('--radius')).toBe('0.625rem');
- // Primary do Hackerman (h=127)
- expect(document.documentElement.style.getPropertyValue('--primary')).toBe('127 65% 46%');
+ // Primary do Hackerman (h=127) — L=40% pós-ajuste WCAG.
+ expect(document.documentElement.style.getPropertyValue('--primary')).toBe('127 65% 40%');
});
});
diff --git a/tests/lib/theme-radius-smoke.test.ts b/tests/lib/theme-radius-smoke.test.ts
index 49dd9de02..3a9cc1dee 100644
--- a/tests/lib/theme-radius-smoke.test.ts
+++ b/tests/lib/theme-radius-smoke.test.ts
@@ -97,7 +97,7 @@ describe('Smoke E2E — fluxo completo: corporate → GX → corporate', () => {
applyRadius(cfg.radius);
expect(radiusPx()).toBe(10);
expect(document.documentElement.style.getPropertyValue('--font-sans')).toContain('Inter');
- expect(document.documentElement.style.getPropertyValue('--primary')).toBe('127 65% 46%');
+ expect(document.documentElement.style.getPropertyValue('--primary')).toBe('127 65% 40%');
// 3. Volta para Corporate
cfg = { presetId: 'corporate', radius: 14, mode: 'auto' };
diff --git a/tests/unit/syntax-integrity.test.tsx b/tests/unit/syntax-integrity.test.tsx
index 4c6853c0c..b2c6e8e63 100644
--- a/tests/unit/syntax-integrity.test.tsx
+++ b/tests/unit/syntax-integrity.test.tsx
@@ -13,6 +13,25 @@ import { SellerCartProvider } from "@/contexts/SellerCartContext";
import { OrganizationProvider } from "@/contexts/OrganizationContext";
import { AriaLiveProvider } from "@/components/a11y";
+// Mock OrganizationContext para evitar fetchOrganizations real (que dispara
+// queries internas que travam o jsdom em CI). Usa React.createElement em
+// vez de JSX para evitar problemas de hoisting do vi.mock.
+vi.mock('@/contexts/OrganizationContext', async () => {
+ const ReactMod = await import('react');
+ return {
+ OrganizationProvider: ({ children }: { children: React.ReactNode }) =>
+ ReactMod.createElement(ReactMod.Fragment, null, children),
+ useOrganization: () => ({
+ organizations: [],
+ currentOrg: null,
+ currentRole: null,
+ isLoading: false,
+ switchOrganization: vi.fn(),
+ createOrganization: vi.fn(),
+ }),
+ };
+});
+
// Mock das dependências que poderiam causar efeitos colaterais ou erros de contexto
vi.mock("@/integrations/supabase/client", () => ({