'use client' import { useState, useEffect, useRef } from 'react' import type { MetafieldDefinition, MetafieldResolution } from '@/types/creation' import { normalizeMetafieldKey } from '@/lib/shopifyMetafields' interface Props { specificColumns: string[] onResolved: (resolutions: MetafieldResolution[]) => void } export function MetafieldResolver({ specificColumns, onResolved }: Props) { const [definitions, setDefinitions] = useState([]) const [matched, setMatched] = useState>({}) const [unmatched, setUnmatched] = useState([]) const [resolutions, setResolutions] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [searches, setSearches] = useState>({}) const [openDropdown, setOpenDropdown] = useState(null) const dropdownRef = useRef(null) const saveTimeoutRef = useRef | null>(null) useEffect(() => { if (specificColumns.length === 0) { setLoading(false); onResolved([]); return } const params = specificColumns.map(c => encodeURIComponent(c)).join(',') Promise.all([ fetch(`/api/creation/metafields?columns=${params}`).then(r => r.json()), fetch('/api/creation/resolutions').then(r => r.json()), ]) .then(([data, cache]: [{ error?: string; definitions: MetafieldDefinition[]; matched: Record; unmatched: string[] }, Record]) => { if (data.error) throw new Error(data.error) setDefinitions(data.definitions) setMatched(data.matched) setUnmatched(data.unmatched) const init: Record = { ...data.matched } data.unmatched.forEach((col: string) => { // Utilise le cache persisté si disponible, sinon 'skip' par défaut init[col] = cache[col] ?? { columnHeader: col, action: 'skip', namespace: 'custom', key: normalizeMetafieldKey(col) } }) setResolutions(init) }) .catch(e => setError((e as Error).message)) .finally(() => setLoading(false)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [specificColumns.join(',')]) useEffect(() => { if (!loading) onResolved(Object.values(resolutions)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [resolutions, loading]) const updateResolution = (col: string, partial: Partial) => { setResolutions(prev => { const next = { ...prev, [col]: { ...prev[col], ...partial } } // Sauvegarde en différé pour éviter un appel par frappe if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) saveTimeoutRef.current = setTimeout(() => { fetch('/api/creation/resolutions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(next), }).catch(() => null) }, 800) return next }) } const filteredDefs = (col: string) => { const q = (searches[col] ?? '').toLowerCase() if (!q) return definitions return definitions.filter(d => d.name.toLowerCase().includes(q) || d.key.toLowerCase().includes(q) || d.namespace.toLowerCase().includes(q) ) } if (loading) return

Analyse des métachamps Shopify…

if (error) return

{error}

if (specificColumns.length === 0) return

Aucune colonne spécifique détectée.

return (
{Object.entries(matched).length > 0 && (

✓ {Object.entries(matched).length} colonne{Object.entries(matched).length > 1 ? 's' : ''} avec correspondance automatique

{Object.entries(matched).map(([col, res]) => (
{col} → {res.namespace}.{res.key}
))}
)} {unmatched.length > 0 && (

Ces colonnes n'ont pas de métachamp correspondant dans Shopify. Choisissez pour chacune : Créer nouveau (nouveau métachamp Shopify), Associer (utiliser un existant), ou Ignorer (ne pas synchroniser).

{unmatched.map(col => { const res = resolutions[col] if (!res) return null return (

{col}

{res.action === 'create' && ✓ Sera créé dans Shopify} {res.action === 'map' && ✓ Associé à un existant} {res.action === 'skip' && — Ignoré}
{(['create', 'map', 'skip'] as const).map(action => ( ))}
{res.action === 'create' && (
updateResolution(col, { key: e.target.value })} placeholder="ex: puissance" className="w-full bg-slate-600 border border-slate-500 rounded px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" />
updateResolution(col, { namespace: e.target.value })} placeholder="custom" className="w-full bg-slate-600 border border-slate-500 rounded px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" />
)} {res.action === 'map' && (
{res.existingDefinitionId ? (
{definitions.find(d => d.id === res.existingDefinitionId)?.name ?? res.key} ({res.namespace}.{res.key})
) : (
{ setSearches(p => ({ ...p, [col]: e.target.value })); setOpenDropdown(col) }} onFocus={() => setOpenDropdown(col)} placeholder="Rechercher un métachamp…" className="w-full bg-slate-600 border border-slate-500 rounded px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-blue-500" /> {openDropdown === col && (
{filteredDefs(col).length === 0 ? (

Aucun résultat

) : filteredDefs(col).map(d => ( ))}
)}
)}
)}
) })}
{/* Résumé des actions */}
{Object.values(resolutions).filter(r => r.action === 'create').length} à créer {Object.values(resolutions).filter(r => r.action === 'map').length} à associer {Object.values(resolutions).filter(r => r.action === 'skip').length} ignorés
)}
) }