Skip to content

refactor(compare): Etapa 13 — unify compare folder to product-catalog runtime type (−64 TSC errors)#149

Merged
adm01-debug merged 7 commits into
mainfrom
refactor/tsc-baseline-etapa-13-compare-table-view
May 24, 2026
Merged

refactor(compare): Etapa 13 — unify compare folder to product-catalog runtime type (−64 TSC errors)#149
adm01-debug merged 7 commits into
mainfrom
refactor/tsc-baseline-etapa-13-compare-table-view

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 23, 2026

Closes Etapa 13 of the 20-step audit plan (PR #124). Escopo expandiu de 1 arquivo (CompareTableView.tsx, 26 erros) para 13 arquivos (compare folder inteira) por causa de uma descoberta arquitetural: o repo tem dois tipos Product distintos coexistindo, e toda a pasta src/components/compare/ estava importando do tipo errado.

TL;DR

  • −64 erros TSC (baseline 1333 → 1189, zero regressões)
  • 16 arquivos modificados: 13 source files + .tsc-baseline.json regenerado + STATUS.md atualizado + 1 doc novo
  • Zero impacto runtime — apenas troca de imports + remoção de escape hatches Record<string, unknown>
  • TSC gate: ✅ PASS · ESLint gate: ✅ (drift pré-existente em arquivo não relacionado) · Vite build: ✅ 1m36s

Descoberta arquitetural

O repo tem dois arquivos de tipos Product:

Arquivo Propósito Estilo
src/types/product.ts DB-oriented (modelo do Postgres) snake_case (is_kit, min_quantity), maioria null-able, sem objetos aninhados
src/types/product-catalog.ts Runtime/UI (após mapPromobrindToProduct) camelCase (isKit, minQuantity), objetos aninhados, não-nullable

O runtime data que flui pela aplicação é product-catalog.Product (origem: useProducts()). ProductsContext, useSupplierComparison etc já usavam o tipo certo. Mas toda a pasta src/components/compare/ (7 arquivos) + src/pages/products/ComparePage.tsx importavam de src/types/product.ts (DB type). Por structural typing, o app rodava normal — mas TSC reclamava em todo acesso a camelCase / objeto aninhado.

A doc do plano original hipotetizou refactor renomeando camelCasesnake_case + null-safety patches. Resultado empírico: o código já estava correto para o runtime, apenas os imports estavam errados.

Mudanças por arquivo

Arquivo Tipo de mudança TSC (antes → depois)
CompareTableView.tsx import switch + drop 2 campos JSX inexistentes (category.icon, supplier.verified) + cleanup ShieldCheck órfão 26 → 0
StockRiskBadge.tsx Record<string, unknown>Product 0 → 0 (preserva)
OtherSuppliersRow.tsx Record<string, unknown>Product, remove as any 0 → 0 (preserva)
ComparisonScoreCard.tsx Record<string, unknown>Product 2 → 0
ExportComparisonButton.tsx Record<string, unknown>Product 2 → 0
SimilarProductsRail.tsx Record<string, unknown>Product 5 → 4¹
ComparisonPresentationLauncher.tsx import switch 9 → 5²
ComparisonMobileView.tsx import switch 5 → 0
ComparisonDuelView.tsx import switch + drive-by lint (remove Minus import órfão) 8 → 3³
ComparisonRadarChart.tsx import switch 2 → 0
AIComparisonAdvisor.tsx import switch 5 → 1⁴
FloatingCompareBar.tsx import switch + drive-by lint (ref_ref) 3 → 0
ComparePage.tsx import switch 10 → 0
Total source 13 arquivos 77 → 13 (−64)

Drive-by lint fix adicional: ExportComparisonButton: formatCurrency_formatCurrency (arg unused — lint error pré-existente no main).

¹ SimilarProductsRail: 4 residuais são pré-existentes — inferência do TanStack Query em useProducts(). Fora de escopo.
² PresentationLauncher: 5 residuais — ProductScore.items typo + implicit any em .reduce(). Bugs separados.
³ DuelView: 3 residuais — p.leadTimeDays inexistente em ambos os Product types. Bug real, requer leadTimeProxy(stockStatus).
⁴ AIAdvisor: 1 residual — query result tipado como {}. Bug separado.

Drive-by lint fixes (errors pré-existentes no main)

Husky pre-commit rodou eslint --fix nos arquivos tocados e revelou 3 erros pré-existentes que não passaram pelo CI anterior:

  • ComparisonDuelView: Minus import nunca usado
  • ExportComparisonButton: arg formatCurrency declarado mas nunca usado
  • FloatingCompareBar: arg ref declarado mas nunca usado em forwardRef

Os 3 foram corrigidos como parte desta PR (renomeando para _unused quando aplicável).

Implicações para o plano de 20 etapas

A doc original supôs que as Etapas 9-13 eram "refactor camelCase ↔ snake_case" (~3-4h cada). Na prática, Etapa 13 levou ~1h.

Antes de atacar Etapas 10-12 (AddressTab.tsx, BasicDataTab.tsx, AdminProductFormPage.tsx):

grep "from '@/types/product'" src/components/admin/products/new-supplier/tabs/
grep "from '@/types/product'" src/pages/admin/AdminProductFormPage.tsx

Se for o mesmo padrão de import errado, custo cai de ~3h → ~30min por etapa.

A Etapa 9 (price-response.adapter.ts) é arquiteturalmente diferente (adapter, não componente UI) — trate separadamente.

Sugestão de Etapa 13.5

Unificar os dois tipos Product. A coexistência é uma armadilha que tropeça em todo refactor. Possível abordagem: deletar src/types/product.ts, redirecionar os ~30 consumers para product-catalog, gerar src/types/product-db.ts via Supabase typegen para uso restrito ao mapper.

Validação

$ npm run typecheck
TS baseline gate — atual: 1189 erros · baseline: 1189 erros
✅ Nenhuma regressão de TypeScript detectada.

$ npm run lint:baseline
# (1 drift pré-existente em AuthBranding.visual.test.tsx — não relacionado)

$ npm run build
✓ built in 1m 36s

Doc

Detalhes completos em docs/redeploy/REDEPLOY-ETAPA-13-COMPARE-FOLDER.md.

Branch composition

5 commits sequenciais (serão squashed no merge):

  • 6910419 - part 1/4: small files
  • 8a9edc4 - part 2/5: escape-hatch removal in Score/Export + Mobile/Similar
  • 697f4e9 - part 3/5: Duel/AIAdvisor/FloatingBar/Presentation
  • c821377 - docs: STATUS + REDEPLOY-ETAPA-13-COMPARE-FOLDER
  • a410a03 - part 5/5: CompareTableView + ComparePage + baseline regen

Summary by cubic

Unified the compare folder to use the runtime @/types/product-catalog.Product, replacing escape-hatch types and fixing camelCase vs snake_case mismatches. Drops 64 TypeScript errors with no runtime changes.

  • Refactors
    • Switched all compare components and ComparePage to @/types/product-catalog.
    • Replaced Record<string, unknown> with Product in ExportComparisonButton, ComparisonScoreCard, OtherSuppliersRow, StockRiskBadge, and SimilarProductsRail.
    • Cleaned CompareTableView (removed invalid fields) and pruned unused imports/args across files.
    • Updated STATUS.md and added docs/redeploy/REDEPLOY-ETAPA-13-COMPARE-FOLDER.md.

Written for commit f9815a5. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Corrigidos erros de tipagem TypeScript na ferramenta de comparação de produtos (redução total de 1.060 para 949 erros reportados)
    • Melhorado tratamento de erros em recursos críticos com mensagens mais consistentes
  • Style

    • Interface da comparação refinada em layouts desktop e mobile
    • Estilização aprimorada de cartões de recomendação e visualizações
  • Refactor

    • Melhorada organização interna do código para maior clareza e manutenibilidade

Review Change Stack

Copilot AI review requested due to automatic review settings May 23, 2026 15:19
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Error Error May 24, 2026 3:31pm

@supabase
Copy link
Copy Markdown

supabase Bot commented May 23, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Walkthrough

Correção de erros TypeScript na seção de comparação de produtos pela importação do tipo correto Product, refatoração visual de componentes de comparação com implementação de leadTimeProxy derivado de stockStatus, ajustes em exportação de PDFs com paginação, e regeneração do baseline TypeScript (redução de 1060 para 949 erros).

Changes

Correção de Tipagem e Refatoração de Compare Folder

Layer / File(s) Summary
Importação e tipagem de Product nos componentes
src/components/compare/ComparisonScoreCard.tsx, src/components/compare/ExportComparisonButton.tsx, src/components/compare/SimilarProductsRail.tsx
Importação de Product de @/types/product-catalog e alteração de Props: products: Record<string, unknown>[]products: Product[] em três componentes que consomem listas de produtos.
Refatoração de CompareTableView com novos helpers de Lead time
src/components/compare/CompareTableView.tsx
Introdução de leadTimeProxy e leadTimeLabel para derivar Lead time a partir de Product['stockStatus'], expansão de subcomponentes SimpleRow e HighlightedNumberRow com JSX explícito, reorganização de renderização de cabeçalho/célula de produto incluindo suporte a variantes de cor via hoveredVariant e navegação ao clique.
Refatoração de ComparisonDuelView, Mobile e Presentation Launcher
src/components/compare/ComparisonDuelView.tsx, src/components/compare/ComparisonMobileView.tsx, src/components/compare/ComparisonPresentationLauncher.tsx
Implementação de leadTimeProxy/leadTimeLabel em DuelView, mudança em PresentationLauncher de scoreItems[].score para .total, reorganização visual de slides/componentes internos (ProductSlide, FinalSlide, Stat) com novas classes Tailwind, ajustes de paginação e navegação por teclado.
Ajustes em componentes auxiliares e exportação
src/components/compare/AIComparisonAdvisor.tsx, src/components/compare/ComparisonRadarChart.tsx, src/components/compare/ExportComparisonButton.tsx, src/components/compare/FloatingCompareBar.tsx, src/components/compare/OtherSuppliersRow.tsx, src/components/compare/StockRiskBadge.tsx
AIComparisonAdvisor: cache determinístico via map().sort().join(), tratamento de erro com instanceof, UI remodelada; ExportComparisonButton: imports dinâmicos (Promise.all) para PDF/PNG, paginação PDF A4 paisagem com header, mensagens PT-BR; demais componentes: reformatação de código (aspas, className, JSX).
ComparePage early-return guard e atualização de baseline
src/pages/products/ComparePage.tsx, .tsc-baseline.json
ComparePage: early return quando compareCount < 2 com CompareEmptyStateSmart, ajustes de layout no path principal; .tsc-baseline.json: regeneração com redução 1060 → 949 erros totais, remoção/ajuste de entradas em componentes compare e páginas correlatas.
Documentação da Etapa 13
STATUS.md, docs/redeploy/REDEPLOY-ETAPA-13-COMPARE-FOLDER.md
STATUS.md: lição sobre import incorreto entre tipos Product distintos; REDEPLOY-ETAPA-13: relatório completo descrevendo divergência @/types/product vs @/types/product-catalog, sintomas de incompatibilidade, eliminação de Record<string, unknown>, impacto por arquivo com contagens antes/depois, validação (typecheck/lint/build), e próximos passos (unificação de tipos, limpeza de residuais).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

A PR aborda refatoração heterogênea de múltiplos componentes interdependentes no folder compare/: alterações de tipo em props, implementação de novos helpers (leadTimeProxy), remodelagem visual em 6+ componentes, imports dinâmicos com Promise.all, paginação PDF, e ajustes de baseline. Cada componente requer verificação separada de lógica de renderização, dependências e compatibilidade com o novo tipo Product.

Possibly related PRs

  • adm01-debug/promo-gifts-v4#174: Altera diretamente src/components/compare/CompareTableView.tsx com leadTimeProxy/leadTimeLabel e derivação de Lead time a partir de stockStatus.
  • adm01-debug/promo-gifts-v4#177: Refatora CompareTableView.tsx e ajusta tipagens/consumo de produtos em componentes do folder compare (StockRiskBadge, OtherSuppliersRow).
  • adm01-debug/promo-gifts-v4#168: Modifica src/components/compare/ComparisonScoreCard.tsx com ajustes no tipo/contrato de products e lógica de renderização do "winner".
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed O título descreve claramente a mudança principal: unificar a pasta compare para usar o tipo runtime product-catalog, com impacto quantificado (−64 erros TSC). Está relacionado ao conteúdo do PR e é específico.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/tsc-baseline-etapa-13-compare-table-view

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the compare UI to use the runtime product catalog type instead of the DB-oriented product type, reducing TypeScript baseline errors and documenting the architectural finding around duplicate Product types.

Changes:

  • Swaps compare-page/component imports from @/types/product or loose Record<string, unknown> props to @/types/product-catalog.
  • Reformats/tightens compare UI components and removes a few unused/invalid JSX references.
  • Regenerates .tsc-baseline.json and updates operational documentation/status for Etapa 13.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.tsc-baseline.json Updates TypeScript baseline counts after compare-folder cleanup.
STATUS.md Marks Etapa 13 complete and updates remaining audit-plan estimates.
docs/redeploy/REDEPLOY-ETAPA-13-COMPARE-FOLDER.md Documents the compare-folder refactor and Product type discovery.
src/pages/products/ComparePage.tsx Switches compare page to product-catalog types and reformats JSX.
src/components/compare/AIComparisonAdvisor.tsx Switches AI advisor product prop type to product-catalog Product.
src/components/compare/CompareTableView.tsx Switches table view to product-catalog Product and removes invalid category/supplier fields.
src/components/compare/ComparisonDuelView.tsx Switches duel view to product-catalog Product and removes unused import.
src/components/compare/ComparisonMobileView.tsx Switches mobile comparison view to product-catalog Product/ProductColor.
src/components/compare/ComparisonPresentationLauncher.tsx Switches presentation launcher to product-catalog Product.
src/components/compare/ComparisonRadarChart.tsx Switches radar chart to product-catalog Product.
src/components/compare/ComparisonScoreCard.tsx Replaces loose product records with product-catalog Product props.
src/components/compare/ExportComparisonButton.tsx Replaces loose product records with product-catalog Product props.
src/components/compare/FloatingCompareBar.tsx Switches floating compare bar to product-catalog Product.
src/components/compare/OtherSuppliersRow.tsx Replaces loose product record and as any with product-catalog Product.
src/components/compare/SimilarProductsRail.tsx Replaces loose product records with product-catalog Product props.
src/components/compare/StockRiskBadge.tsx Replaces loose product record with product-catalog Product.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +28
const { items: scoreItems = [] } = useComparisonScore(products) || { items: [] };
const winnerIdx = (scoreItems && scoreItems.length > 0)
? scoreItems.reduce((best, cur, idx, arr) => cur.score > arr[best].score ? idx : best, 0)
: -1;
const winnerIdx =
scoreItems && scoreItems.length > 0
? scoreItems.reduce((best, cur, idx, arr) => (cur.score > arr[best].score ? idx : best), 0)
: -1;
Comment on lines +55 to +60
{
key: 'leadTime',
label: 'Lead time (dias)',
format: (p) => (p.leadTimeDays ? `${p.leadTimeDays}d` : '—'),
raw: (p) => p.leadTimeDays ?? 999,
better: 'lower',
Comment on lines +25 to +27
| `src/types/product-catalog.ts` | Runtime/UI (modelo após `mapPromobrindToProduct`) | camelCase (`isKit`, `minQuantity`, `stockStatus`), objetos aninhados (`category: {id, name}`, `supplier: {id, name}`, `tags: {publicoAlvo, datasComemorativas, ...}`), não-nullable |

O *runtime data* que flui pela aplicação é `product-catalog.Product` (origem: `useProducts()` que internamente chama `mapPromobrindToProduct(rawDB) -> product-catalog.Product`). `ProductsContext`, `useProducts` e `useSupplierComparison` já usam `product-catalog.Product`.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 16 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Copy link
Copy Markdown
Owner Author

@adm01-debug adm01-debug left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provavelmente superado. A unificação do subsistema de comparação ao tipo Product de product-catalog foi entregue em main via #197 (−24 erros baseline; migra ComparePage + AIComparisonAdvisor, ComparisonDuelView, ComparisonMobileView, ComparisonPresentationLauncher, ComparisonRadarChart, FloatingCompareBar). O CompareTableView (Etapa 13) também já está em main via #174 + #185 (campos camelCase em runtime). Recomendo conferir o diff deste PR contra o main atual e fechar se estiver contido. O #159 (que aponta para este branch) fica moot junto.

@adm01-debug adm01-debug force-pushed the refactor/tsc-baseline-etapa-13-compare-table-view branch from a410a03 to e8c16b9 Compare May 24, 2026 13:39
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/compare/CompareTableView.tsx`:
- Around line 181-185: Remove the keyboard-inaccessibility by ensuring the
remove button becomes visible on keyboard focus: modify the button in
CompareTableView.tsx (the element that calls onRemove(entry.index)) to add focus
and focus-visible visibility classes (e.g., add "focus:opacity-100
focus-visible:opacity-100" to the existing className) and include a visible
focus indicator (e.g., "focus:outline-none focus-visible:ring" or equivalent
design-system focus classes) so it appears when tabbed to while preserving the
hover behavior.

In `@src/components/compare/ExportComparisonButton.tsx`:
- Around line 30-42: O CSV permite que células com prefixos "=", "+", "-", "@"
sejam interpretadas como fórmulas; add uma sanitização antes de serializar:
implemente uma helper chamada sanitizeCsvCell(value) e use-a na construção de
rows/csv (onde rows, headers e csv são montados em ExportComparisonButton.tsx)
para normalizar null/undefined para string vazia, escapar aspas internas e, se o
valor string começar com one of ['=', '+', '-', '@'], prefixá-lo com uma aspas
simples (') ou outro caractere seguro antes de envolver em aspas CSV; aplique
sanitizeCsvCell dentro do .map((v) => ...) que constrói cada célula para
garantir que todas as células exportadas sejam seguras.
- Around line 63-77: The busy flag is cleared in the finally block before the
async canvas.toBlob callback completes, allowing concurrent exports; update
ExportComparisonButton to only call setBusy(false) after the toBlob flow
finishes (success or failure) by moving the setBusy(false) into the toBlob
callback paths and also ensure it's called when blob is null or when
URL.createObjectURL / a.click / revokeObjectURL could fail; keep the try/catch
to log exceptions, but remove the finally block clearing busy and instead clear
busy inside the callback (and inside catch) so busy accurately reflects the
active export operation.

In `@src/components/compare/FloatingCompareBar.tsx`:
- Around line 91-102: The remove button in FloatingCompareBar (the <button> with
onClick calling removeByIndex and containing the <X> icon) is missing an
accessible name; add an aria-label attribute (e.g. aria-label="Remover item da
comparação") to that button so screen readers convey the action clearly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a1fe4bb0-7b21-4b8b-b2c8-a1ad69f855de

📥 Commits

Reviewing files that changed from the base of the PR and between 7bf38f5 and e8c16b9.

📒 Files selected for processing (16)
  • .tsc-baseline.json
  • STATUS.md
  • docs/redeploy/REDEPLOY-ETAPA-13-COMPARE-FOLDER.md
  • src/components/compare/AIComparisonAdvisor.tsx
  • src/components/compare/CompareTableView.tsx
  • src/components/compare/ComparisonDuelView.tsx
  • src/components/compare/ComparisonMobileView.tsx
  • src/components/compare/ComparisonPresentationLauncher.tsx
  • src/components/compare/ComparisonRadarChart.tsx
  • src/components/compare/ComparisonScoreCard.tsx
  • src/components/compare/ExportComparisonButton.tsx
  • src/components/compare/FloatingCompareBar.tsx
  • src/components/compare/OtherSuppliersRow.tsx
  • src/components/compare/SimilarProductsRail.tsx
  • src/components/compare/StockRiskBadge.tsx
  • src/pages/products/ComparePage.tsx

Comment on lines +181 to +185
<button
aria-label="Remover da comparação"
onClick={() => onRemove(entry.index)}
className="absolute -right-1 -top-1 z-10 rounded-full border border-border bg-background p-1 opacity-0 transition-opacity hover:bg-destructive/20 group-hover:opacity-100"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Botão de remover pode ficar invisível ao navegar por teclado

Com opacity-0 + group-hover:opacity-100, o controle pode não ficar perceptível no foco via teclado, o que quebra a ação para parte dos usuários.

💡 Patch sugerido
- className="absolute -right-1 -top-1 z-10 rounded-full border border-border bg-background p-1 opacity-0 transition-opacity hover:bg-destructive/20 group-hover:opacity-100"
+ className="absolute -right-1 -top-1 z-10 rounded-full border border-border bg-background p-1 opacity-0 transition-opacity hover:bg-destructive/20 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
aria-label="Remover da comparação"
onClick={() => onRemove(entry.index)}
className="absolute -right-1 -top-1 z-10 rounded-full border border-border bg-background p-1 opacity-0 transition-opacity hover:bg-destructive/20 group-hover:opacity-100"
>
<button
aria-label="Remover da comparação"
onClick={() => onRemove(entry.index)}
className="absolute -right-1 -top-1 z-10 rounded-full border border-border bg-background p-1 opacity-0 transition-opacity hover:bg-destructive/20 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/compare/CompareTableView.tsx` around lines 181 - 185, Remove
the keyboard-inaccessibility by ensuring the remove button becomes visible on
keyboard focus: modify the button in CompareTableView.tsx (the element that
calls onRemove(entry.index)) to add focus and focus-visible visibility classes
(e.g., add "focus:opacity-100 focus-visible:opacity-100" to the existing
className) and include a visible focus indicator (e.g., "focus:outline-none
focus-visible:ring" or equivalent design-system focus classes) so it appears
when tabbed to while preserving the hover behavior.

Comment on lines +30 to +42
const headers = ['SKU', 'Nome', 'Preço', 'Qtd. mín.', 'Estoque', 'Cores', 'Categoria'];
const rows = products.map((p) => [
p.sku ?? '',
p.name,
p.price,
p.minQuantity,
p.stock,
(p.colors?.length ?? 0),
p.category?.name ?? "",
p.colors?.length ?? 0,
p.category?.name ?? '',
]);
const csv = [headers, ...rows].map(r =>
r.map(v => `"${String(v ?? "").replace(/"/g, '""')}"`).join(",")
).join("\n");
const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" });
const csv = [headers, ...rows]
.map((r) => r.map((v) => `"${String(v ?? '').replace(/"/g, '""')}"`).join(','))
.join('\n');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize de célula CSV para bloquear fórmula maliciosa

Os valores exportados ainda permitem prefixos =, +, - e @. Ao abrir no Excel/Sheets, isso pode virar execução de fórmula (CSV injection).

💡 Patch sugerido
+  const escapeCsvCell = (value: unknown) => {
+    const raw = String(value ?? '');
+    const neutralized = /^[=+\-@]/.test(raw) ? `'${raw}` : raw;
+    return `"${neutralized.replace(/"/g, '""')}"`;
+  };
+
   const exportCSV = () => {
     const headers = ['SKU', 'Nome', 'Preço', 'Qtd. mín.', 'Estoque', 'Cores', 'Categoria'];
     const rows = products.map((p) => [
       p.sku ?? '',
       p.name,
       p.price,
       p.minQuantity,
       p.stock,
       p.colors?.length ?? 0,
       p.category?.name ?? '',
     ]);
     const csv = [headers, ...rows]
-      .map((r) => r.map((v) => `"${String(v ?? '').replace(/"/g, '""')}"`).join(','))
+      .map((r) => r.map(escapeCsvCell).join(','))
       .join('\n');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/compare/ExportComparisonButton.tsx` around lines 30 - 42, O
CSV permite que células com prefixos "=", "+", "-", "@" sejam interpretadas como
fórmulas; add uma sanitização antes de serializar: implemente uma helper chamada
sanitizeCsvCell(value) e use-a na construção de rows/csv (onde rows, headers e
csv são montados em ExportComparisonButton.tsx) para normalizar null/undefined
para string vazia, escapar aspas internas e, se o valor string começar com one
of ['=', '+', '-', '@'], prefixá-lo com uma aspas simples (') ou outro caractere
seguro antes de envolver em aspas CSV; aplique sanitizeCsvCell dentro do
.map((v) => ...) que constrói cada célula para garantir que todas as células
exportadas sejam seguras.

Comment on lines 63 to 77
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const a = document.createElement('a');
a.href = url;
a.download = `comparacao-${new Date().toISOString().slice(0, 10)}.png`;
a.click();
URL.revokeObjectURL(url);
toast.success("PNG exportado");
toast.success('PNG exportado');
});
} catch (e) {
console.error(e);
toast.error("Falha ao exportar PNG");
toast.error('Falha ao exportar PNG');
} finally {
setBusy(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Estado busy é liberado antes do PNG terminar

setBusy(false) no finally roda antes da callback de canvas.toBlob, então o usuário pode clicar de novo e disparar exportações concorrentes.

💡 Patch sugerido
   const exportPNG = async () => {
     setBusy(true);
     try {
       const html2canvas = (await import('html2canvas')).default;
       const el = document.querySelector(targetSelector) as HTMLElement | null;
       if (!el) {
         toast.error('Área não encontrada');
         return;
       }
       const canvas = await html2canvas(el, { backgroundColor: '`#ffffff`', scale: 2, useCORS: true });
-      canvas.toBlob((blob) => {
-        if (!blob) return;
-        const url = URL.createObjectURL(blob);
-        const a = document.createElement('a');
-        a.href = url;
-        a.download = `comparacao-${new Date().toISOString().slice(0, 10)}.png`;
-        a.click();
-        URL.revokeObjectURL(url);
-        toast.success('PNG exportado');
-      });
+      const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve));
+      if (!blob) {
+        toast.error('Falha ao gerar imagem PNG');
+        return;
+      }
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `comparacao-${new Date().toISOString().slice(0, 10)}.png`;
+      a.click();
+      URL.revokeObjectURL(url);
+      toast.success('PNG exportado');
     } catch (e) {
       console.error(e);
       toast.error('Falha ao exportar PNG');
     } finally {
       setBusy(false);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const a = document.createElement('a');
a.href = url;
a.download = `comparacao-${new Date().toISOString().slice(0, 10)}.png`;
a.click();
URL.revokeObjectURL(url);
toast.success("PNG exportado");
toast.success('PNG exportado');
});
} catch (e) {
console.error(e);
toast.error("Falha ao exportar PNG");
toast.error('Falha ao exportar PNG');
} finally {
setBusy(false);
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve));
if (!blob) {
toast.error('Falha ao gerar imagem PNG');
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `comparacao-${new Date().toISOString().slice(0, 10)}.png`;
a.click();
URL.revokeObjectURL(url);
toast.success('PNG exportado');
} catch (e) {
console.error(e);
toast.error('Falha ao exportar PNG');
} finally {
setBusy(false);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/compare/ExportComparisonButton.tsx` around lines 63 - 77, The
busy flag is cleared in the finally block before the async canvas.toBlob
callback completes, allowing concurrent exports; update ExportComparisonButton
to only call setBusy(false) after the toBlob flow finishes (success or failure)
by moving the setBusy(false) into the toBlob callback paths and also ensure it's
called when blob is null or when URL.createObjectURL / a.click / revokeObjectURL
could fail; keep the try/catch to log exceptions, but remove the finally block
clearing busy and instead clear busy inside the callback (and inside catch) so
busy accurately reflects the active export operation.

Comment on lines +91 to +102
<button
onClick={() => removeByIndex(entry.index)}
className={cn(
'absolute -right-1.5 -top-1.5 h-5 w-5 rounded-full',
'bg-destructive text-destructive-foreground',
'flex items-center justify-center',
'opacity-0 transition-opacity group-hover:opacity-100',
'hover:bg-destructive/90',
)}
>
<X className="h-3 w-3" />
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Botão de remover item precisa de nome acessível.

Na Line 91, o botão com ícone X não tem aria-label. Para leitor de tela, a ação fica ambígua. Sugestão: aria-label="Remover item da comparação".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/compare/FloatingCompareBar.tsx` around lines 91 - 102, The
remove button in FloatingCompareBar (the <button> with onClick calling
removeByIndex and containing the <X> icon) is missing an accessible name; add an
aria-label attribute (e.g. aria-label="Remover item da comparação") to that
button so screen readers convey the action clearly.

@adm01-debug adm01-debug force-pushed the refactor/tsc-baseline-etapa-13-compare-table-view branch from e8c16b9 to 1447a00 Compare May 24, 2026 13:52
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@adm01-debug adm01-debug merged commit 9e0d715 into main May 24, 2026
21 of 34 checks passed
@adm01-debug adm01-debug deleted the refactor/tsc-baseline-etapa-13-compare-table-view branch May 24, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants