feat: clear Shopify IDs from Sheets to allow product recreation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ export default function CreationPage() {
|
||||
const [selectedTab, setSelectedTab] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [clearingIds, setClearingIds] = useState(false)
|
||||
const [clearResult, setClearResult] = useState<string | null>(null)
|
||||
const [byTab, setByTab] = useState<Record<string, PendingProduct[]>>({})
|
||||
const [total, setTotal] = useState(0)
|
||||
const [specificColumns, setSpecificColumns] = useState<string[]>([])
|
||||
@@ -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() {
|
||||
<p className="text-gray-300 text-sm">
|
||||
Détecte les lignes de <strong className="text-white">{selectedTab}</strong> où <code className="text-indigo-300">Publié=1</code> et <code className="text-indigo-300">ID vide</code>.
|
||||
</p>
|
||||
<button onClick={analyse} disabled={loading}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
{loading ? 'Analyse en cours…' : 'Analyser le Sheets'}
|
||||
</button>
|
||||
<div className="flex gap-3 flex-wrap items-center">
|
||||
<button onClick={analyse} disabled={loading}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
{loading ? 'Analyse en cours…' : 'Analyser le Sheets'}
|
||||
</button>
|
||||
<button onClick={clearSheetIds} disabled={clearingIds}
|
||||
className="px-4 py-2 bg-amber-700 hover:bg-amber-600 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
{clearingIds ? 'Nettoyage…' : '🗑 Vider les IDs Shopify du Sheets'}
|
||||
</button>
|
||||
</div>
|
||||
{clearResult && <p className="text-green-400 text-xs">{clearResult}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
|
||||
async function getAccessToken(): Promise<string | null> {
|
||||
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 })
|
||||
}
|
||||
Reference in New Issue
Block a user