163 lines
8.6 KiB
TypeScript
163 lines
8.6 KiB
TypeScript
'use client'
|
|
import { useState, useEffect } 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<MetafieldDefinition[]>([])
|
|
const [matched, setMatched] = useState<Record<string, MetafieldResolution>>({})
|
|
const [unmatched, setUnmatched] = useState<string[]>([])
|
|
const [resolutions, setResolutions] = useState<Record<string, MetafieldResolution>>({})
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (specificColumns.length === 0) { setLoading(false); onResolved([]); return }
|
|
const params = specificColumns.map(c => encodeURIComponent(c)).join(',')
|
|
fetch(`/api/creation/metafields?columns=${params}`)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.error) throw new Error(data.error)
|
|
setDefinitions(data.definitions)
|
|
setMatched(data.matched)
|
|
setUnmatched(data.unmatched)
|
|
const init: Record<string, MetafieldResolution> = { ...data.matched }
|
|
// Défaut : 'skip' — l'utilisateur choisit explicitement ce qu'il veut créer
|
|
data.unmatched.forEach((col: string) => {
|
|
init[col] = { columnHeader: col, action: 'skip', namespace: 'custom', key: normalizeMetafieldKey(col) }
|
|
})
|
|
setResolutions(init)
|
|
})
|
|
.catch(e => setError(e.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<MetafieldResolution>) => {
|
|
setResolutions(prev => ({ ...prev, [col]: { ...prev[col], ...partial } }))
|
|
}
|
|
|
|
if (loading) return <p className="text-gray-400 text-sm">Analyse des métachamps Shopify…</p>
|
|
if (error) return <p className="text-red-400 text-sm">{error}</p>
|
|
if (specificColumns.length === 0) return <p className="text-gray-400 text-sm">Aucune colonne spécifique détectée.</p>
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{Object.entries(matched).length > 0 && (
|
|
<div>
|
|
<h3 className="text-sm font-medium text-green-400 mb-2">
|
|
✓ {Object.entries(matched).length} colonne{Object.entries(matched).length > 1 ? 's' : ''} avec correspondance automatique
|
|
</h3>
|
|
<div className="space-y-1">
|
|
{Object.entries(matched).map(([col, res]) => (
|
|
<div key={col} className="flex items-center gap-3 px-3 py-2 bg-slate-700/50 rounded text-sm">
|
|
<span className="text-gray-300 flex-1">{col}</span>
|
|
<span className="text-green-400 text-xs">→ {res.namespace}.{res.key}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{unmatched.length > 0 && (
|
|
<div>
|
|
<div className="mb-3 p-3 bg-slate-700/50 rounded-lg border border-slate-600">
|
|
<p className="text-xs text-gray-400">
|
|
Ces colonnes n'ont pas de métachamp correspondant dans Shopify.
|
|
Choisissez pour chacune : <span className="text-indigo-300 font-medium">Créer nouveau</span> (nouveau métachamp Shopify),
|
|
<span className="text-blue-300 font-medium"> Associer</span> (utiliser un existant), ou
|
|
<span className="text-gray-300 font-medium"> Ignorer</span> (ne pas synchroniser).
|
|
</p>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{unmatched.map(col => {
|
|
const res = resolutions[col]
|
|
if (!res) return null
|
|
return (
|
|
<div key={col} className={`p-3 rounded-lg space-y-2 border ${res.action === 'skip' ? 'bg-slate-700/40 border-slate-700' : res.action === 'create' ? 'bg-indigo-900/20 border-indigo-700/50' : 'bg-blue-900/20 border-blue-700/50'}`}>
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm text-white font-medium">{col}</p>
|
|
{res.action === 'create' && <span className="text-xs text-indigo-300">✓ Sera créé dans Shopify</span>}
|
|
{res.action === 'map' && <span className="text-xs text-blue-300">✓ Associé à un existant</span>}
|
|
{res.action === 'skip' && <span className="text-xs text-gray-500">— Ignoré</span>}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{(['create', 'map', 'skip'] as const).map(action => (
|
|
<button
|
|
key={action}
|
|
type="button"
|
|
onClick={() => updateResolution(col, { action })}
|
|
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
|
res.action === action
|
|
? action === 'create' ? 'bg-indigo-600 text-white'
|
|
: action === 'map' ? 'bg-blue-600 text-white'
|
|
: 'bg-slate-500 text-white'
|
|
: 'bg-slate-600 text-gray-400 hover:bg-slate-500 hover:text-gray-200'
|
|
}`}
|
|
>
|
|
{action === 'create' ? '+ Créer nouveau' : action === 'map' ? '↔ Associer existant' : '✕ Ignorer'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{res.action === 'create' && (
|
|
<div className="flex gap-2 mt-1">
|
|
<div className="flex-1">
|
|
<label className="block text-xs text-gray-400 mb-1">Clé Shopify</label>
|
|
<input
|
|
type="text"
|
|
value={res.key}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div className="w-28">
|
|
<label className="block text-xs text-gray-400 mb-1">Namespace</label>
|
|
<input
|
|
type="text"
|
|
value={res.namespace}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{res.action === 'map' && (
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Métachamp existant</label>
|
|
<select
|
|
value={res.existingDefinitionId ?? ''}
|
|
onChange={e => { const def = definitions.find(d => d.id === e.target.value); if (def) updateResolution(col, { existingDefinitionId: def.id, key: def.key, namespace: def.namespace }) }}
|
|
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"
|
|
>
|
|
<option value="">-- Choisir un métachamp existant --</option>
|
|
{definitions.map(d => <option key={d.id} value={d.id}>{d.name} ({d.namespace}.{d.key})</option>)}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
{/* Résumé des actions */}
|
|
<div className="mt-3 flex gap-4 text-xs text-gray-400">
|
|
<span className="text-indigo-300 font-medium">{Object.values(resolutions).filter(r => r.action === 'create').length} à créer</span>
|
|
<span className="text-blue-300 font-medium">{Object.values(resolutions).filter(r => r.action === 'map').length} à associer</span>
|
|
<span>{Object.values(resolutions).filter(r => r.action === 'skip').length} ignorés</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|