diff --git a/src/app/(dashboard)/creation/page.tsx b/src/app/(dashboard)/creation/page.tsx new file mode 100644 index 0000000..f98bf1e --- /dev/null +++ b/src/app/(dashboard)/creation/page.tsx @@ -0,0 +1,166 @@ +'use client' +import { useState, useCallback } from 'react' +import { Header } from '@/components/layout/Header' +import { PendingRowsTable } from '@/components/creation/PendingRowsTable' +import { MetafieldResolver } from '@/components/creation/MetafieldResolver' +import { CreationProgress } from '@/components/creation/CreationProgress' +import type { PendingProduct, MetafieldResolution, CreationStreamEvent } from '@/types/creation' + +type Step = 'analyse' | 'metafields' | 'confirmation' | 'execution' + +export default function CreationPage() { + const [step, setStep] = useState('analyse') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [byTab, setByTab] = useState>({}) + const [total, setTotal] = useState(0) + const [specificColumns, setSpecificColumns] = useState([]) + const [resolutions, setResolutions] = useState([]) + const [events, setEvents] = useState([]) + const [done, setDone] = useState(false) + + const analyse = async () => { + setLoading(true) + setError(null) + try { + const res = await fetch('/api/creation/preview') + const data = await res.json() + if (data.error) throw new Error(data.error) + setByTab(data.byTab) + setTotal(data.total) + setSpecificColumns(data.specificColumns) + setStep('metafields') + } catch (e) { + setError(e instanceof Error ? e.message : 'Erreur') + } finally { + setLoading(false) + } + } + + const handleResolved = useCallback((r: MetafieldResolution[]) => { setResolutions(r) }, []) + + const execute = async () => { + setStep('execution') + setEvents([]) + setDone(false) + const res = await fetch('/api/creation/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ resolutions }), + }) + if (!res.body) return + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + while (true) { + const { done: readerDone, value } = await reader.read() + if (readerDone) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + for (const line of lines) { + if (!line.trim()) continue + try { + const event = JSON.parse(line) as CreationStreamEvent + setEvents(prev => [...prev, event]) + if (event.type === 'done') setDone(true) + } catch { /* ligne incomplète */ } + } + } + setDone(true) + } + + const STEPS: { id: Step; label: string }[] = [ + { id: 'analyse', label: 'Analyse' }, + { id: 'metafields', label: 'Métachamps' }, + { id: 'confirmation', label: 'Confirmation' }, + { id: 'execution', label: 'Exécution' }, + ] + const currentIdx = STEPS.findIndex(s => s.id === step) + + return ( + <> +
+
+
+ {STEPS.map((s, i) => ( +
+ + {i + 1}. {s.label} + + {i < STEPS.length - 1 && } +
+ ))} +
+ +
+ {error &&
{error}
} + + {step === 'analyse' && ( +
+

+ Lit tous les onglets du Google Sheets et détecte les lignes où Publié=1 et ID vide. +

+ +
+ )} + + {step === 'metafields' && ( +
+ + {total > 0 && ( + <> +
+
+

Résolution des métachamps

+ +
+ + + )} +
+ )} + + {step === 'confirmation' && ( +
+
+

{total} produit{total > 1 ? 's' : ''} à créer dans Shopify

+

{resolutions.filter(r => r.action !== 'skip').length} type{resolutions.filter(r => r.action !== 'skip').length > 1 ? 's' : ''} de métachamps à assigner

+

{resolutions.filter(r => r.action === 'create').length} nouvelle{resolutions.filter(r => r.action === 'create').length > 1 ? 's' : ''} définition{resolutions.filter(r => r.action === 'create').length > 1 ? 's' : ''} à créer dans Shopify

+

⚠ Les descriptions seront générées par OpenAI GPT-4o (~0.01$ par produit)

+
+
+ + +
+
+ )} + + {step === 'execution' && ( +
+ + {done && ( + + )} +
+ )} +
+
+ + ) +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 2acaaf5..916d7e9 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -15,6 +15,7 @@ const NAV_ITEMS: NavItem[] = [ { href: '/produits', label: 'Produits', icon: '📦' }, { href: '/images', label: 'Images', icon: '🖼️' }, { href: '/sync', label: 'Sync Sheets', icon: '🔄' }, + { href: '/creation', label: 'Création', icon: '✨' }, { href: '/parametres', label: 'Paramètres', icon: '⚙️', adminOnly: true }, ] diff --git a/src/lib/shopifyMetafields.ts b/src/lib/shopifyMetafields.ts index 9c00d4b..d6d47de 100644 --- a/src/lib/shopifyMetafields.ts +++ b/src/lib/shopifyMetafields.ts @@ -16,7 +16,7 @@ export function normalizeMetafieldKey(header: string): string { return header .toLowerCase() .normalize('NFD') - .replace(/\p{Diacritic}/gu, '') + .replace(/[̀-ͯ]/g, '') // retire les accents .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .replace(/_+/g, '_')