'use client' import { useState, useCallback, useEffect, useRef } 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 { FamilySelector } from '@/components/common/FamilySelector' import type { PendingProduct, MetafieldResolution, CreationStreamEvent } from '@/types/creation' type Step = 'famille' | 'analyse' | 'metafields' | 'confirmation' | 'execution' interface ShopifyCollection { id: string; title: string } export default function CreationPage() { const [step, setStep] = useState('famille') const [selectedTab, setSelectedTab] = useState(null) 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 [stopped, setStopped] = useState(false) const abortRef = useRef(null) // Sync stock const [stockSyncing, setStockSyncing] = useState(false) const [stockResult, setStockResult] = useState<{ updated: number; errors: number } | null>(null) const [stockProgress, setStockProgress] = useState<{ current: number; total: number; title: string } | null>(null) const syncStock = async () => { setStockSyncing(true) setStockResult(null) setStockProgress(null) try { const res = await fetch('/api/creation/sync-stock', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) if (!res.body) return const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) 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 evt = JSON.parse(line) if (evt.type === 'progress') setStockProgress({ current: evt.current, total: evt.total, title: evt.title }) if (evt.type === 'done') setStockResult({ updated: evt.updated, errors: evt.errors }) } catch { /* */ } } } } finally { setStockSyncing(false) setStockProgress(null) } } // Collection const [collections, setCollections] = useState([]) const [detectedCollectionId, setDetectedCollectionId] = useState('') const [collectionOverride, setCollectionOverride] = useState('') const [collectionSearch, setCollectionSearch] = useState('') const [loadingCollections, setLoadingCollections] = useState(false) useEffect(() => { setLoadingCollections(true) fetch('/api/creation/collections') .then(r => r.json()) .then(d => setCollections(d.collections ?? [])) .finally(() => setLoadingCollections(false)) }, []) const handleSelectTab = (tab: string) => { setSelectedTab(tab) setStep('analyse') setDetectedCollectionId('') setCollectionOverride('') setCollectionSearch('') } const analyse = async () => { setLoading(true) setError(null) try { const url = selectedTab ? `/api/creation/preview?tab=${encodeURIComponent(selectedTab)}` : '/api/creation/preview' const res = await fetch(url) const data = await res.json() if (data.error) throw new Error(data.error) setByTab(data.byTab) setTotal(data.total) setSpecificColumns(data.specificColumns) // Détecter la collection depuis les produits analysés const products: PendingProduct[] = Object.values(data.byTab as Record).flat() const ids = Array.from(new Set(products.map(p => p.collectionId).filter(Boolean))) if (ids.length === 1) { setDetectedCollectionId(ids[0]) setCollectionOverride(ids[0]) } else { setDetectedCollectionId('') setCollectionOverride('') } setStep('metafields') } catch (e) { setError(e instanceof Error ? e.message : 'Erreur') } finally { setLoading(false) } } const handleResolved = useCallback((r: MetafieldResolution[]) => { setResolutions(r) }, []) const stop = useCallback(() => { abortRef.current?.abort() setStopped(true) setDone(true) }, []) const execute = async () => { const controller = new AbortController() abortRef.current = controller setStep('execution') setEvents([]) setDone(false) setStopped(false) try { const res = await fetch('/api/creation/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }), signal: controller.signal, }) 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 */ } } } } catch (e) { if ((e as Error).name !== 'AbortError') { setEvents(prev => [...prev, { type: 'error', title: '', message: (e as Error).message }]) } } 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) const reset = () => { abortRef.current?.abort() abortRef.current = null setStep('famille') setSelectedTab(null) setEvents([]) setDone(false) setStopped(false) setByTab({}) setTotal(0) setDetectedCollectionId('') setCollectionOverride('') } const filteredCollections = collectionSearch ? collections.filter(c => c.title.toLowerCase().includes(collectionSearch.toLowerCase())) : collections const selectedCollection = collections.find(c => c.id === collectionOverride) return ( <>
{STEPS.map((s, i) => (
{i + 1}. {s.label} {i < STEPS.length - 1 && }
))}
{/* Étape 0 — Sélection famille */} {step === 'famille' && (

Choisir une famille

Synchroniser le stock

Met à jour le stock Shopify pour tous les produits existants (ceux avec un ID Shopify dans le Sheets).

{stockSyncing && stockProgress && (

→ {stockProgress.title}

)} {stockResult && (

{stockResult.errors === 0 ? ✓ {stockResult.updated} produit{stockResult.updated > 1 ? 's' : ''} mis à jour : ✓ {stockResult.updated} mis à jour — ⚠ {stockResult.errors} erreur{stockResult.errors > 1 ? 's' : ''} }

)}
)}
{error &&
{error}
} {step === 'analyse' && (
📂 Famille : {selectedTab}

Détecte les lignes de {selectedTab}Publié=1 et ID vide.

)} {step === 'metafields' && (
{total > 0 && ( <>
{/* Collection Shopify */}

Collection Shopify

{detectedCollectionId ? (
✓ Détectée depuis le Sheets : {collections.find(c => c.id === detectedCollectionId)?.title ?? `ID ${detectedCollectionId}`}
) : (

⚠ Aucune collection détectée dans le Sheets — choisissez-en une :

)} {(!detectedCollectionId || collectionOverride !== detectedCollectionId) && (
{selectedCollection && (
→ {selectedCollection.title}
)} setCollectionSearch(e.target.value)} placeholder={loadingCollections ? 'Chargement…' : 'Rechercher une collection Shopify…'} className="w-full max-w-sm bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" /> {collectionSearch && filteredCollections.length > 0 && (
{filteredCollections.map(c => ( ))}
)} {collectionSearch && filteredCollections.length === 0 && (

Aucune collection trouvée

)}
)}

Résolution des métachamps

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

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

Collection : {collections.find(c => c.id === collectionOverride)?.title ?? (collectionOverride ? `ID ${collectionOverride}` : Aucune)}

{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 && ( )} {done && ( <> {stopped &&

⚠ Création interrompue — les produits déjà créés sont conservés dans Shopify.

} )}
)}
) }