Files
gestion-materiaux-destock/src/app/(dashboard)/sync/page.tsx
T
2026-06-08 22:37:54 +02:00

223 lines
7.9 KiB
TypeScript

'use client'
import { useState, useCallback } from 'react'
import { Header } from '@/components/layout/Header'
import { SyncStepper } from '@/components/sync/SyncStepper'
import { DiffTable } from '@/components/sync/DiffTable'
import { SyncProgress } from '@/components/sync/SyncProgress'
import { SyncHistory } from '@/components/sync/SyncHistory'
import type { DiffResult, SyncChange, SyncHistoryEntry } from '@/types/sync'
type Step = 1 | 2 | 3 | 4
export default function SyncPage() {
const [step, setStep] = useState<Step>(1)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [diff, setDiff] = useState<DiffResult | null>(null)
const [history, setHistory] = useState<SyncHistoryEntry[]>([])
const [historyLoaded, setHistoryLoaded] = useState(false)
// Log streaming state
const [log, setLog] = useState<string[]>([])
const [progress, setProgress] = useState<{ current: number; total: number } | null>(null)
const [summary, setSummary] = useState<{
created: number; updated: number; deactivated: number; errors: number; status: string
} | null>(null)
// Étape 1 → 2 : lancer le preview
const handleAnalyze = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/sync/preview')
const data = await res.json()
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
setDiff(data)
setStep(2)
} catch (err) {
setError((err as Error).message)
} finally {
setLoading(false)
}
}, [])
// Étape 3 → 4 : lancer la sync en streaming
const handleExecute = useCallback(async () => {
if (!diff) return
setStep(4)
setLog([])
setProgress(null)
setSummary(null)
const actionableChanges: SyncChange[] = diff.changes.filter((c) => c.type !== 'unchanged')
try {
const res = await fetch('/api/sync/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changes: actionableChanges }),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || `HTTP ${res.status}`)
}
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.filter(Boolean)) {
try {
const event = JSON.parse(line)
if (event.type === 'progress') {
setProgress({ current: event.current, total: event.total })
setLog((prev) => [...prev, event.message])
} else if (event.type === 'done') {
setSummary(event.summary)
setHistoryLoaded(false) // force reload history
}
} catch { /* ligne incomplète */ }
}
}
} catch (err) {
setLog((prev) => [...prev, `❌ Erreur : ${(err as Error).message}`])
}
}, [diff])
// Charger l'historique au besoin
const loadHistory = useCallback(async () => {
if (historyLoaded) return
try {
const res = await fetch('/api/sync/history')
if (res.ok) {
setHistory(await res.json())
setHistoryLoaded(true)
}
} catch { /* silencieux */ }
}, [historyLoaded])
const actionableCount = diff
? diff.summary.create + diff.summary.update + diff.summary.deactivate
: 0
return (
<>
<Header title="🔄 Sync Sheets" />
<div className="p-6 flex flex-col gap-6 flex-1 overflow-y-auto">
{/* Stepper */}
<SyncStepper currentStep={step} />
{/* Étape 1 — Analyser */}
{step === 1 && (
<div className="flex flex-col items-center gap-4 py-8">
<p className="text-slate-400 text-sm text-center max-w-md">
Lit Google Sheets et compare avec le catalogue Shopify actuel pour générer l&apos;aperçu des changements.
</p>
{error && (
<div className="bg-red-950 border border-red-800 text-red-300 text-sm rounded-lg px-4 py-2 max-w-md">
{error}
</div>
)}
<button
onClick={handleAnalyze}
disabled={loading}
className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white font-semibold px-6 py-3 rounded-xl text-sm transition-colors"
>
{loading ? '⏳ Analyse en cours…' : '🔍 Analyser le Sheets'}
</button>
</div>
)}
{/* Étape 2 — Aperçu diff */}
{step === 2 && diff && (
<div className="flex flex-col gap-4">
<DiffTable diff={diff} />
<div className="flex gap-3 justify-end">
<button
onClick={() => { setStep(1); setDiff(null) }}
className="px-4 py-2 text-sm bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg"
>
Relancer l&apos;analyse
</button>
<button
onClick={() => setStep(3)}
disabled={actionableCount === 0}
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 text-white font-semibold rounded-lg"
>
Confirmer
</button>
</div>
</div>
)}
{/* Étape 3 — Confirmation */}
{step === 3 && diff && (
<div className="flex flex-col items-center gap-6 py-8">
<div className="bg-slate-900 border border-slate-700 rounded-xl p-6 text-center max-w-sm">
<p className="text-lg font-semibold text-white mb-2">
{actionableCount} changement{actionableCount > 1 ? 's' : ''} vont être appliqués à Shopify
</p>
<p className="text-sm text-slate-400">
{diff.summary.create > 0 && `${diff.summary.create} créations · `}
{diff.summary.update > 0 && `${diff.summary.update} mises à jour · `}
{diff.summary.deactivate > 0 && `${diff.summary.deactivate} désactivations`}
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setStep(2)}
className="px-4 py-2 text-sm bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg"
>
Annuler
</button>
<button
onClick={handleExecute}
className="px-6 py-2 text-sm bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg"
>
Confirmer et synchroniser
</button>
</div>
</div>
)}
{/* Étape 4 — Progression */}
{step === 4 && (
<div className="flex flex-col gap-4">
<SyncProgress log={log} progress={progress} summary={summary} />
{summary && (
<div className="flex gap-3 justify-end">
<button
onClick={() => { setStep(1); setDiff(null); setSummary(null); setLog([]) }}
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg"
>
Nouvelle sync
</button>
</div>
)}
</div>
)}
{/* Historique */}
<div className="mt-4">
<button
onClick={loadHistory}
className="text-sm text-slate-500 hover:text-slate-300 mb-3"
>
{historyLoaded ? '📋 Historique des syncs' : '📋 Charger l\'historique'}
</button>
{historyLoaded && <SyncHistory entries={history} />}
</div>
</div>
</>
)
}