diff --git a/src/__tests__/api/sync/preview.test.ts b/src/__tests__/api/sync/preview.test.ts index 45e18ef..278288f 100644 --- a/src/__tests__/api/sync/preview.test.ts +++ b/src/__tests__/api/sync/preview.test.ts @@ -1,5 +1,6 @@ import { GET } from '@/app/api/sync/preview/route' import { NextRequest } from 'next/server' +import type { SyncProduct, ShopifyProductFull } from '@/types/sync' jest.mock('@/lib/googleSheets', () => ({ readSheetProducts: jest.fn(), @@ -23,8 +24,8 @@ describe('GET /api/sync/preview', () => { beforeEach(() => jest.clearAllMocks()) it('retourne le DiffResult complet', async () => { - const fakeSheetProducts = [{ ref: 'REF-1', handle: 'ref-1', title: 'T', description: '', price: '10.00', stock: 1, status: 'active' }] - const fakeShopifyProducts = [] + const fakeSheetProducts: SyncProduct[] = [{ ref: 'REF-1', handle: 'ref-1', title: 'T', description: '', price: '10.00', stock: 1, status: 'active' }] + const fakeShopifyProducts: ShopifyProductFull[] = [] const fakeDiff = { changes: [{ type: 'create', ref: 'REF-1', sheetProduct: fakeSheetProducts[0] }], summary: { create: 1, update: 0, deactivate: 0, unchanged: 0 }, diff --git a/src/app/(dashboard)/sync/page.tsx b/src/app/(dashboard)/sync/page.tsx index 2116231..016011e 100644 --- a/src/app/(dashboard)/sync/page.tsx +++ b/src/app/(dashboard)/sync/page.tsx @@ -1,9 +1,222 @@ +'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(1) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [diff, setDiff] = useState(null) + const [history, setHistory] = useState([]) + const [historyLoaded, setHistoryLoaded] = useState(false) + + // Log streaming state + const [log, setLog] = useState([]) + 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 ( <>
-

Module Sync — à implémenter (Plan 3)

+ +
+ {/* Stepper */} + + + {/* Étape 1 — Analyser */} + {step === 1 && ( +
+

+ Lit Google Sheets et compare avec le catalogue Shopify actuel pour générer l'aperçu des changements. +

+ {error && ( +
+ {error} +
+ )} + +
+ )} + + {/* Étape 2 — Aperçu diff */} + {step === 2 && diff && ( +
+ +
+ + +
+
+ )} + + {/* Étape 3 — Confirmation */} + {step === 3 && diff && ( +
+
+

+ {actionableCount} changement{actionableCount > 1 ? 's' : ''} vont être appliqués à Shopify +

+

+ {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`} +

+
+
+ + +
+
+ )} + + {/* Étape 4 — Progression */} + {step === 4 && ( +
+ + {summary && ( +
+ +
+ )} +
+ )} + + {/* Historique */} +
+ + {historyLoaded && } +
+
) }