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:
+61
-22
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user