Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b5b3d0d
security: remove hardcoded simulation-bypass backdoor from edges and …
claude May 22, 2026
449e063
fix(hooks): stop calling useOnboardingContext inside try/catch (rules…
claude May 22, 2026
a058a58
fix(qa): batch 2 — AuthContext race conditions, useQuotes null-safety…
claude May 22, 2026
1e4b646
test(qa): batch 3 — destrava 24 testes (theme + stepper + integration…
claude May 22, 2026
38da998
fix(qa): batch 4 — sidebarOpen prop drilling, SupabaseConnectionsTab …
claude May 22, 2026
d4b55e8
refactor(qa): batch 5 — elimina 17 non-null assertions adicionais
claude May 22, 2026
68d89c7
test(qa): syntax-integrity — mocka OrganizationContext
claude May 22, 2026
aaae734
resolve(#99): scripts/massive-load-test.mjs — resolve conflito + BD c…
adm01-debug May 22, 2026
50ff1f6
resolve(#99): contract-testing.mjs — versao refatorada do main (PR #87)
adm01-debug May 22, 2026
3c96889
resolve(#99): auth.ts — versao main com constantTimeEqual (anti-timin…
adm01-debug May 22, 2026
b2cdb8f
resolve(#99): test-contract-orchestrator — versao main fail-closed (503)
adm01-debug May 22, 2026
4e803fe
resolve(#99): MainLayout.tsx — versao main (Header sem searchQuery)
adm01-debug May 22, 2026
e272ec3
refactor(qa): batch 6 — elimina 21 non-null assertions em personaliza…
claude May 22, 2026
6e0eafe
refactor(qa): batch 7 — useColorEnrichment elimina 5 non-null assertions
claude May 22, 2026
5566463
refactor(qa): batch 8 — useMagicUpGeneration + ProductClassificationS…
claude May 22, 2026
2ca34cc
refactor(qa): batch 9 — RegressionGuardrailBanner + MarginInsightBadge
claude May 22, 2026
c4ff565
resolve(#99): Header.tsx — manter versão main (props removidas, getRo…
adm01-debug May 22, 2026
d5fbbb4
fix(#99): corrige BD hardcoded errado no smoke test E2E
adm01-debug May 22, 2026
057ca90
refactor(qa): batch 10 — elimina 16 non-null assertions
claude May 22, 2026
0efc096
refactor(qa): batch 11 — elimina 19 non-null assertions em UI + services
claude May 22, 2026
c32931d
fix(merge): restaura Header.tsx que ficou base64-encoded após resolve…
claude May 22, 2026
5f011b6
refactor(qa): batch 12 — elimina 13 non-null assertions em widgets + …
claude May 22, 2026
58ff350
refactor(qa): batch 13 — elimina 13 non-null assertions em UI dialogs
claude May 22, 2026
49c2621
refactor(qa): batch 14 — elimina 10 non-null assertions em quotes + s…
claude May 22, 2026
43a2d71
refactor(qa): batch 15 — elimina 10 non-null assertions em hooks + lib
claude May 22, 2026
bd46003
refactor(qa): batch 16 — elimina 10 non-null assertions em pages
claude May 22, 2026
ca8c3c1
Merge main into PR 99
May 24, 2026
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
18 changes: 13 additions & 5 deletions src/components/kit-builder/kit-summary/KitCompositionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ export function KitCompositionCard({ kitState, kitQuantity, stockByProduct }: Ki
{item.material ? ` • ${item.material}` : ''}
{item.isOptional && <Badge variant="secondary" className="ml-1 text-[10px] px-1 py-0">Opcional</Badge>}
</p>
{stockByProduct.has(item.id) && (
<Badge variant={stockByProduct.get(item.id)! >= item.quantity * kitQuantity ? 'secondary' : 'destructive'} className="text-[10px] px-1.5 py-0">
{stockByProduct.get(item.id)! >= item.quantity * kitQuantity ? `${stockByProduct.get(item.id)} em estoque` : `⚠ ${stockByProduct.get(item.id)} disponível`}
</Badge>
)}
{(() => {
const stockQty = stockByProduct.get(item.id);
if (stockQty === undefined) return null;
const enough = stockQty >= item.quantity * kitQuantity;
return (
<Badge
variant={enough ? 'secondary' : 'destructive'}
className="text-[10px] px-1.5 py-0"
>
{enough ? `${stockQty} em estoque` : `⚠ ${stockQty} disponível`}
</Badge>
);
})()}
</div>
</div>
<div className="flex items-center gap-2">
Expand Down
56 changes: 36 additions & 20 deletions src/components/mockup/ProductSearchCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,24 @@ export function ProductSearchCombobox({
<div className="flex items-center gap-3 flex-1 min-w-0">
{/* Product thumbnail */}
<div className="flex-shrink-0 w-8 h-8 rounded-md bg-muted overflow-hidden">
{getProductImage(selectedProduct) ? (
<img
src={getProductImage(selectedProduct)!}
alt={selectedProduct.name}
className="w-full h-full object-cover" loading="lazy" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-4 w-4 text-muted-foreground" />
</div>
)}
{(() => {
const img = getProductImage(selectedProduct);
if (img) {
return (
<img
src={img}
alt={selectedProduct.name}
className="w-full h-full object-cover"
loading="lazy"
/>
);
}
return (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-4 w-4 text-muted-foreground" />
</div>
);
})()}
</div>

{/* Product info */}
Expand Down Expand Up @@ -208,16 +216,24 @@ export function ProductSearchCombobox({

{/* Product thumbnail */}
<div className="flex-shrink-0 w-10 h-10 rounded-md bg-muted overflow-hidden">
{getProductImage(product) ? (
<img
src={getProductImage(product)!}
alt={product.name}
className="w-full h-full object-cover" loading="lazy" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-5 w-5 text-muted-foreground" />
</div>
)}
{(() => {
const img = getProductImage(product);
if (img) {
return (
<img
src={img}
alt={product.name}
className="w-full h-full object-cover"
loading="lazy"
/>
);
}
return (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-5 w-5 text-muted-foreground" />
</div>
);
})()}
</div>

{/* Product info */}
Expand Down
39 changes: 25 additions & 14 deletions src/components/pdf/proposal/LogoWithTransparentBg.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState } from 'react';

// ── Module-level cache so the image is processed once and reused instantly ──
const logoCache = new Map<string, string>();
Expand All @@ -10,8 +10,10 @@ const logoPromises = new Map<string, Promise<string>>();
* Result is cached for the lifetime of the page.
*/
export function processLogoTransparent(src: string): Promise<string> {
if (logoCache.has(src)) return Promise.resolve(logoCache.get(src)!);
if (logoPromises.has(src)) return logoPromises.get(src)!;
const cached = logoCache.get(src);
if (cached) return Promise.resolve(cached);
const inflight = logoPromises.get(src);
if (inflight) return inflight;

const promise = fetch(src)
.then((res) => res.blob())
Expand All @@ -21,28 +23,36 @@ export function processLogoTransparent(src: string): Promise<string> {
const objectUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) { resolve(src); return; }
const ctx = canvas.getContext('2d');
if (!ctx) {
resolve(src);
return;
}
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2];
const r = d[i],
g = d[i + 1],
b = d[i + 2];
// Threshold 235 catches off-white anti-alias edges
if (r > 235 && g > 235 && b > 235) d[i + 3] = 0;
}
ctx.putImageData(imageData, 0, 0);
URL.revokeObjectURL(objectUrl);
const dataUrl = canvas.toDataURL("image/png");
const dataUrl = canvas.toDataURL('image/png');
logoCache.set(src, dataUrl);
resolve(dataUrl);
};
img.onerror = () => { URL.revokeObjectURL(objectUrl); resolve(src); };
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
resolve(src);
};
img.src = objectUrl;
})
}),
)
.catch(() => src);

Expand All @@ -58,17 +68,18 @@ interface Props {

export function LogoWithTransparentBg({ src, style, alt }: Props) {
// Immediately use cached result if available (no flash)
const [dataUrl, setDataUrl] = useState<string>(() => logoCache.get(src) ?? "");
const [dataUrl, setDataUrl] = useState<string>(() => logoCache.get(src) ?? '');

useEffect(() => {
if (logoCache.has(src)) {
setDataUrl(logoCache.get(src)!);
const cached = logoCache.get(src);
if (cached) {
setDataUrl(cached);
return;
}
processLogoTransparent(src).then(setDataUrl);
}, [src]);

if (!dataUrl) return <div style={{ ...style, opacity: 0 }} />;

return <img src={dataUrl} alt={alt ?? "Logo"} style={style} loading="lazy" />;
return <img src={dataUrl} alt={alt ?? 'Logo'} style={style} loading="lazy" />;
}
22 changes: 12 additions & 10 deletions src/components/quotes/QuoteBuilderNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { QuoteBuilderStep } from "./QuoteBuilderStepper";
import { Button } from '@/components/ui/button';
import { type QuoteBuilderStep } from './QuoteBuilderStepper';

interface QuoteBuilderNavigationProps {
currentStep: QuoteBuilderStep;
Expand All @@ -8,21 +8,23 @@ interface QuoteBuilderNavigationProps {
isLastStep: boolean;
}

export function QuoteBuilderNavigation({ currentStep, onNext, onPrev, isLastStep }: QuoteBuilderNavigationProps) {
export function QuoteBuilderNavigation({
currentStep,
onNext,
onPrev,
isLastStep,
}: QuoteBuilderNavigationProps) {
return (
<div className="fixed bottom-0 left-0 right-0 z-50 flex items-center justify-between border-t bg-background px-6 py-3 shadow-lg md:relative md:border-t-0 md:bg-transparent md:px-0 md:shadow-none">
<Button
variant="outline"
onClick={onPrev}
<Button
variant="outline"
onClick={onPrev}
disabled={currentStep === 'client'}
data-testid="wizard-prev-button"
>
Voltar
</Button>
<Button
onClick={onNext}
data-testid="wizard-next-button"
>
<Button onClick={onNext} data-testid="wizard-next-button">
{isLastStep ? 'Salvar Orçamento' : 'Próximo'}
</Button>
</div>
Expand Down
Loading
Loading