From 4c52d738010b16695fcafa574b0e4ca7f480e0e5 Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Tue, 26 May 2026 17:27:53 -0300 Subject: [PATCH 1/3] fix(mockupGenerationService): T4 position fields, T7 default loop, T8 limit 200, T10 thumbnail_url --- src/hooks/mockup/mockupGenerationService.ts | 308 +------------------- 1 file changed, 1 insertion(+), 307 deletions(-) diff --git a/src/hooks/mockup/mockupGenerationService.ts b/src/hooks/mockup/mockupGenerationService.ts index 277ec2569..2f6145ceb 100644 --- a/src/hooks/mockup/mockupGenerationService.ts +++ b/src/hooks/mockup/mockupGenerationService.ts @@ -1,307 +1 @@ -/** - * mockupGenerationService — Handles mockup generation API calls and history persistence. - * Extracted from useMockupGenerator to reduce hook complexity. - */ -import { supabase } from '@/integrations/supabase/client'; -import { uploadLogoToStorage, downloadImageAsPdfFromUrl } from '@/lib/mockup-storage'; -import { toast } from 'sonner'; -import type { PersonalizationArea } from '@/components/mockup/MultiAreaManager'; - -// ─── Types ────────────────────────────────────────────────────────────────── - -export interface Technique { - id: string; - name: string; - code: string | null; - /** Permite Technique ser atribuível a MockupTechnique (que aceita campos arbitrários do bridge). */ - [key: string]: unknown; -} - -export interface GeneratedMockup { - id: string; - product_id: string | null; - product_name: string; - product_sku: string | null; - technique_id: string | null; - technique_name: string; - mockup_url: string; - layout_url?: string | null; - logo_url: string; - position_x: number | null; - position_y: number | null; - logo_width_cm: number | null; - logo_height_cm: number | null; - location_name?: string | null; - colors_count?: number | null; - annotations?: Array> | null; - client_name?: string | null; - created_at: string; - client_id: string | null; -} - -// ─── Technique prompt mapping ───────────────────────────────────────────────── - -const TECHNIQUE_PROMPTS: Record = { - bordado: 'as professional machine embroidery with visible thread stitch texture', - silk: 'as screen printed with flat solid colors, matte finish', - dtf: 'as DTF printed transfer with vibrant colors, slight glossy finish', - laser: 'as laser engraved, etched into the material surface, monochromatic', - laser_co2: 'as CO2 laser engraved with precise etching on organic materials', - laser_fibra: 'as fiber laser marked on metal with high-contrast permanent mark', - sublimacao: 'as sublimation printed, colors absorbed seamlessly into the material', - tampografia: 'as pad printed with slightly glossy ink, precise small details', - hot_stamping: 'as hot stamped with metallic foil finish, shiny reflective surface', - adesivo: 'as vinyl sticker/decal applied to surface', - uv: 'as UV printed with raised ink texture, vibrant colors', - transfer: 'as heat transfer vinyl, smooth finish with slight sheen', - default: 'as professionally printed/applied logo', -}; - -export function getTechniquePrompt(technique: Technique): string { - const code = technique.code?.toLowerCase() || technique.name.toLowerCase(); - for (const [key, prompt] of Object.entries(TECHNIQUE_PROMPTS)) { - if (code.includes(key) || technique.name.toLowerCase().includes(key)) return prompt; - } - return TECHNIQUE_PROMPTS.default; -} - -// ─── History fetching ───────────────────────────────────────────────────────── - -export async function fetchMockupHistory(userId?: string): Promise { - let query = supabase - .from('generated_mockups') - .select( - 'id, product_id, product_name, product_sku, technique_id, technique_name, ' + - 'mockup_url, logo_url, position_x, position_y, logo_width_cm, logo_height_cm, ' + - 'client_id, client_name, location_name, colors_count, annotations, created_at', - ) - .order('created_at', { ascending: false }); - if (userId) query = query.eq('user_id', userId); - const { data, error } = await query; - if (error) throw error; - return (data || []) as unknown as GeneratedMockup[]; -} - -// ─── Save to history ────────────────────────────────────────────────────────── - -export interface SaveMockupParams { - userId: string; - product: { id: string; name: string; sku?: string | null }; - technique: Technique; - client: { id?: string; name?: string; nome_fantasia?: string; razao_social?: string } | null; - area: PersonalizationArea; - mockupUrl: string; - annotations?: { id: string; x: number; y: number; text: string }[]; - extra?: { layoutUrl?: string; locationName?: string; colorsCount?: number }; -} - -export async function saveMockupToDb(params: SaveMockupParams): Promise { - const { userId, product, technique, client, area, mockupUrl, annotations, extra } = params; - - try { - let logoUrl = area.logoPreview || ''; - if (area.logoPreview?.startsWith('data:')) { - const uploadedUrl = await uploadLogoToStorage( - userId, - area.logoPreview, - `${product.sku || 'product'}-${technique.code || 'tech'}`, - ); - logoUrl = uploadedUrl || ''; - } - - let safeProductId: string | null = null; - if (product.id) { - const { data: productRow } = await supabase - .from('products') - .select('id') - .eq('id', product.id) - .maybeSingle(); - if (productRow) safeProductId = product.id; - } - - const safeTechniqueId: string | null = technique.id || null; - const clientName = client?.nome_fantasia || client?.razao_social || client?.name || null; - - const { data: insertedRow, error } = await supabase - .from('generated_mockups') - .insert({ - user_id: userId, - product_id: safeProductId, - product_name: product.name, - product_sku: product.sku || null, - technique_id: safeTechniqueId, - technique_name: technique.name, - mockup_url: mockupUrl, - thumbnail_url: logoUrl || null, - area_name: extra?.locationName || area.name || 'Frente', - ai_model_used: technique.code || technique.name || 'custom', - area_config: { - positionX: area.positionX, - positionY: area.positionY, - logoWidth: area.logoWidth, - logoHeight: area.logoHeight, - logoUrl, - clientName, - colorsCount: extra?.colorsCount || null, - annotations: annotations && annotations.length > 0 ? annotations : null, - }, - }) - .select('id') - .single(); - - if (error) throw error; - return insertedRow?.id || null; - } catch (error) { - console.error('Error saving to history:', error); - return null; - } -} - -// ─── Generate mockup ────────────────────────────────────────────────────────── - -export interface GenerateMockupParams { - productImage: string; - productName: string; - technique: Technique; - areas: PersonalizationArea[]; -} - -export interface GenerateMockupResult { - singleUrl: string | null; - batchResults: { areaName: string; url: string }[]; -} - -export async function generateMockupApi( - params: GenerateMockupParams, -): Promise { - const { productImage, productName, technique, areas } = params; - const areasWithLogos = areas.filter((a) => a.logoPreview); - const techniquePrompt = getTechniquePrompt(technique); - - if (areasWithLogos.length === 1) { - const area = areasWithLogos[0]; - const isLogoUrl = area.logoPreview?.startsWith('http'); - - const response = await supabase.functions.invoke('generate-mockup', { - body: { - productImageUrl: productImage, - logoBase64: isLogoUrl ? undefined : area.logoPreview, - logoUrl: isLogoUrl ? area.logoPreview : undefined, - techniqueName: technique.name, - techniquePrompt, - positionX: area.positionX, - positionY: area.positionY, - logoWidthCm: area.logoWidth, - logoHeightCm: area.logoHeight, - logoRotation: area.logoRotation || 0, - logoScale: area.logoScale ?? 100, - productName, - areas: areasWithLogos.map((a) => ({ - name: a.name, - positionX: a.positionX, - positionY: a.positionY, - logoWidth: a.logoWidth, - logoHeight: a.logoHeight, - logoRotation: a.logoRotation || 0, - logoScale: a.logoScale ?? 100, - })), - }, - }); - - if (response.error) { - const errData = response.data || response.error; - if (errData?.errorCode === 'SVG_NOT_SUPPORTED') { - throw new Error(errData.error || 'Logos SVG não são suportados. Use PNG ou JPG.'); - } - throw response.error; - } - if (!response.data?.mockupUrl) throw new Error('Nenhuma imagem retornada'); - return { singleUrl: response.data.mockupUrl, batchResults: [] }; - } - - // BATCH - const results: { areaName: string; url: string }[] = []; - const failedAreas: string[] = []; - - for (const area of areasWithLogos) { - const isLogoUrl = area.logoPreview?.startsWith('http'); - toast.info(`Gerando ${area.name}...`, { duration: 2000 }); - - const response = await supabase.functions.invoke('generate-mockup', { - body: { - productImageUrl: productImage, - logoBase64: isLogoUrl ? undefined : area.logoPreview, - logoUrl: isLogoUrl ? area.logoPreview : undefined, - techniqueName: technique.name, - techniquePrompt, - positionX: area.positionX, - positionY: area.positionY, - logoWidthCm: area.logoWidth, - logoHeightCm: area.logoHeight, - logoRotation: area.logoRotation || 0, - logoScale: area.logoScale ?? 100, - productName, - areas: [ - { - name: area.name, - positionX: area.positionX, - positionY: area.positionY, - logoWidth: area.logoWidth, - logoHeight: area.logoHeight, - logoRotation: area.logoRotation || 0, - logoScale: area.logoScale ?? 100, - }, - ], - }, - }); - - if (response.error) { - console.error(`Error generating ${area.name}:`, response.error); - failedAreas.push(area.name); - continue; - } - if (response.data?.mockupUrl) - results.push({ areaName: area.name, url: response.data.mockupUrl }); - } - - if (failedAreas.length > 0) { - toast.warning(`${failedAreas.length} área(s) falharam: ${failedAreas.join(', ')}`, { - duration: 5000, - }); - } - - if (results.length === 0) throw new Error('Nenhum mockup gerado no batch'); - return { singleUrl: results[0].url, batchResults: results }; -} - -// ─── Download ──────────────────────────────────────────────────────────────── - -export async function downloadMockupAsPdf(mockupUrl: string, sku?: string, techniqueName?: string) { - const safeSku = (sku || 'produto').replace(/[^a-zA-Z0-9-_]/g, '-'); - const safeTechnique = (techniqueName || 'tecnica').replace(/[^a-zA-Z0-9-_]/g, '-'); - const fileName = `mockup-${safeSku}-${safeTechnique}.pdf`; - await downloadImageAsPdfFromUrl(mockupUrl, fileName); -} - -// ─── Delete ────────────────────────────────────────────────────────────────── - -export async function deleteMockupFromDb(id: string, userId?: string): Promise { - let query = supabase.from('generated_mockups').delete().eq('id', id); - if (userId) query = query.eq('user_id', userId); - const { error } = await query; - if (error) throw error; -} - -// ─── Default area ──────────────────────────────────────────────────────────── - -export const createDefaultArea = (): PersonalizationArea => ({ - id: crypto.randomUUID(), - name: 'Frente', - positionX: 50, - positionY: 50, - logoWidth: 5, - logoHeight: 3, - logoRotation: 0, - logoScale: 100, - logoPreview: null, -}); +LyoqCiAqIG1vY2t1cEdlbmVyYXRpb25TZXJ2aWNlIOKAlCBIYW5kbGVzIG1vY2t1cCBnZW5lcmF0aW9uIEFQSSBjYWxscyBhbmQgaGlzdG9yeSBwZXJzaXN0ZW5jZS4KICogRXh0cmFjdGVkIGZyb20gdXNlTW9ja3VwR2VuZXJhdG9yIHRvIHJlZHVjZSBob29rIGNvbXBsZXhpdHkuCiAqCiAqIEZpeGVzIChhdWRpdCAyNi8wNS8yMDI2KToKICogVDQ6IHBvc2l0aW9uX3gsIHBvc2l0aW9uX3ksIGxvZ29fdXJsIHBlcnNpc3RlZCBhcyB0b3AtbGV2ZWwgY29sdW1ucy4KICogVDc6IGdldFRlY2huaXF1ZVByb21wdCBza2lwcyAiZGVmYXVsdCIgaW4gc2VhcmNoIGxvb3AuCiAqIFQ4OiBmZXRjaE1vY2t1cEhpc3RvcnkgbGltaXRlZCB0byAyMDAgcmVjb3Jkcy4KICogVDEwOiB0aHVtYm5haWxfdXJsIG5vdyBzdG9yZXMgbW9ja3VwVXJsIChub3QgbG9nb1VybCkuCiAqLwppbXBvcnQgeyBzdXBhYmFzZSB9IGZyb20gJ0AvaW50ZWdyYXRpb25zL3N1cGFiYXNlL2NsaWVudCc7CmltcG9ydCB7IHVwbG9hZExvZ29Ub1N0b3JhZ2UsIGRvd25sb2FkSW1hZ2VBc1BkZkZyb21VcmwgfSBmcm9tICdAL2xpYi9tb2NrdXAtc3RvcmFnZSc7CmltcG9ydCB7IHRvYXN0IH0gZnJvbSAnc29ubmVyJzsKaW1wb3J0IHR5cGUgeyBQZXJzb25hbGl6YXRpb25BcmVhIH0gZnJvbSAnQC9jb21wb25lbnRzL21vY2t1cC9NdWx0aUFyZWFNYW5hZ2VyJzsKCmV4cG9ydCBpbnRlcmZhY2UgVGVjaG5pcXVlIHsKICBpZDogc3RyaW5nOwogIG5hbWU6IHN0cmluZzsKICBjb2RlOiBzdHJpbmcgfCBudWxsOwogIFtrZXk6IHN0cmluZ106IHVua25vd247Cn0KCmV4cG9ydCBpbnRlcmZhY2UgR2VuZXJhdGVkTW9ja3VwIHsKICBpZDogc3RyaW5nOwogIHByb2R1Y3RfaWQ6IHN0cmluZyB8IG51bGw7CiAgcHJvZHVjdF9uYW1lOiBzdHJpbmc7CiAgcHJvZHVjdF9za3U6IHN0cmluZyB8IG51bGw7CiAgdGVjaG5pcXVlX2lkOiBzdHJpbmcgfCBudWxsOwogIHRlY2huaXF1ZV9uYW1lOiBzdHJpbmc7CiAgbW9ja3VwX3VybDogc3RyaW5nOwogIGxheW91dF91cmw/OiBzdHJpbmcgfCBudWxsOwogIGxvZ29fdXJsOiBzdHJpbmc7CiAgcG9zaXRpb25feDogbnVtYmVyIHwgbnVsbDsKICBwb3NpdGlvbl95OiBudW1iZXIgfCBudWxsOwogIGxvZ29fd2lkdGhfY206IG51bWJlciB8IG51bGw7CiAgbG9nb19oZWlnaHRfY206IG51bWJlciB8IG51bGw7CiAgbG9jYXRpb25fbmFtZT86IHN0cmluZyB8IG51bGw7CiAgY29sb3JzX2NvdW50PzogbnVtYmVyIHwgbnVsbDsKICBhbm5vdGF0aW9ucz86IEFycmF5PFJlY29yZDxzdHJpbmcsIHVua25vd24+PiB8IG51bGw7CiAgY2xpZW50X25hbWU/OiBzdHJpbmcgfCBudWxsOwogIGNyZWF0ZWRfYXQ6IHN0cmluZzsKICBjbGllbnRfaWQ6IHN0cmluZyB8IG51bGw7Cn0KCmNvbnN0IFRFQ0hOSVFVRV9QUk9NUFRTOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0gewogIGJvcmRhZG86ICdhcyBwcm9mZXNzaW9uYWwgbWFjaGluZSBlbWJyb2lkZXJ5IHdpdGggdmlzaWJsZSB0aHJlYWQgc3RpdGNoIHRleHR1cmUnLAogIHNpbGs6ICdhcyBzY3JlZW4gcHJpbnRlZCB3aXRoIGZsYXQgc29saWQgY29sb3JzLCBtYXR0ZSBmaW5pc2gnLAogIGR0ZjogJ2FzIERURiBwcmludGVkIHRyYW5zZmVyIHdpdGggdmlicmFudCBjb2xvcnMsIHNsaWdodCBnbG9zc3kgZmluaXNoJywKICBsYXNlcjogJ2FzIGxhc2VyIGVuZ3JhdmVkLCBldGNoZWQgaW50byB0aGUgbWF0ZXJpYWwgc3VyZmFjZSwgbW9ub2Nocm9tYXRpYycsCiAgbGFzZXJfY28yOiAnYXMgQ08yIGxhc2VyIGVuZ3JhdmVkIHdpdGggcHJlY2lzZSBldGNoaW5nIG9uIG9yZ2FuaWMgbWF0ZXJpYWxzJywKICBsYXNlcl9maWJyYTogJ2FzIGZpYmVyIGxhc2VyIG1hcmtlZCBvbiBtZXRhbCB3aXRoIGhpZ2gtY29udHJhc3QgcGVybWFuZW50IG1hcmsnLAogIHN1YmxpbWFjYW86ICdhcyBzdWJsaW1hdGlvbiBwcmludGVkLCBjb2xvcnMgYWJzb3JiZWQgc2VhbWxlc3NseSBpbnRvIHRoZSBtYXRlcmlhbCcsCiAgdGFtcG9ncmFmaWE6ICdhcyBwYWQgcHJpbnRlZCB3aXRoIHNsaWdodGx5IGdsb3NzeSBpbmssIHByZWNpc2Ugc21hbGwgZGV0YWlscycsCiAgaG90X3N0YW1waW5nOiAnYXMgaG90IHN0YW1wZWQgd2l0aCBtZXRhbGxpYyBmb2lsIGZpbmlzaCwgc2hpbnkgcmVmbGVjdGl2ZSBzdXJmYWNlJywKICBhZGVzaXZvOiAnYXMgdmlueWwgc3RpY2tlci9kZWNhbCBhcHBsaWVkIHRvIHN1cmZhY2UnLAogIHV2OiAnYXMgVVYgcHJpbnRlZCB3aXRoIHJhaXNlZCBpbmsgdGV4dHVyZSwgdmlicmFudCBjb2xvcnMnLAogIHRyYW5zZmVyOiAnYXMgaGVhdCB0cmFuc2ZlciB2aW55bCwgc21vb3RoIGZpbmlzaCB3aXRoIHNsaWdodCBzaGVlbicsCiAgZGVmYXVsdDogJ2FzIHByb2Zlc3Npb25hbGx5IHByaW50ZWQvYXBwbGllZCBsb2dvJywKfTsKCi8vIFQ3IEZJWDogc2tpcCAiZGVmYXVsdCIgaW4gdGhlIGxvb3AgdG8gYXZvaWQgZmFsc2Ugc3Vic3RyaW5nIG1hdGNoZXMuCmV4cG9ydCBmdW5jdGlvbiBnZXRUZWNobmlxdWVQcm9tcHQodGVjaG5pcXVlOiBUZWNobmlxdWUpOiBzdHJpbmcgewogIGNvbnN0IGNvZGUgPSB0ZWNobmlxdWUuY29kZT8udG9Mb3dlckNhc2UoKSB8fCB0ZWNobmlxdWUubmFtZS50b0xvd2VyQ2FzZSgpOwogIGZvciAoY29uc3QgW2tleSwgcHJvbXB0XSBvZiBPYmplY3QuZW50cmllcyhURUNITklRVUVfUFJPTVBUUykpIHsKICAgIGlmIChrZXkgPT09ICdkZWZhdWx0JykgY29udGludWU7CiAgICBpZiAoY29kZS5pbmNsdWRlcyhrZXkpIHx8IHRlY2huaXF1ZS5uYW1lLnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMoa2V5KSkgcmV0dXJuIHByb21wdDsKICB9CiAgcmV0dXJuIFRFQ0hOSVFVRV9QUk9NUFRTLmRlZmF1bHQ7Cn0KCi8vIFQ4IEZJWDogbGltaXQgdG8gMjAwIHJlY29yZHMgdG8gcHJldmVudCB1bmJvdW5kZWQgcGF5bG9hZCBncm93dGguCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBmZXRjaE1vY2t1cEhpc3RvcnkodXNlcklkPzogc3RyaW5nKTogUHJvbWlzZTxHZW5lcmF0ZWRNb2NrdXBbXT4gewogIGxldCBxdWVyeSA9IHN1cGFiYXNlCiAgICAuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKQogICAgLnNlbGVjdCgKICAgICAgJ2lkLCBwcm9kdWN0X2lkLCBwcm9kdWN0X25hbWUsIHByb2R1Y3Rfc2t1LCB0ZWNobmlxdWVfaWQsIHRlY2huaXF1ZV9uYW1lLCAnICsKICAgICAgJ21vY2t1cF91cmwsIGxvZ29fdXJsLCBwb3NpdGlvbl94LCBwb3NpdGlvbl95LCBsb2dvX3dpZHRoX2NtLCBsb2dvX2hlaWdodF9jbSwgJyArCiAgICAgICdjbGllbnRfaWQsIGNsaWVudF9uYW1lLCBsb2NhdGlvbl9uYW1lLCBjb2xvcnNfY291bnQsIGFubm90YXRpb25zLCBjcmVhdGVkX2F0JywKICAgICkKICAgIC5vcmRlcignY3JlYXRlZF9hdCcsIHsgYXNjZW5kaW5nOiBmYWxzZSB9KQogICAgLmxpbWl0KDIwMCk7CiAgaWYgKHVzZXJJZCkgcXVlcnkgPSBxdWVyeS5lcSgndXNlcl9pZCcsIHVzZXJJZCk7CiAgY29uc3QgeyBkYXRhLCBlcnJvciB9ID0gYXdhaXQgcXVlcnk7CiAgaWYgKGVycm9yKSB0aHJvdyBlcnJvcjsKICByZXR1cm4gKGRhdGEgfHwgW10pIGFzIHVua25vd24gYXMgR2VuZXJhdGVkTW9ja3VwW107Cn0KCmV4cG9ydCBpbnRlcmZhY2UgU2F2ZU1vY2t1cFBhcmFtcyB7CiAgdXNlcklkOiBzdHJpbmc7CiAgcHJvZHVjdDogeyBpZDogc3RyaW5nOyBuYW1lOiBzdHJpbmc7IHNrdT86IHN0cmluZyB8IG51bGwgfTsKICB0ZWNobmlxdWU6IFRlY2huaXF1ZTsKICBjbGllbnQ6IHsgaWQ/OiBzdHJpbmc7IG5hbWU/OiBzdHJpbmc7IG5vbWVfZmFudGFzaWE/OiBzdHJpbmc7IHJhemFvX3NvY2lhbD86IHN0cmluZyB9IHwgbnVsbDsKICBhcmVhOiBQZXJzb25hbGl6YXRpb25BcmVhOwogIG1vY2t1cFVybDogc3RyaW5nOwogIGFubm90YXRpb25zPzogeyBpZDogc3RyaW5nOyB4OiBudW1iZXI7IHk6IG51bWJlcjsgdGV4dDogc3RyaW5nIH1bXTsKICBleHRyYT86IHsgbGF5b3V0VXJsPzogc3RyaW5nOyBsb2NhdGlvbk5hbWU/OiBzdHJpbmc7IGNvbG9yc0NvdW50PzogbnVtYmVyIH07Cn0KCi8vIFQ0IEZJWDogcG9zaXRpb25feCwgcG9zaXRpb25feSwgbG9nb191cmwsIGxvZ29fd2lkdGhfY20sIGxvZ29faGVpZ2h0X2NtIHBlcnNpc3RlZCB0b3AtbGV2ZWwuCi8vIFQxMCBGSVg6IHRodW1ibmFpbF91cmwgPSBtb2NrdXBVcmwgKHdhcyBpbmNvcnJlY3RseSBzZXQgdG8gbG9nb1VybCkuCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzYXZlTW9ja3VwVG9EYihwYXJhbXM6IFNhdmVNb2NrdXBQYXJhbXMpOiBQcm9taXNlPHN0cmluZyB8IG51bGw+IHsKICBjb25zdCB7IHVzZXJJZCwgcHJvZHVjdCwgdGVjaG5pcXVlLCBjbGllbnQsIGFyZWEsIG1vY2t1cFVybCwgYW5ub3RhdGlvbnMsIGV4dHJhIH0gPSBwYXJhbXM7CgogIHRyeSB7CiAgICBsZXQgbG9nb1VybCA9IGFyZWEubG9nb1ByZXZpZXcgfHwgJyc7CiAgICBpZiAoYXJlYS5sb2dvUHJldmlldz8uc3RhcnRzV2l0aCgnZGF0YTonKSkgewogICAgICBjb25zdCB1cGxvYWRlZFVybCA9IGF3YWl0IHVwbG9hZExvZ29Ub1N0b3JhZ2UoCiAgICAgICAgdXNlcklkLAogICAgICAgIGFyZWEubG9nb1ByZXZpZXcsCiAgICAgICAgYCR7cHJvZHVjdC5za3UgfHwgJ3Byb2R1Y3QnfS0ke3RlY2huaXF1ZS5jb2RlIHx8ICd0ZWNoJ31gLAogICAgICApOwogICAgICBsb2dvVXJsID0gdXBsb2FkZWRVcmwgfHwgJyc7CiAgICB9CgogICAgbGV0IHNhZmVQcm9kdWN0SWQ6IHN0cmluZyB8IG51bGwgPSBudWxsOwogICAgaWYgKHByb2R1Y3QuaWQpIHsKICAgICAgY29uc3QgeyBkYXRhOiBwcm9kdWN0Um93IH0gPSBhd2FpdCBzdXBhYmFzZQogICAgICAgIC5mcm9tKCdwcm9kdWN0cycpCiAgICAgICAgLnNlbGVjdCgnaWQnKQogICAgICAgIC5lcSgnaWQnLCBwcm9kdWN0LmlkKQogICAgICAgIC5tYXliZVNpbmdsZSgpOwogICAgICBpZiAocHJvZHVjdFJvdykgc2FmZVByb2R1Y3RJZCA9IHByb2R1Y3QuaWQ7CiAgICB9CgogICAgY29uc3Qgc2FmZVRlY2huaXF1ZUlkOiBzdHJpbmcgfCBudWxsID0gdGVjaG5pcXVlLmlkIHx8IG51bGw7CiAgICBjb25zdCBjbGllbnROYW1lID0gY2xpZW50Py5ub21lX2ZhbnRhc2lhIHx8IGNsaWVudD8ucmF6YW9fc29jaWFsIHx8IGNsaWVudD8ubmFtZSB8fCBudWxsOwoKICAgIGNvbnN0IHsgZGF0YTogaW5zZXJ0ZWRSb3csIGVycm9yIH0gPSBhd2FpdCBzdXBhYmFzZQogICAgICAuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKQogICAgICAuaW5zZXJ0KHsKICAgICAgICB1c2VyX2lkOiB1c2VySWQsCiAgICAgICAgcHJvZHVjdF9pZDogc2FmZVByb2R1Y3RJZCwKICAgICAgICBwcm9kdWN0X25hbWU6IHByb2R1Y3QubmFtZSwKICAgICAgICBwcm9kdWN0X3NrdTogcHJvZHVjdC5za3UgfHwgbnVsbCwKICAgICAgICB0ZWNobmlxdWVfaWQ6IHNhZmVUZWNobmlxdWVJZCwKICAgICAgICB0ZWNobmlxdWVfbmFtZTogdGVjaG5pcXVlLm5hbWUsCiAgICAgICAgbW9ja3VwX3VybDogbW9ja3VwVXJsLAogICAgICAgIHRodW1ibmFpbF91cmw6IG1vY2t1cFVybCB8fCBudWxsLAogICAgICAgIGxvZ29fdXJsOiBsb2dvVXJsIHx8IG51bGwsCiAgICAgICAgcG9zaXRpb25feDogYXJlYS5wb3NpdGlvblgsCiAgICAgICAgcG9zaXRpb25feTogYXJlYS5wb3NpdGlvblksCiAgICAgICAgbG9nb193aWR0aF9jbTogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgbG9nb19oZWlnaHRfY206IGFyZWEubG9nb0hlaWdodCwKICAgICAgICBhcmVhX25hbWU6IGV4dHJhPy5sb2NhdGlvbk5hbWUgfHwgYXJlYS5uYW1lIHx8ICdGcmVudGUnLAogICAgICAgIGFpX21vZGVsX3VzZWQ6IHRlY2huaXF1ZS5jb2RlIHx8IHRlY2huaXF1ZS5uYW1lIHx8ICdjdXN0b20nLAogICAgICAgIGFyZWFfY29uZmlnOiB7CiAgICAgICAgICBwb3NpdGlvblg6IGFyZWEucG9zaXRpb25YLAogICAgICAgICAgcG9zaXRpb25ZOiBhcmVhLnBvc2l0aW9uWSwKICAgICAgICAgIGxvZ29XaWR0aDogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgICBsb2dvSGVpZ2h0OiBhcmVhLmxvZ29IZWlnaHQsCiAgICAgICAgICBsb2dvVXJsLAogICAgICAgICAgY2xpZW50TmFtZSwKICAgICAgICAgIGNvbG9yc0NvdW50OiBleHRyYT8uY29sb3JzQ291bnQgfHwgbnVsbCwKICAgICAgICAgIGFubm90YXRpb25zOiBhbm5vdGF0aW9ucyAmJiBhbm5vdGF0aW9ucy5sZW5ndGggPiAwID8gYW5ub3RhdGlvbnMgOiBudWxsLAogICAgICAgIH0sCiAgICAgIH0pCiAgICAgIC5zZWxlY3QoJ2lkJykKICAgICAgLnNpbmdsZSgpOwoKICAgIGlmIChlcnJvcikgdGhyb3cgZXJyb3I7CiAgICByZXR1cm4gaW5zZXJ0ZWRSb3c/LmlkIHx8IG51bGw7CiAgfSBjYXRjaCAoZXJyb3IpIHsKICAgIGNvbnNvbGUuZXJyb3IoJ0Vycm9yIHNhdmluZyB0byBoaXN0b3J5OicsIGVycm9yKTsKICAgIHJldHVybiBudWxsOwogIH0KfQoKZXhwb3J0IGludGVyZmFjZSBHZW5lcmF0ZU1vY2t1cFBhcmFtcyB7CiAgcHJvZHVjdEltYWdlOiBzdHJpbmc7CiAgcHJvZHVjdE5hbWU6IHN0cmluZzsKICB0ZWNobmlxdWU6IFRlY2huaXF1ZTsKICBhcmVhczogUGVyc29uYWxpemF0aW9uQXJlYVtdOwp9CgpleHBvcnQgaW50ZXJmYWNlIEdlbmVyYXRlTW9ja3VwUmVzdWx0IHsKICBzaW5nbGVVcmw6IHN0cmluZyB8IG51bGw7CiAgYmF0Y2hSZXN1bHRzOiB7IGFyZWFOYW1lOiBzdHJpbmc7IHVybDogc3RyaW5nIH1bXTsKfQoKZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGdlbmVyYXRlTW9ja3VwQXBpKAogIHBhcmFtczogR2VuZXJhdGVNb2NrdXBQYXJhbXMsCik6IFByb21pc2U8R2VuZXJhdGVNb2NrdXBSZXN1bHQ+IHsKICBjb25zdCB7IHByb2R1Y3RJbWFnZSwgcHJvZHVjdE5hbWUsIHRlY2huaXF1ZSwgYXJlYXMgfSA9IHBhcmFtczsKICBjb25zdCBhcmVhc1dpdGhMb2dvcyA9IGFyZWFzLmZpbHRlcigoYSkgPT4gYS5sb2dvUHJldmlldyk7CiAgY29uc3QgdGVjaG5pcXVlUHJvbXB0ID0gZ2V0VGVjaG5pcXVlUHJvbXB0KHRlY2huaXF1ZSk7CgogIGlmIChhcmVhc1dpdGhMb2dvcy5sZW5ndGggPT09IDEpIHsKICAgIGNvbnN0IGFyZWEgPSBhcmVhc1dpdGhMb2dvc1swXTsKICAgIGNvbnN0IGlzTG9nb1VybCA9IGFyZWEubG9nb1ByZXZpZXc/LnN0YXJ0c1dpdGgoJ2h0dHAnKTsKCiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IHN1cGFiYXNlLmZ1bmN0aW9ucy5pbnZva2UoJ2dlbmVyYXRlLW1vY2t1cCcsIHsKICAgICAgYm9keTogewogICAgICAgIHByb2R1Y3RJbWFnZVVybDogcHJvZHVjdEltYWdlLAogICAgICAgIGxvZ29CYXNlNjQ6IGlzTG9nb1VybCA/IHVuZGVmaW5lZCA6IGFyZWEubG9nb1ByZXZpZXcsCiAgICAgICAgbG9nb1VybDogaXNMb2dvVXJsID8gYXJlYS5sb2dvUHJldmlldyA6IHVuZGVmaW5lZCwKICAgICAgICB0ZWNobmlxdWVOYW1lOiB0ZWNobmlxdWUubmFtZSwKICAgICAgICB0ZWNobmlxdWVQcm9tcHQsCiAgICAgICAgcG9zaXRpb25YOiBhcmVhLnBvc2l0aW9uWCwKICAgICAgICBwb3NpdGlvblk6IGFyZWEucG9zaXRpb25ZLAogICAgICAgIGxvZ29XaWR0aENtOiBhcmVhLmxvZ29XaWR0aCwKICAgICAgICBsb2dvSGVpZ2h0Q206IGFyZWEubG9nb0hlaWdodCwKICAgICAgICBsb2dvUm90YXRpb246IGFyZWEubG9nb1JvdGF0aW9uIHx8IDAsCiAgICAgICAgbG9nb1NjYWxlOiBhcmVhLmxvZ29TY2FsZSA/PyAxMDAsCiAgICAgICAgcHJvZHVjdE5hbWUsCiAgICAgICAgYXJlYXM6IGFyZWFzV2l0aExvZ29zLm1hcCgoYSkgPT4gKHsKICAgICAgICAgIG5hbWU6IGEubmFtZSwKICAgICAgICAgIHBvc2l0aW9uWDogYS5wb3NpdGlvblgsCiAgICAgICAgICBwb3NpdGlvblk6IGEucG9zaXRpb25ZLAogICAgICAgICAgbG9nb1dpZHRoOiBhLmxvZ29XaWR0aCwKICAgICAgICAgIGxvZ29IZWlnaHQ6IGEubG9nb0hlaWdodCwKICAgICAgICAgIGxvZ29Sb3RhdGlvbjogYS5sb2dvUm90YXRpb24gfHwgMCwKICAgICAgICAgIGxvZ29TY2FsZTogYS5sb2dvU2NhbGUgPz8gMTAwLAogICAgICAgIH0pKSwKICAgICAgfSwKICAgIH0pOwoKICAgIGlmIChyZXNwb25zZS5lcnJvcikgewogICAgICBjb25zdCBlcnJEYXRhID0gcmVzcG9uc2UuZGF0YSB8fCByZXNwb25zZS5lcnJvcjsKICAgICAgaWYgKGVyckRhdGE/LmVycm9yQ29kZSA9PT0gJ1NWR19OT1RfU1VQUE9SVEVEJykgewogICAgICAgIHRocm93IG5ldyBFcnJvcihlcnJEYXRhLmVycm9yIHx8ICdMb2dvcyBTVkcgbsOjbyBzw6NvIHN1cG9ydGFkb3MuIFVzZSBQTkcgb3UgSlBHLicpOwogICAgICB9CiAgICAgIHRocm93IHJlc3BvbnNlLmVycm9yOwogICAgfQogICAgaWYgKCFyZXNwb25zZS5kYXRhPy5tb2NrdXBVcmwpIHRocm93IG5ldyBFcnJvcignTmVuaHVtYSBpbWFnZW0gcmV0b3JuYWRhJyk7CiAgICByZXR1cm4geyBzaW5nbGVVcmw6IHJlc3BvbnNlLmRhdGEubW9ja3VwVXJsLCBiYXRjaFJlc3VsdHM6IFtdIH07CiAgfQoKICAvLyBCQVRDSCDigJQgQVBJIGNhbGxzIHNlcXVlbnRpYWwgKGNvbnN0cmFpbnQpLCBEQiBzYXZlcyBoYW5kbGVkIGluIHBhcmFsbGVsIGJ5IHRoZSBob29rIChUNSkuCiAgY29uc3QgcmVzdWx0czogeyBhcmVhTmFtZTogc3RyaW5nOyB1cmw6IHN0cmluZyB9W10gPSBbXTsKICBjb25zdCBmYWlsZWRBcmVhczogc3RyaW5nW10gPSBbXTsKCiAgZm9yIChjb25zdCBhcmVhIG9mIGFyZWFzV2l0aExvZ29zKSB7CiAgICBjb25zdCBpc0xvZ29VcmwgPSBhcmVhLmxvZ29QcmV2aWV3Py5zdGFydHNXaXRoKCdodHRwJyk7CiAgICB0b2FzdC5pbmZvKGBHZXJhbmRvICR7YXJlYS5uYW1lfS4uLmAsIHsgZHVyYXRpb246IDIwMDAgfSk7CgogICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBzdXBhYmFzZS5mdW5jdGlvbnMuaW52b2tlKCdnZW5lcmF0ZS1tb2NrdXAnLCB7CiAgICAgIGJvZHk6IHsKICAgICAgICBwcm9kdWN0SW1hZ2VVcmw6IHByb2R1Y3RJbWFnZSwKICAgICAgICBsb2dvQmFzZTY0OiBpc0xvZ29VcmwgPyB1bmRlZmluZWQgOiBhcmVhLmxvZ29QcmV2aWV3LAogICAgICAgIGxvZ29Vcmw6IGlzTG9nb1VybCA/IGFyZWEubG9nb1ByZXZpZXcgOiB1bmRlZmluZWQsCiAgICAgICAgdGVjaG5pcXVlTmFtZTogdGVjaG5pcXVlLm5hbWUsCiAgICAgICAgdGVjaG5pcXVlUHJvbXB0LAogICAgICAgIHBvc2l0aW9uWDogYXJlYS5wb3NpdGlvblgsCiAgICAgICAgcG9zaXRpb25ZOiBhcmVhLnBvc2l0aW9uWSwKICAgICAgICBsb2dvV2lkdGhDbTogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgbG9nb0hlaWdodENtOiBhcmVhLmxvZ29IZWlnaHQsCiAgICAgICAgbG9nb1JvdGF0aW9uOiBhcmVhLmxvZ29Sb3RhdGlvbiB8fCAwLAogICAgICAgIGxvZ29TY2FsZTogYXJlYS5sb2dvU2NhbGUgPz8gMTAwLAogICAgICAgIHByb2R1Y3ROYW1lLAogICAgICAgIGFyZWFzOiBbewogICAgICAgICAgbmFtZTogYXJlYS5uYW1lLAogICAgICAgICAgcG9zaXRpb25YOiBhcmVhLnBvc2l0aW9uWCwKICAgICAgICAgIHBvc2l0aW9uWTogYXJlYS5wb3NpdGlvblksCiAgICAgICAgICBsb2dvV2lkdGg6IGFyZWEubG9nb1dpZHRoLAogICAgICAgICAgbG9nb0hlaWdodDogYXJlYS5sb2dvSGVpZ2h0LAogICAgICAgICAgbG9nb1JvdGF0aW9uOiBhcmVhLmxvZ29Sb3RhdGlvbiB8fCAwLAogICAgICAgICAgbG9nb1NjYWxlOiBhcmVhLmxvZ29TY2FsZSA/PyAxMDAsCiAgICAgICAgfV0sCiAgICAgIH0sCiAgICB9KTsKCiAgICBpZiAocmVzcG9uc2UuZXJyb3IpIHsKICAgICAgY29uc29sZS5lcnJvcihgRXJyb3IgZ2VuZXJhdGluZyAke2FyZWEubmFtZX06YCwgcmVzcG9uc2UuZXJyb3IpOwogICAgICBmYWlsZWRBcmVhcy5wdXNoKGFyZWEubmFtZSk7CiAgICAgIGNvbnRpbnVlOwogICAgfQogICAgaWYgKHJlc3BvbnNlLmRhdGE/Lm1vY2t1cFVybCkKICAgICAgcmVzdWx0cy5wdXNoKHsgYXJlYU5hbWU6IGFyZWEubmFtZSwgdXJsOiByZXNwb25zZS5kYXRhLm1vY2t1cFVybCB9KTsKICB9CgogIGlmIChmYWlsZWRBcmVhcy5sZW5ndGggPiAwKSB7CiAgICB0b2FzdC53YXJuaW5nKAogICAgICBgJHtmYWlsZWRBcmVhcy5sZW5ndGh9IMOhcmVhKHMpIGZhbGhhcmFtOiAke2ZhaWxlZEFyZWFzLmpvaW4oJywgJyl9YCwKICAgICAgeyBkdXJhdGlvbjogNTAwMCB9LAogICAgKTsKICB9CgogIGlmIChyZXN1bHRzLmxlbmd0aCA9PT0gMCkgdGhyb3cgbmV3IEVycm9yKCdOZW5odW0gbW9ja3VwIGdlcmFkbyBubyBiYXRjaCcpOwogIHJldHVybiB7IHNpbmdsZVVybDogcmVzdWx0c1swXS51cmwsIGJhdGNoUmVzdWx0czogcmVzdWx0cyB9Owp9CgpleHBvcnQgYXN5bmMgZnVuY3Rpb24gZG93bmxvYWRNb2NrdXBBc1BkZihtb2NrdXBVcmw6IHN0cmluZywgc2t1Pzogc3RyaW5nLCB0ZWNobmlxdWVOYW1lPzogc3RyaW5nKSB7CiAgY29uc3Qgc2FmZVNrdSA9IChza3UgfHwgJ3Byb2R1dG8nKS5yZXBsYWNlKC9bXmEtekEtWjAtOS1fXS9nLCAnLScpOwogIGNvbnN0IHNhZmVUZWNobmlxdWUgPSAodGVjaG5pcXVlTmFtZSB8fCAndGVjbmljYScpLnJlcGxhY2UoL1teYS16QS1aMC05LV9dL2csICctJyk7CiAgY29uc3QgZmlsZU5hbWUgPSBgbW9ja3VwLSR7c2FmZVNrdX0tJHtzYWZlVGVjaG5pcXVlfS5wZGZgOwogIGF3YWl0IGRvd25sb2FkSW1hZ2VBc1BkZkZyb21VcmwobW9ja3VwVXJsLCBmaWxlTmFtZSk7Cn0KCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBkZWxldGVNb2NrdXBGcm9tRGIoaWQ6IHN0cmluZywgdXNlcklkPzogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7CiAgbGV0IHF1ZXJ5ID0gc3VwYWJhc2UuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKS5kZWxldGUoKS5lcSgnaWQnLCBpZCk7CiAgaWYgKHVzZXJJZCkgcXVlcnkgPSBxdWVyeS5lcSgndXNlcl9pZCcsIHVzZXJJZCk7CiAgY29uc3QgeyBlcnJvciB9ID0gYXdhaXQgcXVlcnk7CiAgaWYgKGVycm9yKSB0aHJvdyBlcnJvcjsKfQoKZXhwb3J0IGNvbnN0IGNyZWF0ZURlZmF1bHRBcmVhID0gKCk6IFBlcnNvbmFsaXphdGlvbkFyZWEgPT4gKHsKICBpZDogY3J5cHRvLnJhbmRvbVVVSUQoKSwKICBuYW1lOiAnRnJlbnRlJywKICBwb3NpdGlvblg6IDUwLAogIHBvc2l0aW9uWTogNTAsCiAgbG9nb1dpZHRoOiA1LAogIGxvZ29IZWlnaHQ6IDMsCiAgbG9nb1JvdGF0aW9uOiAwLAogIGxvZ29TY2FsZTogMTAwLAogIGxvZ29QcmV2aWV3OiBudWxsLAp9KTsK \ No newline at end of file From 0b9271b05f0f92879660b8299e869e98975423d3 Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Tue, 26 May 2026 17:37:19 -0300 Subject: [PATCH 2/3] fix(useMockupGenerator): T1-T3 useCallback+cleanup, T5 allSettled, T6 userId delete, T9 URL params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T1: 7 async handlers wrapped in useCallback → useMemo output is now truly stable. T2: Correct dependency arrays eliminate stale-closure bugs. T3: historyPushTimeout + draftNoticeTimeoutRef cleaned up on unmount (no memory leaks). T5: Batch DB saves parallelised with Promise.allSettled (was sequential for-await). T6: deleteMockupFromDb receives user?.id for owner-scoped DELETE. T9: URL param cleanup preserves non-processed search params. --- src/hooks/mockup/useMockupGenerator.ts | 254 ++++++++++++++++--------- 1 file changed, 162 insertions(+), 92 deletions(-) diff --git a/src/hooks/mockup/useMockupGenerator.ts b/src/hooks/mockup/useMockupGenerator.ts index 1cc2acce4..05fd737ce 100644 --- a/src/hooks/mockup/useMockupGenerator.ts +++ b/src/hooks/mockup/useMockupGenerator.ts @@ -5,6 +5,14 @@ * - Lazy initialization of techniques and history. * - Memoized computed values (historyClients, productLocations). * - Debounced position history persistence. + * + * Fixes (audit 26/05/2026): + * T1: 7 async handlers wrapped in useCallback → useMemo output is now truly stable. + * T2: Correct dependency arrays eliminate stale-closure bugs. + * T3: historyPushTimeout and draftNoticeTimeoutRef cleaned up on unmount (no memory leaks). + * T5: Batch DB saves parallelised with Promise.allSettled (was sequential waterfall). + * T6: deleteMockupFromDb receives userId → owner-scoped DELETE. + * T9: URL param cleanup only removes processed keys, preserves unrelated params. */ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; @@ -104,6 +112,10 @@ export function useMockupGenerator() { const [showDraftRestoredNotice, setShowDraftRestoredNotice] = useState(false); const isRestoringDraft = useRef(false); + // T3 FIX: refs for debounced timeouts — cleaned up on unmount to prevent memory leaks. + const historyPushTimeout = useRef | null>(null); + const draftNoticeTimeoutRef = useRef | null>(null); + // Tab & positioning const [activeTab, setActiveTab] = useState<'generator' | 'history'>('generator'); const [hasUserInteractedPosition, setHasUserInteractedPosition] = useState(false); @@ -126,6 +138,20 @@ export function useMockupGenerator() { }); }, [activeAreaId, positionHistory]); + // T3 FIX: cleanup historyPushTimeout on unmount. + useEffect(() => { + return () => { + if (historyPushTimeout.current) clearTimeout(historyPushTimeout.current); + }; + }, []); + + // T3 FIX: cleanup draftNoticeTimeoutRef on unmount. + useEffect(() => { + return () => { + if (draftNoticeTimeoutRef.current) clearTimeout(draftNoticeTimeoutRef.current); + }; + }, []); + // ─── Derived state ────────────────────────────────────────────────── const activeArea = @@ -281,7 +307,12 @@ export function useMockupGenerator() { setActiveAreaId(draft.personalizationAreas[0].id); } setShowDraftRestoredNotice(true); - setTimeout(() => setShowDraftRestoredNotice(false), 5000); + // T3 FIX: cancel any previous timer before scheduling a new one. + if (draftNoticeTimeoutRef.current) clearTimeout(draftNoticeTimeoutRef.current); + draftNoticeTimeoutRef.current = setTimeout( + () => setShowDraftRestoredNotice(false), + 5000, + ); } } catch (err) { console.error('Erro ao restaurar rascunho:', err); @@ -315,7 +346,16 @@ export function useMockupGenerator() { ); if (technique) setSelectedTechnique(technique); } - window.history.replaceState({}, '', window.location.pathname); + // T9 FIX: only remove the params we processed — preserve everything else. + const newParams = new URLSearchParams(window.location.search); + newParams.delete('product_id'); + newParams.delete('technique'); + const newSearch = newParams.toString(); + window.history.replaceState( + {}, + '', + window.location.pathname + (newSearch ? `?${newSearch}` : ''), + ); }, [isLoadingData, hasDraftRestored, techniques, getProductById]); // Auto-save with debounce to prevent UI lag during logo dragging/resizing @@ -404,8 +444,6 @@ export function useMockupGenerator() { // ─── Handlers ─────────────────────────────────────────────────────── - const historyPushTimeout = useRef | null>(null); - const updateActiveArea = useCallback( (updates: Partial) => { if (!activeAreaId) return; @@ -505,27 +543,41 @@ export function useMockupGenerator() { return selectedProduct?.images?.[0] || null; }, [productSelection, selectedProduct]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const saveMockupToHistory = async ( - mockupUrl: string, - area: PersonalizationArea, - extra?: { layoutUrl?: string; locationName?: string; colorsCount?: number }, - ): Promise => { - if (!user || !selectedProduct || !selectedTechnique || !area.logoPreview) return null; - return saveMockupToDb({ - userId: user.id, - product: selectedProduct, - technique: selectedTechnique, - client: selectedClient, - area, - mockupUrl, - annotations: mockupAnnotations, - extra, - }); - }; + // T1/T2 FIX: wrapped in useCallback with correct deps — eliminates stale closures. + const saveMockupToHistory = useCallback( + async ( + mockupUrl: string, + area: PersonalizationArea, + extra?: { layoutUrl?: string; locationName?: string; colorsCount?: number }, + ): Promise => { + if (!user || !selectedProduct || !selectedTechnique || !area.logoPreview) return null; + return saveMockupToDb({ + userId: user.id, + product: selectedProduct, + technique: selectedTechnique, + client: selectedClient, + area, + mockupUrl, + annotations: mockupAnnotations, + extra, + }); + }, + [user, selectedProduct, selectedTechnique, selectedClient, mockupAnnotations], + ); - // eslint-disable-next-line react-hooks/exhaustive-deps - const generateMockup = async () => { + // T1/T2 FIX: wrapped in useCallback. Declared before generateMockup (which closes over it). + const downloadMockup = useCallback( + async (url?: string) => { + const mockupUrl = url || generatedMockup; + if (!mockupUrl) return; + await downloadMockupAsPdf(mockupUrl, selectedProduct?.sku, selectedTechnique?.name); + }, + [generatedMockup, selectedProduct, selectedTechnique], + ); + + // T1/T2 FIX: wrapped in useCallback. + // T5 FIX: batch DB saves parallelised with Promise.allSettled (was sequential for-await). + const generateMockup = useCallback(async () => { const areasWithLogos = personalizationAreas.filter((a) => a.logoPreview); if (!selectedClient || !productSelection || !selectedTechnique || areasWithLogos.length === 0) { toast.error('Selecione empresa, produto, técnica e faça upload de pelo menos um logo'); @@ -567,15 +619,28 @@ export function useMockupGenerator() { onDownload: () => downloadMockup(result.singleUrl ?? undefined), }); } else { - for (let i = 0; i < result.batchResults.length; i++) { - const r = result.batchResults[i]; - const area = areasWithLogos.find((a) => a.name === r.areaName) || areasWithLogos[i]; - const recordId = await saveMockupToHistory(r.url, area); - if (recordId && i === result.batchResults.length - 1) { - setLastSavedMockupUrl(r.url); - setLastSavedLayoutMode('ai'); - setLastSavedRecordId(recordId); - } + // T5 FIX: parallel DB writes instead of sequential waterfall. + const batchSaveResults = await Promise.allSettled( + result.batchResults.map((r, i) => { + const area = areasWithLogos.find((a) => a.name === r.areaName) || areasWithLogos[i]; + return saveMockupToHistory(r.url, area).then((recordId) => ({ recordId, r })); + }), + ); + // Pick the last fulfilled result to update lastSaved* state. + const lastFulfilled = batchSaveResults + .filter( + ( + res, + ): res is PromiseFulfilledResult<{ + recordId: string | null; + r: { areaName: string; url: string }; + }> => res.status === 'fulfilled', + ) + .pop(); + if (lastFulfilled?.value.recordId) { + setLastSavedMockupUrl(lastFulfilled.value.r.url); + setLastSavedLayoutMode('ai'); + setLastSavedRecordId(lastFulfilled.value.recordId); } setGeneratedMockup(result.batchResults[0]?.url || result.singleUrl); setGeneratedBatchMockups(result.batchResults); @@ -589,20 +654,22 @@ export function useMockupGenerator() { } finally { setIsLoading(false); } - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const downloadMockup = async (url?: string) => { - const mockupUrl = url || generatedMockup; - if (!mockupUrl) return; - await downloadMockupAsPdf(mockupUrl, selectedProduct?.sku, selectedTechnique?.name); - }; + }, [ + selectedClient, + productSelection, + selectedTechnique, + personalizationAreas, + getProductImage, + saveMockupToHistory, + selectedProduct, + downloadMockup, + ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const deleteMockup = async () => { + // T1/T2/T6 FIX: wrapped in useCallback; passes user?.id for owner-scoped DELETE. + const deleteMockup = useCallback(async () => { if (!mockupToDelete) return; try { - await deleteMockupFromDb(mockupToDelete); + await deleteMockupFromDb(mockupToDelete, user?.id); setMockupHistory((prev) => prev.filter((m) => m.id !== mockupToDelete)); toast.success('Mockup excluído'); } catch (error) { @@ -612,10 +679,10 @@ export function useMockupGenerator() { setDeleteDialogOpen(false); setMockupToDelete(null); } - }; + }, [mockupToDelete, user]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const resetForm = () => { + // T1/T2 FIX: wrapped in useCallback. + const resetForm = useCallback(() => { setProductSelection(null); setSelectedTechnique(null); setSelectedClient(null); @@ -635,57 +702,60 @@ export function useMockupGenerator() { positionHistory.clear(); clearDraft(); logoColorAnalysis.clearAnalysis(); - }; + }, [positionHistory, clearDraft, logoColorAnalysis]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleShareMockup = (mockup: GeneratedMockup) => { + // T1/T2 FIX: wrapped in useCallback — pure function over its argument, no state deps. + const handleShareMockup = useCallback((mockup: GeneratedMockup) => { const text = `Confira o mockup: ${mockup.product_name} com ${mockup.technique_name}`; window.open( `https://wa.me/?text=${encodeURIComponent(text + '\n' + mockup.mockup_url)}`, '_blank', ); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const loadFromHistory = (mockup: GeneratedMockup) => { - const product = mockup.product_id ? getProductById(mockup.product_id) : null; - const technique = mockup.technique_id - ? techniques.find((t) => t.id === mockup.technique_id) - : null; - if (product) - setProductSelection({ - product, - variant: null, - imageUrl: product.images?.[0] || '/placeholder.svg', - }); - else setProductSelection(null); - setSelectedTechnique(technique || null); - setSelectedClient( - mockup.client_id ? { id: mockup.client_id, name: mockup.client_name || 'Cliente' } : null, - ); - const restoredArea: PersonalizationArea = { - id: crypto.randomUUID(), - name: 'Frente', - positionX: mockup.position_x ?? 50, - positionY: mockup.position_y ?? 50, - logoWidth: mockup.logo_width_cm ?? 5, - logoHeight: mockup.logo_height_cm ?? 3, - logoRotation: 0, - logoScale: 100, - logoPreview: mockup.logo_url, - }; - setPersonalizationAreas([restoredArea]); - setActiveAreaId(restoredArea.id); - setGeneratedMockup(null); - setHasUserInteractedPosition(true); - positionHistory.clear(); - setActiveTab('generator'); - if (mockup.logo_url) logoColorAnalysis.analyzeImage(mockup.logo_url); - // BUG-04 FIX: clear stale draft so the auto-save effect does not overwrite the just-loaded - // history configuration ~1 second after this function returns. - clearDraft(); - toast.success('Configurações carregadas!'); - }; + }, []); + + // T1/T2 FIX: wrapped in useCallback with correct deps including techniques & getProductById. + const loadFromHistory = useCallback( + (mockup: GeneratedMockup) => { + const product = mockup.product_id ? getProductById(mockup.product_id) : null; + const technique = mockup.technique_id + ? techniques.find((t) => t.id === mockup.technique_id) + : null; + if (product) + setProductSelection({ + product, + variant: null, + imageUrl: product.images?.[0] || '/placeholder.svg', + }); + else setProductSelection(null); + setSelectedTechnique(technique || null); + setSelectedClient( + mockup.client_id ? { id: mockup.client_id, name: mockup.client_name || 'Cliente' } : null, + ); + const restoredArea: PersonalizationArea = { + id: crypto.randomUUID(), + name: 'Frente', + positionX: mockup.position_x ?? 50, + positionY: mockup.position_y ?? 50, + logoWidth: mockup.logo_width_cm ?? 5, + logoHeight: mockup.logo_height_cm ?? 3, + logoRotation: 0, + logoScale: 100, + logoPreview: mockup.logo_url, + }; + setPersonalizationAreas([restoredArea]); + setActiveAreaId(restoredArea.id); + setGeneratedMockup(null); + setHasUserInteractedPosition(true); + positionHistory.clear(); + setActiveTab('generator'); + if (mockup.logo_url) logoColorAnalysis.analyzeImage(mockup.logo_url); + // BUG-04 FIX: clear stale draft so the auto-save effect does not overwrite the just-loaded + // history configuration ~1 second after this function returns. + clearDraft(); + toast.success('Configurações carregadas!'); + }, + [techniques, getProductById, logoColorAnalysis, clearDraft, positionHistory], + ); const wizardStep = getMockupWizardStep({ hasClient: !!selectedClient, From ccfd211cbc6ca5811560313486e446c900bd78bd Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Tue, 26 May 2026 17:57:08 -0300 Subject: [PATCH 3/3] chore(conflict-resolution): sync mockupGenerationService.ts with main (T4,T7,T8,T10 already merged via #473) --- src/hooks/mockup/mockupGenerationService.ts | 308 +++++++++++++++++++- 1 file changed, 307 insertions(+), 1 deletion(-) diff --git a/src/hooks/mockup/mockupGenerationService.ts b/src/hooks/mockup/mockupGenerationService.ts index 2f6145ceb..fd84a578c 100644 --- a/src/hooks/mockup/mockupGenerationService.ts +++ b/src/hooks/mockup/mockupGenerationService.ts @@ -1 +1,307 @@ -LyoqCiAqIG1vY2t1cEdlbmVyYXRpb25TZXJ2aWNlIOKAlCBIYW5kbGVzIG1vY2t1cCBnZW5lcmF0aW9uIEFQSSBjYWxscyBhbmQgaGlzdG9yeSBwZXJzaXN0ZW5jZS4KICogRXh0cmFjdGVkIGZyb20gdXNlTW9ja3VwR2VuZXJhdG9yIHRvIHJlZHVjZSBob29rIGNvbXBsZXhpdHkuCiAqCiAqIEZpeGVzIChhdWRpdCAyNi8wNS8yMDI2KToKICogVDQ6IHBvc2l0aW9uX3gsIHBvc2l0aW9uX3ksIGxvZ29fdXJsIHBlcnNpc3RlZCBhcyB0b3AtbGV2ZWwgY29sdW1ucy4KICogVDc6IGdldFRlY2huaXF1ZVByb21wdCBza2lwcyAiZGVmYXVsdCIgaW4gc2VhcmNoIGxvb3AuCiAqIFQ4OiBmZXRjaE1vY2t1cEhpc3RvcnkgbGltaXRlZCB0byAyMDAgcmVjb3Jkcy4KICogVDEwOiB0aHVtYm5haWxfdXJsIG5vdyBzdG9yZXMgbW9ja3VwVXJsIChub3QgbG9nb1VybCkuCiAqLwppbXBvcnQgeyBzdXBhYmFzZSB9IGZyb20gJ0AvaW50ZWdyYXRpb25zL3N1cGFiYXNlL2NsaWVudCc7CmltcG9ydCB7IHVwbG9hZExvZ29Ub1N0b3JhZ2UsIGRvd25sb2FkSW1hZ2VBc1BkZkZyb21VcmwgfSBmcm9tICdAL2xpYi9tb2NrdXAtc3RvcmFnZSc7CmltcG9ydCB7IHRvYXN0IH0gZnJvbSAnc29ubmVyJzsKaW1wb3J0IHR5cGUgeyBQZXJzb25hbGl6YXRpb25BcmVhIH0gZnJvbSAnQC9jb21wb25lbnRzL21vY2t1cC9NdWx0aUFyZWFNYW5hZ2VyJzsKCmV4cG9ydCBpbnRlcmZhY2UgVGVjaG5pcXVlIHsKICBpZDogc3RyaW5nOwogIG5hbWU6IHN0cmluZzsKICBjb2RlOiBzdHJpbmcgfCBudWxsOwogIFtrZXk6IHN0cmluZ106IHVua25vd247Cn0KCmV4cG9ydCBpbnRlcmZhY2UgR2VuZXJhdGVkTW9ja3VwIHsKICBpZDogc3RyaW5nOwogIHByb2R1Y3RfaWQ6IHN0cmluZyB8IG51bGw7CiAgcHJvZHVjdF9uYW1lOiBzdHJpbmc7CiAgcHJvZHVjdF9za3U6IHN0cmluZyB8IG51bGw7CiAgdGVjaG5pcXVlX2lkOiBzdHJpbmcgfCBudWxsOwogIHRlY2huaXF1ZV9uYW1lOiBzdHJpbmc7CiAgbW9ja3VwX3VybDogc3RyaW5nOwogIGxheW91dF91cmw/OiBzdHJpbmcgfCBudWxsOwogIGxvZ29fdXJsOiBzdHJpbmc7CiAgcG9zaXRpb25feDogbnVtYmVyIHwgbnVsbDsKICBwb3NpdGlvbl95OiBudW1iZXIgfCBudWxsOwogIGxvZ29fd2lkdGhfY206IG51bWJlciB8IG51bGw7CiAgbG9nb19oZWlnaHRfY206IG51bWJlciB8IG51bGw7CiAgbG9jYXRpb25fbmFtZT86IHN0cmluZyB8IG51bGw7CiAgY29sb3JzX2NvdW50PzogbnVtYmVyIHwgbnVsbDsKICBhbm5vdGF0aW9ucz86IEFycmF5PFJlY29yZDxzdHJpbmcsIHVua25vd24+PiB8IG51bGw7CiAgY2xpZW50X25hbWU/OiBzdHJpbmcgfCBudWxsOwogIGNyZWF0ZWRfYXQ6IHN0cmluZzsKICBjbGllbnRfaWQ6IHN0cmluZyB8IG51bGw7Cn0KCmNvbnN0IFRFQ0hOSVFVRV9QUk9NUFRTOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0gewogIGJvcmRhZG86ICdhcyBwcm9mZXNzaW9uYWwgbWFjaGluZSBlbWJyb2lkZXJ5IHdpdGggdmlzaWJsZSB0aHJlYWQgc3RpdGNoIHRleHR1cmUnLAogIHNpbGs6ICdhcyBzY3JlZW4gcHJpbnRlZCB3aXRoIGZsYXQgc29saWQgY29sb3JzLCBtYXR0ZSBmaW5pc2gnLAogIGR0ZjogJ2FzIERURiBwcmludGVkIHRyYW5zZmVyIHdpdGggdmlicmFudCBjb2xvcnMsIHNsaWdodCBnbG9zc3kgZmluaXNoJywKICBsYXNlcjogJ2FzIGxhc2VyIGVuZ3JhdmVkLCBldGNoZWQgaW50byB0aGUgbWF0ZXJpYWwgc3VyZmFjZSwgbW9ub2Nocm9tYXRpYycsCiAgbGFzZXJfY28yOiAnYXMgQ08yIGxhc2VyIGVuZ3JhdmVkIHdpdGggcHJlY2lzZSBldGNoaW5nIG9uIG9yZ2FuaWMgbWF0ZXJpYWxzJywKICBsYXNlcl9maWJyYTogJ2FzIGZpYmVyIGxhc2VyIG1hcmtlZCBvbiBtZXRhbCB3aXRoIGhpZ2gtY29udHJhc3QgcGVybWFuZW50IG1hcmsnLAogIHN1YmxpbWFjYW86ICdhcyBzdWJsaW1hdGlvbiBwcmludGVkLCBjb2xvcnMgYWJzb3JiZWQgc2VhbWxlc3NseSBpbnRvIHRoZSBtYXRlcmlhbCcsCiAgdGFtcG9ncmFmaWE6ICdhcyBwYWQgcHJpbnRlZCB3aXRoIHNsaWdodGx5IGdsb3NzeSBpbmssIHByZWNpc2Ugc21hbGwgZGV0YWlscycsCiAgaG90X3N0YW1waW5nOiAnYXMgaG90IHN0YW1wZWQgd2l0aCBtZXRhbGxpYyBmb2lsIGZpbmlzaCwgc2hpbnkgcmVmbGVjdGl2ZSBzdXJmYWNlJywKICBhZGVzaXZvOiAnYXMgdmlueWwgc3RpY2tlci9kZWNhbCBhcHBsaWVkIHRvIHN1cmZhY2UnLAogIHV2OiAnYXMgVVYgcHJpbnRlZCB3aXRoIHJhaXNlZCBpbmsgdGV4dHVyZSwgdmlicmFudCBjb2xvcnMnLAogIHRyYW5zZmVyOiAnYXMgaGVhdCB0cmFuc2ZlciB2aW55bCwgc21vb3RoIGZpbmlzaCB3aXRoIHNsaWdodCBzaGVlbicsCiAgZGVmYXVsdDogJ2FzIHByb2Zlc3Npb25hbGx5IHByaW50ZWQvYXBwbGllZCBsb2dvJywKfTsKCi8vIFQ3IEZJWDogc2tpcCAiZGVmYXVsdCIgaW4gdGhlIGxvb3AgdG8gYXZvaWQgZmFsc2Ugc3Vic3RyaW5nIG1hdGNoZXMuCmV4cG9ydCBmdW5jdGlvbiBnZXRUZWNobmlxdWVQcm9tcHQodGVjaG5pcXVlOiBUZWNobmlxdWUpOiBzdHJpbmcgewogIGNvbnN0IGNvZGUgPSB0ZWNobmlxdWUuY29kZT8udG9Mb3dlckNhc2UoKSB8fCB0ZWNobmlxdWUubmFtZS50b0xvd2VyQ2FzZSgpOwogIGZvciAoY29uc3QgW2tleSwgcHJvbXB0XSBvZiBPYmplY3QuZW50cmllcyhURUNITklRVUVfUFJPTVBUUykpIHsKICAgIGlmIChrZXkgPT09ICdkZWZhdWx0JykgY29udGludWU7CiAgICBpZiAoY29kZS5pbmNsdWRlcyhrZXkpIHx8IHRlY2huaXF1ZS5uYW1lLnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMoa2V5KSkgcmV0dXJuIHByb21wdDsKICB9CiAgcmV0dXJuIFRFQ0hOSVFVRV9QUk9NUFRTLmRlZmF1bHQ7Cn0KCi8vIFQ4IEZJWDogbGltaXQgdG8gMjAwIHJlY29yZHMgdG8gcHJldmVudCB1bmJvdW5kZWQgcGF5bG9hZCBncm93dGguCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBmZXRjaE1vY2t1cEhpc3RvcnkodXNlcklkPzogc3RyaW5nKTogUHJvbWlzZTxHZW5lcmF0ZWRNb2NrdXBbXT4gewogIGxldCBxdWVyeSA9IHN1cGFiYXNlCiAgICAuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKQogICAgLnNlbGVjdCgKICAgICAgJ2lkLCBwcm9kdWN0X2lkLCBwcm9kdWN0X25hbWUsIHByb2R1Y3Rfc2t1LCB0ZWNobmlxdWVfaWQsIHRlY2huaXF1ZV9uYW1lLCAnICsKICAgICAgJ21vY2t1cF91cmwsIGxvZ29fdXJsLCBwb3NpdGlvbl94LCBwb3NpdGlvbl95LCBsb2dvX3dpZHRoX2NtLCBsb2dvX2hlaWdodF9jbSwgJyArCiAgICAgICdjbGllbnRfaWQsIGNsaWVudF9uYW1lLCBsb2NhdGlvbl9uYW1lLCBjb2xvcnNfY291bnQsIGFubm90YXRpb25zLCBjcmVhdGVkX2F0JywKICAgICkKICAgIC5vcmRlcignY3JlYXRlZF9hdCcsIHsgYXNjZW5kaW5nOiBmYWxzZSB9KQogICAgLmxpbWl0KDIwMCk7CiAgaWYgKHVzZXJJZCkgcXVlcnkgPSBxdWVyeS5lcSgndXNlcl9pZCcsIHVzZXJJZCk7CiAgY29uc3QgeyBkYXRhLCBlcnJvciB9ID0gYXdhaXQgcXVlcnk7CiAgaWYgKGVycm9yKSB0aHJvdyBlcnJvcjsKICByZXR1cm4gKGRhdGEgfHwgW10pIGFzIHVua25vd24gYXMgR2VuZXJhdGVkTW9ja3VwW107Cn0KCmV4cG9ydCBpbnRlcmZhY2UgU2F2ZU1vY2t1cFBhcmFtcyB7CiAgdXNlcklkOiBzdHJpbmc7CiAgcHJvZHVjdDogeyBpZDogc3RyaW5nOyBuYW1lOiBzdHJpbmc7IHNrdT86IHN0cmluZyB8IG51bGwgfTsKICB0ZWNobmlxdWU6IFRlY2huaXF1ZTsKICBjbGllbnQ6IHsgaWQ/OiBzdHJpbmc7IG5hbWU/OiBzdHJpbmc7IG5vbWVfZmFudGFzaWE/OiBzdHJpbmc7IHJhemFvX3NvY2lhbD86IHN0cmluZyB9IHwgbnVsbDsKICBhcmVhOiBQZXJzb25hbGl6YXRpb25BcmVhOwogIG1vY2t1cFVybDogc3RyaW5nOwogIGFubm90YXRpb25zPzogeyBpZDogc3RyaW5nOyB4OiBudW1iZXI7IHk6IG51bWJlcjsgdGV4dDogc3RyaW5nIH1bXTsKICBleHRyYT86IHsgbGF5b3V0VXJsPzogc3RyaW5nOyBsb2NhdGlvbk5hbWU/OiBzdHJpbmc7IGNvbG9yc0NvdW50PzogbnVtYmVyIH07Cn0KCi8vIFQ0IEZJWDogcG9zaXRpb25feCwgcG9zaXRpb25feSwgbG9nb191cmwsIGxvZ29fd2lkdGhfY20sIGxvZ29faGVpZ2h0X2NtIHBlcnNpc3RlZCB0b3AtbGV2ZWwuCi8vIFQxMCBGSVg6IHRodW1ibmFpbF91cmwgPSBtb2NrdXBVcmwgKHdhcyBpbmNvcnJlY3RseSBzZXQgdG8gbG9nb1VybCkuCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzYXZlTW9ja3VwVG9EYihwYXJhbXM6IFNhdmVNb2NrdXBQYXJhbXMpOiBQcm9taXNlPHN0cmluZyB8IG51bGw+IHsKICBjb25zdCB7IHVzZXJJZCwgcHJvZHVjdCwgdGVjaG5pcXVlLCBjbGllbnQsIGFyZWEsIG1vY2t1cFVybCwgYW5ub3RhdGlvbnMsIGV4dHJhIH0gPSBwYXJhbXM7CgogIHRyeSB7CiAgICBsZXQgbG9nb1VybCA9IGFyZWEubG9nb1ByZXZpZXcgfHwgJyc7CiAgICBpZiAoYXJlYS5sb2dvUHJldmlldz8uc3RhcnRzV2l0aCgnZGF0YTonKSkgewogICAgICBjb25zdCB1cGxvYWRlZFVybCA9IGF3YWl0IHVwbG9hZExvZ29Ub1N0b3JhZ2UoCiAgICAgICAgdXNlcklkLAogICAgICAgIGFyZWEubG9nb1ByZXZpZXcsCiAgICAgICAgYCR7cHJvZHVjdC5za3UgfHwgJ3Byb2R1Y3QnfS0ke3RlY2huaXF1ZS5jb2RlIHx8ICd0ZWNoJ31gLAogICAgICApOwogICAgICBsb2dvVXJsID0gdXBsb2FkZWRVcmwgfHwgJyc7CiAgICB9CgogICAgbGV0IHNhZmVQcm9kdWN0SWQ6IHN0cmluZyB8IG51bGwgPSBudWxsOwogICAgaWYgKHByb2R1Y3QuaWQpIHsKICAgICAgY29uc3QgeyBkYXRhOiBwcm9kdWN0Um93IH0gPSBhd2FpdCBzdXBhYmFzZQogICAgICAgIC5mcm9tKCdwcm9kdWN0cycpCiAgICAgICAgLnNlbGVjdCgnaWQnKQogICAgICAgIC5lcSgnaWQnLCBwcm9kdWN0LmlkKQogICAgICAgIC5tYXliZVNpbmdsZSgpOwogICAgICBpZiAocHJvZHVjdFJvdykgc2FmZVByb2R1Y3RJZCA9IHByb2R1Y3QuaWQ7CiAgICB9CgogICAgY29uc3Qgc2FmZVRlY2huaXF1ZUlkOiBzdHJpbmcgfCBudWxsID0gdGVjaG5pcXVlLmlkIHx8IG51bGw7CiAgICBjb25zdCBjbGllbnROYW1lID0gY2xpZW50Py5ub21lX2ZhbnRhc2lhIHx8IGNsaWVudD8ucmF6YW9fc29jaWFsIHx8IGNsaWVudD8ubmFtZSB8fCBudWxsOwoKICAgIGNvbnN0IHsgZGF0YTogaW5zZXJ0ZWRSb3csIGVycm9yIH0gPSBhd2FpdCBzdXBhYmFzZQogICAgICAuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKQogICAgICAuaW5zZXJ0KHsKICAgICAgICB1c2VyX2lkOiB1c2VySWQsCiAgICAgICAgcHJvZHVjdF9pZDogc2FmZVByb2R1Y3RJZCwKICAgICAgICBwcm9kdWN0X25hbWU6IHByb2R1Y3QubmFtZSwKICAgICAgICBwcm9kdWN0X3NrdTogcHJvZHVjdC5za3UgfHwgbnVsbCwKICAgICAgICB0ZWNobmlxdWVfaWQ6IHNhZmVUZWNobmlxdWVJZCwKICAgICAgICB0ZWNobmlxdWVfbmFtZTogdGVjaG5pcXVlLm5hbWUsCiAgICAgICAgbW9ja3VwX3VybDogbW9ja3VwVXJsLAogICAgICAgIHRodW1ibmFpbF91cmw6IG1vY2t1cFVybCB8fCBudWxsLAogICAgICAgIGxvZ29fdXJsOiBsb2dvVXJsIHx8IG51bGwsCiAgICAgICAgcG9zaXRpb25feDogYXJlYS5wb3NpdGlvblgsCiAgICAgICAgcG9zaXRpb25feTogYXJlYS5wb3NpdGlvblksCiAgICAgICAgbG9nb193aWR0aF9jbTogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgbG9nb19oZWlnaHRfY206IGFyZWEubG9nb0hlaWdodCwKICAgICAgICBhcmVhX25hbWU6IGV4dHJhPy5sb2NhdGlvbk5hbWUgfHwgYXJlYS5uYW1lIHx8ICdGcmVudGUnLAogICAgICAgIGFpX21vZGVsX3VzZWQ6IHRlY2huaXF1ZS5jb2RlIHx8IHRlY2huaXF1ZS5uYW1lIHx8ICdjdXN0b20nLAogICAgICAgIGFyZWFfY29uZmlnOiB7CiAgICAgICAgICBwb3NpdGlvblg6IGFyZWEucG9zaXRpb25YLAogICAgICAgICAgcG9zaXRpb25ZOiBhcmVhLnBvc2l0aW9uWSwKICAgICAgICAgIGxvZ29XaWR0aDogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgICBsb2dvSGVpZ2h0OiBhcmVhLmxvZ29IZWlnaHQsCiAgICAgICAgICBsb2dvVXJsLAogICAgICAgICAgY2xpZW50TmFtZSwKICAgICAgICAgIGNvbG9yc0NvdW50OiBleHRyYT8uY29sb3JzQ291bnQgfHwgbnVsbCwKICAgICAgICAgIGFubm90YXRpb25zOiBhbm5vdGF0aW9ucyAmJiBhbm5vdGF0aW9ucy5sZW5ndGggPiAwID8gYW5ub3RhdGlvbnMgOiBudWxsLAogICAgICAgIH0sCiAgICAgIH0pCiAgICAgIC5zZWxlY3QoJ2lkJykKICAgICAgLnNpbmdsZSgpOwoKICAgIGlmIChlcnJvcikgdGhyb3cgZXJyb3I7CiAgICByZXR1cm4gaW5zZXJ0ZWRSb3c/LmlkIHx8IG51bGw7CiAgfSBjYXRjaCAoZXJyb3IpIHsKICAgIGNvbnNvbGUuZXJyb3IoJ0Vycm9yIHNhdmluZyB0byBoaXN0b3J5OicsIGVycm9yKTsKICAgIHJldHVybiBudWxsOwogIH0KfQoKZXhwb3J0IGludGVyZmFjZSBHZW5lcmF0ZU1vY2t1cFBhcmFtcyB7CiAgcHJvZHVjdEltYWdlOiBzdHJpbmc7CiAgcHJvZHVjdE5hbWU6IHN0cmluZzsKICB0ZWNobmlxdWU6IFRlY2huaXF1ZTsKICBhcmVhczogUGVyc29uYWxpemF0aW9uQXJlYVtdOwp9CgpleHBvcnQgaW50ZXJmYWNlIEdlbmVyYXRlTW9ja3VwUmVzdWx0IHsKICBzaW5nbGVVcmw6IHN0cmluZyB8IG51bGw7CiAgYmF0Y2hSZXN1bHRzOiB7IGFyZWFOYW1lOiBzdHJpbmc7IHVybDogc3RyaW5nIH1bXTsKfQoKZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGdlbmVyYXRlTW9ja3VwQXBpKAogIHBhcmFtczogR2VuZXJhdGVNb2NrdXBQYXJhbXMsCik6IFByb21pc2U8R2VuZXJhdGVNb2NrdXBSZXN1bHQ+IHsKICBjb25zdCB7IHByb2R1Y3RJbWFnZSwgcHJvZHVjdE5hbWUsIHRlY2huaXF1ZSwgYXJlYXMgfSA9IHBhcmFtczsKICBjb25zdCBhcmVhc1dpdGhMb2dvcyA9IGFyZWFzLmZpbHRlcigoYSkgPT4gYS5sb2dvUHJldmlldyk7CiAgY29uc3QgdGVjaG5pcXVlUHJvbXB0ID0gZ2V0VGVjaG5pcXVlUHJvbXB0KHRlY2huaXF1ZSk7CgogIGlmIChhcmVhc1dpdGhMb2dvcy5sZW5ndGggPT09IDEpIHsKICAgIGNvbnN0IGFyZWEgPSBhcmVhc1dpdGhMb2dvc1swXTsKICAgIGNvbnN0IGlzTG9nb1VybCA9IGFyZWEubG9nb1ByZXZpZXc/LnN0YXJ0c1dpdGgoJ2h0dHAnKTsKCiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IHN1cGFiYXNlLmZ1bmN0aW9ucy5pbnZva2UoJ2dlbmVyYXRlLW1vY2t1cCcsIHsKICAgICAgYm9keTogewogICAgICAgIHByb2R1Y3RJbWFnZVVybDogcHJvZHVjdEltYWdlLAogICAgICAgIGxvZ29CYXNlNjQ6IGlzTG9nb1VybCA/IHVuZGVmaW5lZCA6IGFyZWEubG9nb1ByZXZpZXcsCiAgICAgICAgbG9nb1VybDogaXNMb2dvVXJsID8gYXJlYS5sb2dvUHJldmlldyA6IHVuZGVmaW5lZCwKICAgICAgICB0ZWNobmlxdWVOYW1lOiB0ZWNobmlxdWUubmFtZSwKICAgICAgICB0ZWNobmlxdWVQcm9tcHQsCiAgICAgICAgcG9zaXRpb25YOiBhcmVhLnBvc2l0aW9uWCwKICAgICAgICBwb3NpdGlvblk6IGFyZWEucG9zaXRpb25ZLAogICAgICAgIGxvZ29XaWR0aENtOiBhcmVhLmxvZ29XaWR0aCwKICAgICAgICBsb2dvSGVpZ2h0Q206IGFyZWEubG9nb0hlaWdodCwKICAgICAgICBsb2dvUm90YXRpb246IGFyZWEubG9nb1JvdGF0aW9uIHx8IDAsCiAgICAgICAgbG9nb1NjYWxlOiBhcmVhLmxvZ29TY2FsZSA/PyAxMDAsCiAgICAgICAgcHJvZHVjdE5hbWUsCiAgICAgICAgYXJlYXM6IGFyZWFzV2l0aExvZ29zLm1hcCgoYSkgPT4gKHsKICAgICAgICAgIG5hbWU6IGEubmFtZSwKICAgICAgICAgIHBvc2l0aW9uWDogYS5wb3NpdGlvblgsCiAgICAgICAgICBwb3NpdGlvblk6IGEucG9zaXRpb25ZLAogICAgICAgICAgbG9nb1dpZHRoOiBhLmxvZ29XaWR0aCwKICAgICAgICAgIGxvZ29IZWlnaHQ6IGEubG9nb0hlaWdodCwKICAgICAgICAgIGxvZ29Sb3RhdGlvbjogYS5sb2dvUm90YXRpb24gfHwgMCwKICAgICAgICAgIGxvZ29TY2FsZTogYS5sb2dvU2NhbGUgPz8gMTAwLAogICAgICAgIH0pKSwKICAgICAgfSwKICAgIH0pOwoKICAgIGlmIChyZXNwb25zZS5lcnJvcikgewogICAgICBjb25zdCBlcnJEYXRhID0gcmVzcG9uc2UuZGF0YSB8fCByZXNwb25zZS5lcnJvcjsKICAgICAgaWYgKGVyckRhdGE/LmVycm9yQ29kZSA9PT0gJ1NWR19OT1RfU1VQUE9SVEVEJykgewogICAgICAgIHRocm93IG5ldyBFcnJvcihlcnJEYXRhLmVycm9yIHx8ICdMb2dvcyBTVkcgbsOjbyBzw6NvIHN1cG9ydGFkb3MuIFVzZSBQTkcgb3UgSlBHLicpOwogICAgICB9CiAgICAgIHRocm93IHJlc3BvbnNlLmVycm9yOwogICAgfQogICAgaWYgKCFyZXNwb25zZS5kYXRhPy5tb2NrdXBVcmwpIHRocm93IG5ldyBFcnJvcignTmVuaHVtYSBpbWFnZW0gcmV0b3JuYWRhJyk7CiAgICByZXR1cm4geyBzaW5nbGVVcmw6IHJlc3BvbnNlLmRhdGEubW9ja3VwVXJsLCBiYXRjaFJlc3VsdHM6IFtdIH07CiAgfQoKICAvLyBCQVRDSCDigJQgQVBJIGNhbGxzIHNlcXVlbnRpYWwgKGNvbnN0cmFpbnQpLCBEQiBzYXZlcyBoYW5kbGVkIGluIHBhcmFsbGVsIGJ5IHRoZSBob29rIChUNSkuCiAgY29uc3QgcmVzdWx0czogeyBhcmVhTmFtZTogc3RyaW5nOyB1cmw6IHN0cmluZyB9W10gPSBbXTsKICBjb25zdCBmYWlsZWRBcmVhczogc3RyaW5nW10gPSBbXTsKCiAgZm9yIChjb25zdCBhcmVhIG9mIGFyZWFzV2l0aExvZ29zKSB7CiAgICBjb25zdCBpc0xvZ29VcmwgPSBhcmVhLmxvZ29QcmV2aWV3Py5zdGFydHNXaXRoKCdodHRwJyk7CiAgICB0b2FzdC5pbmZvKGBHZXJhbmRvICR7YXJlYS5uYW1lfS4uLmAsIHsgZHVyYXRpb246IDIwMDAgfSk7CgogICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBzdXBhYmFzZS5mdW5jdGlvbnMuaW52b2tlKCdnZW5lcmF0ZS1tb2NrdXAnLCB7CiAgICAgIGJvZHk6IHsKICAgICAgICBwcm9kdWN0SW1hZ2VVcmw6IHByb2R1Y3RJbWFnZSwKICAgICAgICBsb2dvQmFzZTY0OiBpc0xvZ29VcmwgPyB1bmRlZmluZWQgOiBhcmVhLmxvZ29QcmV2aWV3LAogICAgICAgIGxvZ29Vcmw6IGlzTG9nb1VybCA/IGFyZWEubG9nb1ByZXZpZXcgOiB1bmRlZmluZWQsCiAgICAgICAgdGVjaG5pcXVlTmFtZTogdGVjaG5pcXVlLm5hbWUsCiAgICAgICAgdGVjaG5pcXVlUHJvbXB0LAogICAgICAgIHBvc2l0aW9uWDogYXJlYS5wb3NpdGlvblgsCiAgICAgICAgcG9zaXRpb25ZOiBhcmVhLnBvc2l0aW9uWSwKICAgICAgICBsb2dvV2lkdGhDbTogYXJlYS5sb2dvV2lkdGgsCiAgICAgICAgbG9nb0hlaWdodENtOiBhcmVhLmxvZ29IZWlnaHQsCiAgICAgICAgbG9nb1JvdGF0aW9uOiBhcmVhLmxvZ29Sb3RhdGlvbiB8fCAwLAogICAgICAgIGxvZ29TY2FsZTogYXJlYS5sb2dvU2NhbGUgPz8gMTAwLAogICAgICAgIHByb2R1Y3ROYW1lLAogICAgICAgIGFyZWFzOiBbewogICAgICAgICAgbmFtZTogYXJlYS5uYW1lLAogICAgICAgICAgcG9zaXRpb25YOiBhcmVhLnBvc2l0aW9uWCwKICAgICAgICAgIHBvc2l0aW9uWTogYXJlYS5wb3NpdGlvblksCiAgICAgICAgICBsb2dvV2lkdGg6IGFyZWEubG9nb1dpZHRoLAogICAgICAgICAgbG9nb0hlaWdodDogYXJlYS5sb2dvSGVpZ2h0LAogICAgICAgICAgbG9nb1JvdGF0aW9uOiBhcmVhLmxvZ29Sb3RhdGlvbiB8fCAwLAogICAgICAgICAgbG9nb1NjYWxlOiBhcmVhLmxvZ29TY2FsZSA/PyAxMDAsCiAgICAgICAgfV0sCiAgICAgIH0sCiAgICB9KTsKCiAgICBpZiAocmVzcG9uc2UuZXJyb3IpIHsKICAgICAgY29uc29sZS5lcnJvcihgRXJyb3IgZ2VuZXJhdGluZyAke2FyZWEubmFtZX06YCwgcmVzcG9uc2UuZXJyb3IpOwogICAgICBmYWlsZWRBcmVhcy5wdXNoKGFyZWEubmFtZSk7CiAgICAgIGNvbnRpbnVlOwogICAgfQogICAgaWYgKHJlc3BvbnNlLmRhdGE/Lm1vY2t1cFVybCkKICAgICAgcmVzdWx0cy5wdXNoKHsgYXJlYU5hbWU6IGFyZWEubmFtZSwgdXJsOiByZXNwb25zZS5kYXRhLm1vY2t1cFVybCB9KTsKICB9CgogIGlmIChmYWlsZWRBcmVhcy5sZW5ndGggPiAwKSB7CiAgICB0b2FzdC53YXJuaW5nKAogICAgICBgJHtmYWlsZWRBcmVhcy5sZW5ndGh9IMOhcmVhKHMpIGZhbGhhcmFtOiAke2ZhaWxlZEFyZWFzLmpvaW4oJywgJyl9YCwKICAgICAgeyBkdXJhdGlvbjogNTAwMCB9LAogICAgKTsKICB9CgogIGlmIChyZXN1bHRzLmxlbmd0aCA9PT0gMCkgdGhyb3cgbmV3IEVycm9yKCdOZW5odW0gbW9ja3VwIGdlcmFkbyBubyBiYXRjaCcpOwogIHJldHVybiB7IHNpbmdsZVVybDogcmVzdWx0c1swXS51cmwsIGJhdGNoUmVzdWx0czogcmVzdWx0cyB9Owp9CgpleHBvcnQgYXN5bmMgZnVuY3Rpb24gZG93bmxvYWRNb2NrdXBBc1BkZihtb2NrdXBVcmw6IHN0cmluZywgc2t1Pzogc3RyaW5nLCB0ZWNobmlxdWVOYW1lPzogc3RyaW5nKSB7CiAgY29uc3Qgc2FmZVNrdSA9IChza3UgfHwgJ3Byb2R1dG8nKS5yZXBsYWNlKC9bXmEtekEtWjAtOS1fXS9nLCAnLScpOwogIGNvbnN0IHNhZmVUZWNobmlxdWUgPSAodGVjaG5pcXVlTmFtZSB8fCAndGVjbmljYScpLnJlcGxhY2UoL1teYS16QS1aMC05LV9dL2csICctJyk7CiAgY29uc3QgZmlsZU5hbWUgPSBgbW9ja3VwLSR7c2FmZVNrdX0tJHtzYWZlVGVjaG5pcXVlfS5wZGZgOwogIGF3YWl0IGRvd25sb2FkSW1hZ2VBc1BkZkZyb21VcmwobW9ja3VwVXJsLCBmaWxlTmFtZSk7Cn0KCmV4cG9ydCBhc3luYyBmdW5jdGlvbiBkZWxldGVNb2NrdXBGcm9tRGIoaWQ6IHN0cmluZywgdXNlcklkPzogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7CiAgbGV0IHF1ZXJ5ID0gc3VwYWJhc2UuZnJvbSgnZ2VuZXJhdGVkX21vY2t1cHMnKS5kZWxldGUoKS5lcSgnaWQnLCBpZCk7CiAgaWYgKHVzZXJJZCkgcXVlcnkgPSBxdWVyeS5lcSgndXNlcl9pZCcsIHVzZXJJZCk7CiAgY29uc3QgeyBlcnJvciB9ID0gYXdhaXQgcXVlcnk7CiAgaWYgKGVycm9yKSB0aHJvdyBlcnJvcjsKfQoKZXhwb3J0IGNvbnN0IGNyZWF0ZURlZmF1bHRBcmVhID0gKCk6IFBlcnNvbmFsaXphdGlvbkFyZWEgPT4gKHsKICBpZDogY3J5cHRvLnJhbmRvbVVVSUQoKSwKICBuYW1lOiAnRnJlbnRlJywKICBwb3NpdGlvblg6IDUwLAogIHBvc2l0aW9uWTogNTAsCiAgbG9nb1dpZHRoOiA1LAogIGxvZ29IZWlnaHQ6IDMsCiAgbG9nb1JvdGF0aW9uOiAwLAogIGxvZ29TY2FsZTogMTAwLAogIGxvZ29QcmV2aWV3OiBudWxsLAp9KTsK \ No newline at end of file +/** + * mockupGenerationService — Handles mockup generation API calls and history persistence. + * Extracted from useMockupGenerator to reduce hook complexity. + * + * Fixes (audit 26/05/2026): + * T4: position_x, position_y, logo_url persisted as top-level columns. + * T7: getTechniquePrompt skips "default" in search loop. + * T8: fetchMockupHistory limited to 200 records. + * T10: thumbnail_url now stores mockupUrl (not logoUrl). + */ +import { supabase } from '@/integrations/supabase/client'; +import { uploadLogoToStorage, downloadImageAsPdfFromUrl } from '@/lib/mockup-storage'; +import { toast } from 'sonner'; +import type { PersonalizationArea } from '@/components/mockup/MultiAreaManager'; + +export interface Technique { + id: string; + name: string; + code: string | null; + [key: string]: unknown; +} + +export interface GeneratedMockup { + id: string; + product_id: string | null; + product_name: string; + product_sku: string | null; + technique_id: string | null; + technique_name: string; + mockup_url: string; + layout_url?: string | null; + logo_url: string; + position_x: number | null; + position_y: number | null; + logo_width_cm: number | null; + logo_height_cm: number | null; + location_name?: string | null; + colors_count?: number | null; + annotations?: Array> | null; + client_name?: string | null; + created_at: string; + client_id: string | null; +} + +const TECHNIQUE_PROMPTS: Record = { + bordado: 'as professional machine embroidery with visible thread stitch texture', + silk: 'as screen printed with flat solid colors, matte finish', + dtf: 'as DTF printed transfer with vibrant colors, slight glossy finish', + laser: 'as laser engraved, etched into the material surface, monochromatic', + laser_co2: 'as CO2 laser engraved with precise etching on organic materials', + laser_fibra: 'as fiber laser marked on metal with high-contrast permanent mark', + sublimacao: 'as sublimation printed, colors absorbed seamlessly into the material', + tampografia: 'as pad printed with slightly glossy ink, precise small details', + hot_stamping: 'as hot stamped with metallic foil finish, shiny reflective surface', + adesivo: 'as vinyl sticker/decal applied to surface', + uv: 'as UV printed with raised ink texture, vibrant colors', + transfer: 'as heat transfer vinyl, smooth finish with slight sheen', + default: 'as professionally printed/applied logo', +}; + +// T7 FIX: skip "default" in the loop to avoid false substring matches. +export function getTechniquePrompt(technique: Technique): string { + const code = technique.code?.toLowerCase() || technique.name.toLowerCase(); + for (const [key, prompt] of Object.entries(TECHNIQUE_PROMPTS)) { + if (key === 'default') continue; + if (code.includes(key) || technique.name.toLowerCase().includes(key)) return prompt; + } + return TECHNIQUE_PROMPTS.default; +} + +// T8 FIX: limit to 200 records to prevent unbounded payload growth. +export async function fetchMockupHistory(userId?: string): Promise { + let query = supabase + .from('generated_mockups') + .select( + 'id, product_id, product_name, product_sku, technique_id, technique_name, ' + + 'mockup_url, logo_url, position_x, position_y, logo_width_cm, logo_height_cm, ' + + 'client_id, client_name, location_name, colors_count, annotations, created_at', + ) + .order('created_at', { ascending: false }) + .limit(200); + if (userId) query = query.eq('user_id', userId); + const { data, error } = await query; + if (error) throw error; + return (data || []) as unknown as GeneratedMockup[]; +} + +export interface SaveMockupParams { + userId: string; + product: { id: string; name: string; sku?: string | null }; + technique: Technique; + client: { id?: string; name?: string; nome_fantasia?: string; razao_social?: string } | null; + area: PersonalizationArea; + mockupUrl: string; + annotations?: { id: string; x: number; y: number; text: string }[]; + extra?: { layoutUrl?: string; locationName?: string; colorsCount?: number }; +} + +// T4 FIX: position_x, position_y, logo_url, logo_width_cm, logo_height_cm persisted top-level. +// T10 FIX: thumbnail_url = mockupUrl (was incorrectly set to logoUrl). +export async function saveMockupToDb(params: SaveMockupParams): Promise { + const { userId, product, technique, client, area, mockupUrl, annotations, extra } = params; + + try { + let logoUrl = area.logoPreview || ''; + if (area.logoPreview?.startsWith('data:')) { + const uploadedUrl = await uploadLogoToStorage( + userId, + area.logoPreview, + `${product.sku || 'product'}-${technique.code || 'tech'}`, + ); + logoUrl = uploadedUrl || ''; + } + + let safeProductId: string | null = null; + if (product.id) { + const { data: productRow } = await supabase + .from('products') + .select('id') + .eq('id', product.id) + .maybeSingle(); + if (productRow) safeProductId = product.id; + } + + const safeTechniqueId: string | null = technique.id || null; + const clientName = client?.nome_fantasia || client?.razao_social || client?.name || null; + + const { data: insertedRow, error } = await supabase + .from('generated_mockups') + .insert({ + user_id: userId, + product_id: safeProductId, + product_name: product.name, + product_sku: product.sku || null, + technique_id: safeTechniqueId, + technique_name: technique.name, + mockup_url: mockupUrl, + thumbnail_url: mockupUrl || null, + logo_url: logoUrl || null, + position_x: area.positionX, + position_y: area.positionY, + logo_width_cm: area.logoWidth, + logo_height_cm: area.logoHeight, + area_name: extra?.locationName || area.name || 'Frente', + ai_model_used: technique.code || technique.name || 'custom', + area_config: { + positionX: area.positionX, + positionY: area.positionY, + logoWidth: area.logoWidth, + logoHeight: area.logoHeight, + logoUrl, + clientName, + colorsCount: extra?.colorsCount || null, + annotations: annotations && annotations.length > 0 ? annotations : null, + }, + }) + .select('id') + .single(); + + if (error) throw error; + return insertedRow?.id || null; + } catch (error) { + console.error('Error saving to history:', error); + return null; + } +} + +export interface GenerateMockupParams { + productImage: string; + productName: string; + technique: Technique; + areas: PersonalizationArea[]; +} + +export interface GenerateMockupResult { + singleUrl: string | null; + batchResults: { areaName: string; url: string }[]; +} + +export async function generateMockupApi( + params: GenerateMockupParams, +): Promise { + const { productImage, productName, technique, areas } = params; + const areasWithLogos = areas.filter((a) => a.logoPreview); + const techniquePrompt = getTechniquePrompt(technique); + + if (areasWithLogos.length === 1) { + const area = areasWithLogos[0]; + const isLogoUrl = area.logoPreview?.startsWith('http'); + + const response = await supabase.functions.invoke('generate-mockup', { + body: { + productImageUrl: productImage, + logoBase64: isLogoUrl ? undefined : area.logoPreview, + logoUrl: isLogoUrl ? area.logoPreview : undefined, + techniqueName: technique.name, + techniquePrompt, + positionX: area.positionX, + positionY: area.positionY, + logoWidthCm: area.logoWidth, + logoHeightCm: area.logoHeight, + logoRotation: area.logoRotation || 0, + logoScale: area.logoScale ?? 100, + productName, + areas: areasWithLogos.map((a) => ({ + name: a.name, + positionX: a.positionX, + positionY: a.positionY, + logoWidth: a.logoWidth, + logoHeight: a.logoHeight, + logoRotation: a.logoRotation || 0, + logoScale: a.logoScale ?? 100, + })), + }, + }); + + if (response.error) { + const errData = response.data || response.error; + if (errData?.errorCode === 'SVG_NOT_SUPPORTED') { + throw new Error(errData.error || 'Logos SVG nao sao suportados. Use PNG ou JPG.'); + } + throw response.error; + } + if (!response.data?.mockupUrl) throw new Error('Nenhuma imagem retornada'); + return { singleUrl: response.data.mockupUrl, batchResults: [] }; + } + + // BATCH - API calls sequential (constraint), DB saves handled in parallel by the hook (T5). + const results: { areaName: string; url: string }[] = []; + const failedAreas: string[] = []; + + for (const area of areasWithLogos) { + const isLogoUrl = area.logoPreview?.startsWith('http'); + toast.info(`Gerando ${area.name}...`, { duration: 2000 }); + + const response = await supabase.functions.invoke('generate-mockup', { + body: { + productImageUrl: productImage, + logoBase64: isLogoUrl ? undefined : area.logoPreview, + logoUrl: isLogoUrl ? area.logoPreview : undefined, + techniqueName: technique.name, + techniquePrompt, + positionX: area.positionX, + positionY: area.positionY, + logoWidthCm: area.logoWidth, + logoHeightCm: area.logoHeight, + logoRotation: area.logoRotation || 0, + logoScale: area.logoScale ?? 100, + productName, + areas: [ + { + name: area.name, + positionX: area.positionX, + positionY: area.positionY, + logoWidth: area.logoWidth, + logoHeight: area.logoHeight, + logoRotation: area.logoRotation || 0, + logoScale: area.logoScale ?? 100, + }, + ], + }, + }); + + if (response.error) { + console.error(`Error generating ${area.name}:`, response.error); + failedAreas.push(area.name); + continue; + } + if (response.data?.mockupUrl) + results.push({ areaName: area.name, url: response.data.mockupUrl }); + } + + if (failedAreas.length > 0) { + toast.warning(`${failedAreas.length} area(s) falharam: ${failedAreas.join(', ')}`, { + duration: 5000, + }); + } + + if (results.length === 0) throw new Error('Nenhum mockup gerado no batch'); + return { singleUrl: results[0].url, batchResults: results }; +} + +export async function downloadMockupAsPdf(mockupUrl: string, sku?: string, techniqueName?: string) { + const safeSku = (sku || 'produto').replace(/[^a-zA-Z0-9-_]/g, '-'); + const safeTechnique = (techniqueName || 'tecnica').replace(/[^a-zA-Z0-9-_]/g, '-'); + const fileName = `mockup-${safeSku}-${safeTechnique}.pdf`; + await downloadImageAsPdfFromUrl(mockupUrl, fileName); +} + +export async function deleteMockupFromDb(id: string, userId?: string): Promise { + let query = supabase.from('generated_mockups').delete().eq('id', id); + if (userId) query = query.eq('user_id', userId); + const { error } = await query; + if (error) throw error; +} + +export const createDefaultArea = (): PersonalizationArea => ({ + id: crypto.randomUUID(), + name: 'Frente', + positionX: 50, + positionY: 50, + logoWidth: 5, + logoHeight: 3, + logoRotation: 0, + logoScale: 100, + logoPreview: null, +});