feat: auto-fill mockup dimensions and family from Google Sheets

- Extend Sheets read range from A1:Z to A1:AZ (52 columns)
- Parse longueur, largeur, marque, famille, sousFamille, prixRemise, specificFields
- Add /api/products/sheet-info endpoint to fetch product data by ref
- Auto-fill family and dimensions on product selection in all 3 mockup entry points
- Strengthen carrelage mockup prompt with strict tile-size consistency rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 13:35:39 +02:00
parent 4f617b15b2
commit ac58289bb7
7 changed files with 142 additions and 35 deletions
+61 -22
View File
@@ -1,4 +1,5 @@
import type { SyncProduct } from '@/types/sync'
import { findColIndex, extractSpecificFields } from '@/lib/creationSheets'
function normalizeHandle(ref: string): string {
return ref
@@ -7,40 +8,78 @@ function normalizeHandle(ref: string): string {
.replace(/^-+|-+$/g, '')
}
/**
* Parse des lignes brutes Google Sheets en SyncProduct[].
* Colonnes : A=Référence, B=Nom, C=Description, D=Prix, E=Stock, F=Statut
* Exporté pour les tests unitaires.
*/
const HEADER_WORDS = new Set(['id', 'ref', 'sku', 'nom', 'titre', 'reference', 'prix', 'statut'])
function isValidRef(ref: string): boolean {
if (ref.length < 2) return false
if (/^[^a-zA-Z0-9]+$/.test(ref)) return false // que des caractères spéciaux
if (HEADER_WORDS.has(ref.toLowerCase())) return false
return true
function get(row: string[], idx: number): string {
return idx >= 0 ? (row[idx] ?? '').trim() : ''
}
/**
* Parse des lignes brutes Google Sheets en SyncProduct[].
* La première ligne doit être la ligne d'en-têtes.
* Colonnes détectées dynamiquement : UGS, Nom, Prix marché, Stock, Publié
*/
export function parseSheetRows(rows: string[][]): SyncProduct[] {
return rows
.map((row): SyncProduct | null => {
const ref = row[0]?.trim()
if (!ref || !isValidRef(ref)) return null
if (rows.length === 0) return []
const rawPrice = (row[3] ?? '').replace(',', '.').trim()
// Trouver la ligne d'en-têtes : celle qui contient "UGS"
const headerRowIndex = rows.findIndex((row) =>
row.some((cell) => cell?.trim().toUpperCase() === 'UGS')
)
if (headerRowIndex < 0) return []
const headers = rows[headerRowIndex].map((h) => h?.trim() ?? '')
const idx = {
sku: findColIndex(headers, 'UGS'),
nom: findColIndex(headers, 'Nom'),
prixMarche: findColIndex(headers, 'Prix marché'),
prixRemise: findColIndex(headers, 'Prix remisé'),
stock: findColIndex(headers, 'Stock (pièce)') !== -1 ? findColIndex(headers, 'Stock (pièce)') : findColIndex(headers, 'Stock (en pièce)'),
publie: findColIndex(headers, 'Publié'),
poids: findColIndex(headers, 'Poids (kg) fonction unité'),
marque: findColIndex(headers, 'Marque'),
famille: findColIndex(headers, 'Famille'),
sousFamille: findColIndex(headers, 'Sous famille'),
longueur: findColIndex(headers, 'Longueur'),
largeur: findColIndex(headers, 'Largeur'),
}
// Onglet sans colonne UGS ou Prix marché = onglet non-produit, on skip
if (idx.sku < 0 || idx.prixMarche < 0) return []
return rows.slice(headerRowIndex + 1)
.map((row): SyncProduct | null => {
const ref = get(row, idx.sku)
if (!ref || ref.length < 2) return null
const rawPrice = get(row, idx.prixMarche).replace(',', '.')
const price = parseFloat(rawPrice || '0')
// Ligne sans prix valide = séparateur ou note, pas un produit
if (isNaN(price) || price <= 0) return null
const publie = get(row, idx.publie).toLowerCase()
const status = publie.includes('inac') || publie === 'non' || publie === 'false' ? 'draft' : 'active'
const prixRemise = get(row, idx.prixRemise).replace(',', '.')
const longueur = get(row, idx.longueur)
const largeur = get(row, idx.largeur)
const specificFields = extractSpecificFields(headers, row)
return {
ref,
handle: normalizeHandle(ref),
title: row[1]?.trim() || ref,
description: row[2]?.trim() || '',
title: get(row, idx.nom) || ref,
description: '',
price: price.toFixed(2),
stock: parseInt(row[4] ?? '0', 10) || 0,
status: (row[5] ?? '').toLowerCase().includes('inac') ? 'draft' : 'active',
stock: parseInt(get(row, idx.stock) || '0', 10) || 0,
weightKg: parseFloat(get(row, idx.poids).replace(',', '.') || '0') || undefined,
status,
prixRemise: prixRemise || undefined,
marque: get(row, idx.marque) || undefined,
famille: get(row, idx.famille) || undefined,
sousFamille: get(row, idx.sousFamille) || undefined,
longueur: longueur || undefined,
largeur: largeur || undefined,
specificFields: Object.keys(specificFields).length > 0 ? specificFields : undefined,
}
})
.filter((p): p is SyncProduct => p !== null)
@@ -84,7 +123,7 @@ async function listSheetTabs(sheetId: string, apiKey: string): Promise<string[]>
}
async function readOneTab(sheetId: string, apiKey: string, sheetName: string): Promise<SyncProduct[]> {
const range = encodeURIComponent(`${sheetName}!A2:F`)
const range = encodeURIComponent(`${sheetName}!A1:AZ`)
const url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?key=${apiKey}`
const res = await fetch(url)
if (!res.ok) {