From 34ade6beb2aac8adad8f2cda524c93c14c4a61e2 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Wed, 24 Jun 2026 15:05:43 +0200 Subject: [PATCH] feat: clear Shopify IDs from Sheets to allow product recreation Co-Authored-By: Claude Sonnet 4.6 --- src/app/(dashboard)/creation/page.tsx | 34 +++++++-- src/app/api/creation/clear-ids/route.ts | 93 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/app/api/creation/clear-ids/route.ts diff --git a/src/app/(dashboard)/creation/page.tsx b/src/app/(dashboard)/creation/page.tsx index 2f44eff..49aa0c5 100644 --- a/src/app/(dashboard)/creation/page.tsx +++ b/src/app/(dashboard)/creation/page.tsx @@ -14,6 +14,8 @@ export default function CreationPage() { const [selectedTab, setSelectedTab] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [clearingIds, setClearingIds] = useState(false) + const [clearResult, setClearResult] = useState(null) const [byTab, setByTab] = useState>({}) const [total, setTotal] = useState(0) const [specificColumns, setSpecificColumns] = useState([]) @@ -88,6 +90,23 @@ export default function CreationPage() { ] const currentIdx = STEPS.findIndex(s => s.id === step) + const clearSheetIds = async () => { + if (!selectedTab) return + if (!confirm(`Effacer tous les IDs Shopify dans l'onglet "${selectedTab}" ? Les produits Shopify ne seront PAS supprimés.`)) return + setClearingIds(true) + setClearResult(null) + try { + const res = await fetch(`/api/creation/clear-ids?tab=${encodeURIComponent(selectedTab)}`, { method: 'POST' }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + setClearResult(`✓ ${data.cleared} ID${data.cleared > 1 ? 's' : ''} effacé${data.cleared > 1 ? 's' : ''} dans le Sheets`) + } catch (e) { + setError(e instanceof Error ? e.message : 'Erreur') + } finally { + setClearingIds(false) + } + } + const reset = () => { setStep('famille') setSelectedTab(null) @@ -133,10 +152,17 @@ export default function CreationPage() {

Détecte les lignes de {selectedTab}Publié=1 et ID vide.

- +
+ + +
+ {clearResult &&

{clearResult}

} )} diff --git a/src/app/api/creation/clear-ids/route.ts b/src/app/api/creation/clear-ids/route.ts new file mode 100644 index 0000000..af230ae --- /dev/null +++ b/src/app/api/creation/clear-ids/route.ts @@ -0,0 +1,93 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' + +async function getAccessToken(): Promise { + const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL + const rawKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY + if (!email || !rawKey) return null + + const privateKey = rawKey.replace(/\\n/g, '\n') + const now = Math.floor(Date.now() / 1000) + const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url') + const payload = Buffer.from(JSON.stringify({ + iss: email, + scope: 'https://www.googleapis.com/auth/spreadsheets', + aud: 'https://oauth2.googleapis.com/token', + iat: now, + exp: now + 3600, + })).toString('base64url') + + const { createSign } = await import('crypto') + const sign = createSign('RSA-SHA256') + sign.update(`${header}.${payload}`) + const signature = sign.sign(privateKey, 'base64url') + const jwt = `${header}.${payload}.${signature}` + + const tokenRes = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt }), + }) + if (!tokenRes.ok) return null + const { access_token } = await tokenRes.json() as { access_token: string } + return access_token +} + +// POST /api/creation/clear-ids?tab=SOL CARRELAGE +// Efface la colonne A (ID Shopify) de toutes les lignes produit d'un onglet +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }) + + const tab = new URL(req.url).searchParams.get('tab') + if (!tab) return NextResponse.json({ error: 'tab requis' }, { status: 400 }) + + const apiKey = process.env.GOOGLE_API_KEY + const sheetId = process.env.GOOGLE_SHEETS_ID + if (!apiKey || !sheetId) return NextResponse.json({ error: 'Config Google manquante' }, { status: 500 }) + + // Lire les données de l'onglet + const range = encodeURIComponent(`${tab}!A1:A500`) + const res = await fetch( + `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?key=${apiKey}`, + ) + if (!res.ok) return NextResponse.json({ error: `Sheets read error: ${res.status}` }, { status: 502 }) + + const data = await res.json() as { values?: string[][] } + const rows = data.values ?? [] + + // Trouver la ligne d'en-tête (contient "ID" en col A) + const headerRowIdx = rows.findIndex(r => (r[0] ?? '').trim() === 'ID') + if (headerRowIdx === -1) return NextResponse.json({ error: 'Onglet non reconnu (pas de colonne ID)' }, { status: 400 }) + + // Identifier les lignes produit qui ont un ID Shopify (numérique) + const clearValues: string[][] = [] + const startRow = headerRowIdx + 2 // 1-indexed, skip header row + for (let i = headerRowIdx + 1; i < rows.length; i++) { + const cell = (rows[i]?.[0] ?? '').trim() + clearValues.push([cell && /^\d+$/.test(cell) ? '' : cell]) + } + + if (clearValues.length === 0) return NextResponse.json({ cleared: 0 }) + + const accessToken = await getAccessToken() + if (!accessToken) return NextResponse.json({ error: 'Impossible d\'obtenir le token Google' }, { status: 500 }) + + const writeRange = `${tab}!A${startRow}:A${startRow + clearValues.length - 1}` + const writeRes = await fetch( + `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${encodeURIComponent(writeRange)}?valueInputOption=RAW`, + { + method: 'PUT', + headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ range: writeRange, majorDimension: 'ROWS', values: clearValues }), + }, + ) + if (!writeRes.ok) { + const err = await writeRes.text() + return NextResponse.json({ error: `Sheets write error: ${err}` }, { status: 502 }) + } + + const cleared = clearValues.filter(r => r[0] === '').length + return NextResponse.json({ cleared, tab }) +}