feat: collection picker in product creation flow

- API /api/creation/collections liste toutes les collections Shopify
- Détection automatique depuis la colonne ID FAMILLE du Sheets
- Sélecteur avec recherche si absente ou pour modifier
- Collection affichée dans l'écran de confirmation
- collectionOverride passé à l'exécution comme fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 16:12:54 +02:00
parent 9e7423eee3
commit 8c9bce7aa5
3 changed files with 134 additions and 4 deletions
+98 -2
View File
@@ -1,5 +1,5 @@
'use client'
import { useState, useCallback } from 'react'
import { useState, useCallback, useEffect } from 'react'
import { Header } from '@/components/layout/Header'
import { PendingRowsTable } from '@/components/creation/PendingRowsTable'
import { MetafieldResolver } from '@/components/creation/MetafieldResolver'
@@ -9,6 +9,8 @@ import type { PendingProduct, MetafieldResolution, CreationStreamEvent } from '@
type Step = 'famille' | 'analyse' | 'metafields' | 'confirmation' | 'execution'
interface ShopifyCollection { id: string; title: string }
export default function CreationPage() {
const [step, setStep] = useState<Step>('famille')
const [selectedTab, setSelectedTab] = useState<string | null>(null)
@@ -23,9 +25,28 @@ export default function CreationPage() {
const [events, setEvents] = useState<CreationStreamEvent[]>([])
const [done, setDone] = useState(false)
// Collection
const [collections, setCollections] = useState<ShopifyCollection[]>([])
const [detectedCollectionId, setDetectedCollectionId] = useState<string>('')
const [collectionOverride, setCollectionOverride] = useState<string>('')
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('')
setClearResult(null)
}
const analyse = async () => {
@@ -41,6 +62,17 @@ export default function CreationPage() {
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<string, PendingProduct[]>).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')
@@ -58,7 +90,7 @@ export default function CreationPage() {
const res = await fetch('/api/creation/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolutions, tab: selectedTab }),
body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }),
})
if (!res.body) return
const reader = res.body.getReader()
@@ -114,8 +146,16 @@ export default function CreationPage() {
setDone(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 (
<>
<Header title="Création produits" />
@@ -171,6 +211,57 @@ export default function CreationPage() {
<PendingRowsTable byTab={byTab} total={total} />
{total > 0 && (
<>
<hr className="border-slate-700" />
{/* Collection Shopify */}
<div>
<h2 className="text-base font-semibold text-white mb-1">Collection Shopify</h2>
{detectedCollectionId ? (
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-green-400"> Détectée depuis le Sheets :</span>
<span className="text-xs text-white font-medium">
{collections.find(c => c.id === detectedCollectionId)?.title ?? `ID ${detectedCollectionId}`}
</span>
<button type="button" onClick={() => { setDetectedCollectionId(''); setCollectionOverride('') }}
className="text-xs text-gray-500 hover:text-gray-300">modifier</button>
</div>
) : (
<p className="text-xs text-amber-400 mb-2"> Aucune collection détectée dans le Sheets choisissez-en une :</p>
)}
{(!detectedCollectionId || collectionOverride !== detectedCollectionId) && (
<div className="space-y-2">
{selectedCollection && (
<div className="flex items-center gap-2">
<span className="text-xs text-blue-300 font-medium"> {selectedCollection.title}</span>
<button type="button" onClick={() => setCollectionOverride('')}
className="text-xs text-gray-500 hover:text-gray-300"></button>
</div>
)}
<input
type="text"
value={collectionSearch}
onChange={e => 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 && (
<div className="max-w-sm bg-slate-700 border border-slate-600 rounded shadow-lg max-h-48 overflow-y-auto">
{filteredCollections.map(c => (
<button key={c.id} type="button"
onClick={() => { setCollectionOverride(c.id); setCollectionSearch('') }}
className="w-full text-left px-3 py-1.5 text-sm text-white hover:bg-slate-600">
{c.title}
</button>
))}
</div>
)}
{collectionSearch && filteredCollections.length === 0 && (
<p className="text-xs text-gray-400">Aucune collection trouvée</p>
)}
</div>
)}
</div>
<hr className="border-slate-700" />
<div>
<h2 className="text-base font-semibold text-white mb-3">Résolution des métachamps</h2>
@@ -189,6 +280,11 @@ export default function CreationPage() {
<div className="space-y-4">
<div className="space-y-2 text-sm">
<p className="text-gray-300"><span className="text-white font-medium">{total} produit{total > 1 ? 's' : ''}</span> à créer dans Shopify</p>
<p className="text-gray-300">
Collection : <span className="text-white font-medium">
{collections.find(c => c.id === collectionOverride)?.title ?? (collectionOverride ? `ID ${collectionOverride}` : <span className="text-amber-400">Aucune</span>)}
</span>
</p>
<p className="text-gray-300"><span className="text-white font-medium">{resolutions.filter(r => r.action !== 'skip').length}</span> type{resolutions.filter(r => r.action !== 'skip').length > 1 ? 's' : ''} de métachamps à assigner</p>
<p className="text-gray-300"><span className="text-white font-medium">{resolutions.filter(r => r.action === 'create').length}</span> 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</p>
<p className="text-yellow-300 text-xs mt-2"> Les descriptions seront générées par OpenAI GPT-4o (~0.01$ par produit)</p>