Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions src/hooks/mockup/mockupGenerationService.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
/**
* 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';

// ─── 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;
}

Expand All @@ -39,8 +42,6 @@ export interface GeneratedMockup {
client_id: string | null;
}

// ─── Technique prompt mapping ─────────────────────────────────────────────────

const TECHNIQUE_PROMPTS: Record<string, string> = {
bordado: 'as professional machine embroidery with visible thread stitch texture',
silk: 'as screen printed with flat solid colors, matte finish',
Expand All @@ -57,16 +58,17 @@ const TECHNIQUE_PROMPTS: Record<string, string> = {
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;
}

// ─── History fetching ─────────────────────────────────────────────────────────

// T8 FIX: limit to 200 records to prevent unbounded payload growth.
export async function fetchMockupHistory(userId?: string): Promise<GeneratedMockup[]> {
let query = supabase
.from('generated_mockups')
Expand All @@ -75,15 +77,14 @@ export async function fetchMockupHistory(userId?: string): Promise<GeneratedMock
'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 });
.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[];
}

// ─── Save to history ──────────────────────────────────────────────────────────

export interface SaveMockupParams {
userId: string;
product: { id: string; name: string; sku?: string | null };
Expand All @@ -95,6 +96,8 @@ export interface SaveMockupParams {
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<string | null> {
const { userId, product, technique, client, area, mockupUrl, annotations, extra } = params;

Expand Down Expand Up @@ -132,7 +135,12 @@ export async function saveMockupToDb(params: SaveMockupParams): Promise<string |
technique_id: safeTechniqueId,
technique_name: technique.name,
mockup_url: mockupUrl,
thumbnail_url: logoUrl || null,
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: {
Expand All @@ -157,8 +165,6 @@ export async function saveMockupToDb(params: SaveMockupParams): Promise<string |
}
}

// ─── Generate mockup ──────────────────────────────────────────────────────────

export interface GenerateMockupParams {
productImage: string;
productName: string;
Expand Down Expand Up @@ -211,15 +217,15 @@ export async function generateMockupApi(
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 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
// BATCH - API calls sequential (constraint), DB saves handled in parallel by the hook (T5).
const results: { areaName: string; url: string }[] = [];
const failedAreas: string[] = [];

Expand Down Expand Up @@ -265,7 +271,7 @@ export async function generateMockupApi(
}

if (failedAreas.length > 0) {
toast.warning(`${failedAreas.length} área(s) falharam: ${failedAreas.join(', ')}`, {
toast.warning(`${failedAreas.length} area(s) falharam: ${failedAreas.join(', ')}`, {
duration: 5000,
});
}
Expand All @@ -274,26 +280,20 @@ export async function generateMockupApi(
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<void> {
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',
Expand Down
Loading