feat(plan5): page /creation stepper 4 étapes + lien nav + fix TS shopifyMetafields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Step>('analyse')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [byTab, setByTab] = useState<Record<string, PendingProduct[]>>({})
|
||||
const [total, setTotal] = useState(0)
|
||||
const [specificColumns, setSpecificColumns] = useState<string[]>([])
|
||||
const [resolutions, setResolutions] = useState<MetafieldResolution[]>([])
|
||||
const [events, setEvents] = useState<CreationStreamEvent[]>([])
|
||||
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 (
|
||||
<>
|
||||
<Header title="Création produits" />
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex gap-2">
|
||||
{STEPS.map((s, i) => (
|
||||
<div key={s.id} className="flex items-center gap-2">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${step === s.id ? 'bg-indigo-600 text-white' : i < currentIdx ? 'bg-green-700 text-white' : 'bg-slate-700 text-gray-400'}`}>
|
||||
{i + 1}. {s.label}
|
||||
</span>
|
||||
{i < STEPS.length - 1 && <span className="text-gray-600">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800 rounded-xl p-6">
|
||||
{error && <div className="mb-4 p-3 bg-red-900/30 border border-red-700 rounded-lg text-red-300 text-sm">{error}</div>}
|
||||
|
||||
{step === 'analyse' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300 text-sm">
|
||||
Lit tous les onglets du Google Sheets et détecte les lignes où <code className="text-indigo-300">Publié=1</code> et <code className="text-indigo-300">ID vide</code>.
|
||||
</p>
|
||||
<button onClick={analyse} disabled={loading}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
{loading ? 'Analyse en cours…' : 'Analyser le Sheets'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'metafields' && (
|
||||
<div className="space-y-6">
|
||||
<PendingRowsTable byTab={byTab} total={total} />
|
||||
{total > 0 && (
|
||||
<>
|
||||
<hr className="border-slate-700" />
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-3">Résolution des métachamps</h2>
|
||||
<MetafieldResolver specificColumns={specificColumns} onResolved={handleResolved} />
|
||||
</div>
|
||||
<button onClick={() => setStep('confirmation')}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
Continuer →
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'confirmation' && (
|
||||
<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"><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>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={() => setStep('metafields')}
|
||||
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-gray-300 rounded-lg text-sm font-medium transition-colors">
|
||||
← Retour
|
||||
</button>
|
||||
<button onClick={execute}
|
||||
className="px-4 py-2 bg-green-700 hover:bg-green-600 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
Lancer la création ({total} produits)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'execution' && (
|
||||
<div className="space-y-4">
|
||||
<CreationProgress events={events} done={done} />
|
||||
{done && (
|
||||
<button onClick={() => { setStep('analyse'); setEvents([]); setDone(false); setByTab({}); setTotal(0) }}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
Nouvelle session
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
|
||||
@@ -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, '_')
|
||||
|
||||
Reference in New Issue
Block a user