From 23aa78d3dac3b6b907a0c1ce316c0e56987f907c Mon Sep 17 00:00:00 2001 From: Mathieu Date: Thu, 11 Jun 2026 23:31:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20s=C3=A9lecteur=20de=20famille=20(onglet?= =?UTF-8?q?=20Sheets)=20en=20sync=20et=20cr=C3=A9ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/sheets/tabs : liste les onglets Google Sheets disponibles - readSheetProducts(tab?) et fetchPendingProducts(set, tab?) acceptent un filtre - /api/sync/preview?tab=xxx et /api/creation/preview?tab=xxx filtrent sur un onglet - Composant FamilySelector : grille de boutons chargeant les onglets dynamiquement - Page Sync : étape 0 de sélection famille avant l'analyse - Page Création : étape famille avant analyse, badge de famille sélectionnée Co-Authored-By: Claude Sonnet 4.6 --- src/app/(dashboard)/creation/page.tsx | 44 +++++++++++++++--- src/app/(dashboard)/sync/page.tsx | 36 ++++++++++++--- src/app/api/creation/preview/route.ts | 6 +-- src/app/api/sheets/tabs/route.ts | 31 +++++++++++++ src/app/api/sync/preview/route.ts | 6 +-- src/components/common/FamilySelector.tsx | 59 ++++++++++++++++++++++++ src/lib/creationSheets.ts | 22 +++++---- src/lib/googleSheets.ts | 6 +-- 8 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 src/app/api/sheets/tabs/route.ts create mode 100644 src/components/common/FamilySelector.tsx diff --git a/src/app/(dashboard)/creation/page.tsx b/src/app/(dashboard)/creation/page.tsx index f98bf1e..2f44eff 100644 --- a/src/app/(dashboard)/creation/page.tsx +++ b/src/app/(dashboard)/creation/page.tsx @@ -4,12 +4,14 @@ 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 { FamilySelector } from '@/components/common/FamilySelector' import type { PendingProduct, MetafieldResolution, CreationStreamEvent } from '@/types/creation' -type Step = 'analyse' | 'metafields' | 'confirmation' | 'execution' +type Step = 'famille' | 'analyse' | 'metafields' | 'confirmation' | 'execution' export default function CreationPage() { - const [step, setStep] = useState('analyse') + const [step, setStep] = useState('famille') + const [selectedTab, setSelectedTab] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [byTab, setByTab] = useState>({}) @@ -19,11 +21,19 @@ export default function CreationPage() { const [events, setEvents] = useState([]) const [done, setDone] = useState(false) + const handleSelectTab = (tab: string) => { + setSelectedTab(tab) + setStep('analyse') + } + const analyse = async () => { setLoading(true) setError(null) try { - const res = await fetch('/api/creation/preview') + const url = selectedTab + ? `/api/creation/preview?tab=${encodeURIComponent(selectedTab)}` + : '/api/creation/preview' + const res = await fetch(url) const data = await res.json() if (data.error) throw new Error(data.error) setByTab(data.byTab) @@ -78,6 +88,15 @@ export default function CreationPage() { ] const currentIdx = STEPS.findIndex(s => s.id === step) + const reset = () => { + setStep('famille') + setSelectedTab(null) + setEvents([]) + setDone(false) + setByTab({}) + setTotal(0) + } + return ( <>
@@ -93,13 +112,26 @@ export default function CreationPage() { ))} -
+ {/* Étape 0 — Sélection famille */} + {step === 'famille' && ( +
+

Choisir une famille

+ +
+ )} + +
{error &&
{error}
} {step === 'analyse' && (
+
+ 📂 Famille : + {selectedTab} + +

- Lit tous les onglets du Google Sheets et détecte les lignes où Publié=1 et ID vide. + Détecte les lignes de {selectedTab}Publié=1 et ID vide.

diff --git a/src/app/(dashboard)/sync/page.tsx b/src/app/(dashboard)/sync/page.tsx index c82f3e6..1d37557 100644 --- a/src/app/(dashboard)/sync/page.tsx +++ b/src/app/(dashboard)/sync/page.tsx @@ -5,12 +5,14 @@ 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 { FamilySelector } from '@/components/common/FamilySelector' import type { DiffResult, SyncChange, SyncHistoryEntry } from '@/types/sync' -type Step = 1 | 2 | 3 | 4 +type Step = 0 | 1 | 2 | 3 | 4 export default function SyncPage() { - const [step, setStep] = useState(1) + const [step, setStep] = useState(0) + const [selectedTab, setSelectedTab] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [diff, setDiff] = useState(null) @@ -24,12 +26,19 @@ export default function SyncPage() { created: number; updated: number; deactivated: number; errors: number; status: string } | null>(null) + // Étape 0 → 1 : sélection famille + const handleSelectTab = useCallback((tab: string) => { + setSelectedTab(tab) + setStep(1) + }, []) + // Étape 1 → 2 : lancer le preview const handleAnalyze = useCallback(async () => { setLoading(true) setError(null) try { - const res = await fetch('/api/sync/preview') + const url = selectedTab ? `/api/sync/preview?tab=${encodeURIComponent(selectedTab)}` : '/api/sync/preview' + const res = await fetch(url) const data = await res.json() if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) setDiff(data) @@ -39,7 +48,7 @@ export default function SyncPage() { } finally { setLoading(false) } - }, []) + }, [selectedTab]) // Étape 3 → 4 : lancer la sync en streaming const handleExecute = useCallback(async () => { @@ -114,13 +123,26 @@ export default function SyncPage() {
{/* Stepper */} - + 0 ? step : 1} /> + + {/* Étape 0 — Sélection famille */} + {step === 0 && ( +
+

Choisir une famille

+ +
+ )} {/* Étape 1 — Analyser */} {step === 1 && (
+
+ 📂 Famille : + {selectedTab} + +

- Lit Google Sheets et compare avec le catalogue Shopify actuel pour générer l'aperçu des changements. + Compare la famille {selectedTab} avec le catalogue Shopify.

{error && (
@@ -196,7 +218,7 @@ export default function SyncPage() { {summary && (
+ ))} +
+
+ ) +} diff --git a/src/lib/creationSheets.ts b/src/lib/creationSheets.ts index 84c94c6..a739db9 100644 --- a/src/lib/creationSheets.ts +++ b/src/lib/creationSheets.ts @@ -82,19 +82,25 @@ export function parseTabRows(tab: string, headers: string[], rows: string[][], d export async function fetchPendingProducts( alreadyCreatedRows: Set, + tabFilter?: string, ): Promise { const apiKey = process.env.GOOGLE_API_KEY const sheetId = process.env.GOOGLE_SHEETS_ID if (!apiKey || !sheetId) throw new Error('GOOGLE_API_KEY et GOOGLE_SHEETS_ID requis') - const metaRes = await fetch( - `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`, - ) - if (!metaRes.ok) throw new Error(`Sheets metadata error: ${metaRes.status}`) - const meta = await metaRes.json() - const tabs: string[] = (meta.sheets as Array<{ properties: { title: string } }>).map( - s => s.properties.title, - ) + let tabs: string[] + if (tabFilter) { + tabs = [tabFilter] + } else { + const metaRes = await fetch( + `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`, + ) + if (!metaRes.ok) throw new Error(`Sheets metadata error: ${metaRes.status}`) + const meta = await metaRes.json() + tabs = (meta.sheets as Array<{ properties: { title: string } }>).map( + s => s.properties.title, + ) + } const all: PendingProduct[] = [] for (const tab of tabs) { diff --git a/src/lib/googleSheets.ts b/src/lib/googleSheets.ts index 25bc8fa..78eff5f 100644 --- a/src/lib/googleSheets.ts +++ b/src/lib/googleSheets.ts @@ -52,16 +52,16 @@ export function parseSheetRows(rows: string[][]): SyncProduct[] { * Sinon, lit tous les onglets du fichier et combine les produits. * Le fichier Sheets doit être partagé en lecture ("Toute personne avec le lien"). */ -export async function readSheetProducts(): Promise { +export async function readSheetProducts(tab?: string): Promise { const apiKey = process.env.GOOGLE_API_KEY const sheetId = process.env.GOOGLE_SHEETS_ID - const singleTab = process.env.GOOGLE_SHEETS_TAB + const envTab = process.env.GOOGLE_SHEETS_TAB if (!apiKey || !sheetId) { throw new Error('Variables GOOGLE_API_KEY et GOOGLE_SHEETS_ID requises') } - const sheetNames = singleTab ? [singleTab] : await listSheetTabs(sheetId, apiKey) + const sheetNames = tab ? [tab] : envTab ? [envTab] : await listSheetTabs(sheetId, apiKey) const results = await Promise.all( sheetNames.map((name) => readOneTab(sheetId, apiKey, name)),