feat: sélecteur de famille (onglet Sheets) en sync et création
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,14 @@ import { Header } from '@/components/layout/Header'
|
|||||||
import { PendingRowsTable } from '@/components/creation/PendingRowsTable'
|
import { PendingRowsTable } from '@/components/creation/PendingRowsTable'
|
||||||
import { MetafieldResolver } from '@/components/creation/MetafieldResolver'
|
import { MetafieldResolver } from '@/components/creation/MetafieldResolver'
|
||||||
import { CreationProgress } from '@/components/creation/CreationProgress'
|
import { CreationProgress } from '@/components/creation/CreationProgress'
|
||||||
|
import { FamilySelector } from '@/components/common/FamilySelector'
|
||||||
import type { PendingProduct, MetafieldResolution, CreationStreamEvent } from '@/types/creation'
|
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() {
|
export default function CreationPage() {
|
||||||
const [step, setStep] = useState<Step>('analyse')
|
const [step, setStep] = useState<Step>('famille')
|
||||||
|
const [selectedTab, setSelectedTab] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [byTab, setByTab] = useState<Record<string, PendingProduct[]>>({})
|
const [byTab, setByTab] = useState<Record<string, PendingProduct[]>>({})
|
||||||
@@ -19,11 +21,19 @@ export default function CreationPage() {
|
|||||||
const [events, setEvents] = useState<CreationStreamEvent[]>([])
|
const [events, setEvents] = useState<CreationStreamEvent[]>([])
|
||||||
const [done, setDone] = useState(false)
|
const [done, setDone] = useState(false)
|
||||||
|
|
||||||
|
const handleSelectTab = (tab: string) => {
|
||||||
|
setSelectedTab(tab)
|
||||||
|
setStep('analyse')
|
||||||
|
}
|
||||||
|
|
||||||
const analyse = async () => {
|
const analyse = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
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()
|
const data = await res.json()
|
||||||
if (data.error) throw new Error(data.error)
|
if (data.error) throw new Error(data.error)
|
||||||
setByTab(data.byTab)
|
setByTab(data.byTab)
|
||||||
@@ -78,6 +88,15 @@ export default function CreationPage() {
|
|||||||
]
|
]
|
||||||
const currentIdx = STEPS.findIndex(s => s.id === step)
|
const currentIdx = STEPS.findIndex(s => s.id === step)
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setStep('famille')
|
||||||
|
setSelectedTab(null)
|
||||||
|
setEvents([])
|
||||||
|
setDone(false)
|
||||||
|
setByTab({})
|
||||||
|
setTotal(0)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title="Création produits" />
|
<Header title="Création produits" />
|
||||||
@@ -93,13 +112,26 @@ export default function CreationPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-800 rounded-xl p-6">
|
{/* Étape 0 — Sélection famille */}
|
||||||
|
{step === 'famille' && (
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6">
|
||||||
|
<h2 className="text-sm font-semibold text-white mb-4">Choisir une famille</h2>
|
||||||
|
<FamilySelector onSelect={handleSelectTab} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6" style={{ display: step === 'famille' ? 'none' : undefined }}>
|
||||||
{error && <div className="mb-4 p-3 bg-red-900/30 border border-red-700 rounded-lg text-red-300 text-sm">{error}</div>}
|
{error && <div className="mb-4 p-3 bg-red-900/30 border border-red-700 rounded-lg text-red-300 text-sm">{error}</div>}
|
||||||
|
|
||||||
{step === 'analyse' && (
|
{step === 'analyse' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 bg-indigo-950/50 border border-indigo-800/50 rounded-lg px-3 py-2 w-fit">
|
||||||
|
<span className="text-indigo-400 text-sm">📂 Famille :</span>
|
||||||
|
<span className="text-white text-sm font-semibold">{selectedTab}</span>
|
||||||
|
<button onClick={() => { setStep('famille'); setSelectedTab(null) }} className="ml-2 text-xs text-slate-400 hover:text-slate-200">changer</button>
|
||||||
|
</div>
|
||||||
<p className="text-gray-300 text-sm">
|
<p className="text-gray-300 text-sm">
|
||||||
Lit tous les onglets du Google Sheets et détecte les lignes où <code className="text-indigo-300">Publié=1</code> et <code className="text-indigo-300">ID vide</code>.
|
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>
|
</p>
|
||||||
<button onClick={analyse} disabled={loading}
|
<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">
|
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">
|
||||||
@@ -152,7 +184,7 @@ export default function CreationPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<CreationProgress events={events} done={done} />
|
<CreationProgress events={events} done={done} />
|
||||||
{done && (
|
{done && (
|
||||||
<button onClick={() => { setStep('analyse'); setEvents([]); setDone(false); setByTab({}); setTotal(0) }}
|
<button onClick={reset}
|
||||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors">
|
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||||
Nouvelle session
|
Nouvelle session
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { SyncStepper } from '@/components/sync/SyncStepper'
|
|||||||
import { DiffTable } from '@/components/sync/DiffTable'
|
import { DiffTable } from '@/components/sync/DiffTable'
|
||||||
import { SyncProgress } from '@/components/sync/SyncProgress'
|
import { SyncProgress } from '@/components/sync/SyncProgress'
|
||||||
import { SyncHistory } from '@/components/sync/SyncHistory'
|
import { SyncHistory } from '@/components/sync/SyncHistory'
|
||||||
|
import { FamilySelector } from '@/components/common/FamilySelector'
|
||||||
import type { DiffResult, SyncChange, SyncHistoryEntry } from '@/types/sync'
|
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() {
|
export default function SyncPage() {
|
||||||
const [step, setStep] = useState<Step>(1)
|
const [step, setStep] = useState<Step>(0)
|
||||||
|
const [selectedTab, setSelectedTab] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [diff, setDiff] = useState<DiffResult | null>(null)
|
const [diff, setDiff] = useState<DiffResult | null>(null)
|
||||||
@@ -24,12 +26,19 @@ export default function SyncPage() {
|
|||||||
created: number; updated: number; deactivated: number; errors: number; status: string
|
created: number; updated: number; deactivated: number; errors: number; status: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
// Étape 0 → 1 : sélection famille
|
||||||
|
const handleSelectTab = useCallback((tab: string) => {
|
||||||
|
setSelectedTab(tab)
|
||||||
|
setStep(1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Étape 1 → 2 : lancer le preview
|
// Étape 1 → 2 : lancer le preview
|
||||||
const handleAnalyze = useCallback(async () => {
|
const handleAnalyze = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
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()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
||||||
setDiff(data)
|
setDiff(data)
|
||||||
@@ -39,7 +48,7 @@ export default function SyncPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [selectedTab])
|
||||||
|
|
||||||
// Étape 3 → 4 : lancer la sync en streaming
|
// Étape 3 → 4 : lancer la sync en streaming
|
||||||
const handleExecute = useCallback(async () => {
|
const handleExecute = useCallback(async () => {
|
||||||
@@ -114,13 +123,26 @@ export default function SyncPage() {
|
|||||||
|
|
||||||
<div className="p-6 flex flex-col gap-6 flex-1 overflow-y-auto">
|
<div className="p-6 flex flex-col gap-6 flex-1 overflow-y-auto">
|
||||||
{/* Stepper */}
|
{/* Stepper */}
|
||||||
<SyncStepper currentStep={step} />
|
<SyncStepper currentStep={step > 0 ? step : 1} />
|
||||||
|
|
||||||
|
{/* Étape 0 — Sélection famille */}
|
||||||
|
{step === 0 && (
|
||||||
|
<div className="bg-slate-800/50 border border-slate-700 rounded-xl p-6">
|
||||||
|
<h2 className="text-sm font-semibold text-white mb-4">Choisir une famille</h2>
|
||||||
|
<FamilySelector onSelect={handleSelectTab} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Étape 1 — Analyser */}
|
{/* Étape 1 — Analyser */}
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="flex flex-col items-center gap-4 py-8">
|
<div className="flex flex-col items-center gap-4 py-8">
|
||||||
|
<div className="flex items-center gap-2 bg-indigo-950/50 border border-indigo-800/50 rounded-lg px-4 py-2">
|
||||||
|
<span className="text-indigo-400 text-sm">📂 Famille :</span>
|
||||||
|
<span className="text-white text-sm font-semibold">{selectedTab}</span>
|
||||||
|
<button onClick={() => { setStep(0); setSelectedTab(null) }} className="ml-2 text-xs text-slate-400 hover:text-slate-200">changer</button>
|
||||||
|
</div>
|
||||||
<p className="text-slate-400 text-sm text-center max-w-md">
|
<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'aperçu des changements.
|
Compare la famille <strong className="text-white">{selectedTab}</strong> avec le catalogue Shopify.
|
||||||
</p>
|
</p>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-950 border border-red-800 text-red-300 text-sm rounded-lg px-4 py-2 max-w-md">
|
<div className="bg-red-950 border border-red-800 text-red-300 text-sm rounded-lg px-4 py-2 max-w-md">
|
||||||
@@ -196,7 +218,7 @@ export default function SyncPage() {
|
|||||||
{summary && (
|
{summary && (
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setStep(1); setDiff(null); setSummary(null); setLog([]) }}
|
onClick={() => { setStep(0); setSelectedTab(null); 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"
|
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg"
|
||||||
>
|
>
|
||||||
↺ Nouvelle sync
|
↺ Nouvelle sync
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { fetchPendingProducts } from '@/lib/creationSheets'
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import type { PendingProduct } from '@/types/creation'
|
import type { PendingProduct } from '@/types/creation'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export async function GET(req: NextRequest) {
|
||||||
export async function GET(_req: NextRequest) {
|
|
||||||
const session = await getServerSession(authOptions)
|
const session = await getServerSession(authOptions)
|
||||||
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
|
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
|
||||||
|
const tab = new URL(req.url).searchParams.get('tab') ?? undefined
|
||||||
try {
|
try {
|
||||||
const existing = await prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } })
|
const existing = await prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } })
|
||||||
const alreadyCreated = new Set(existing.map(e => `${e.sheetTab}:${e.sheetRow}`))
|
const alreadyCreated = new Set(existing.map(e => `${e.sheetTab}:${e.sheetRow}`))
|
||||||
const pending = await fetchPendingProducts(alreadyCreated)
|
const pending = await fetchPendingProducts(alreadyCreated, tab)
|
||||||
const byTab: Record<string, PendingProduct[]> = {}
|
const byTab: Record<string, PendingProduct[]> = {}
|
||||||
const specificColumns = new Set<string>()
|
const specificColumns = new Set<string>()
|
||||||
for (const p of pending) {
|
for (const p of pending) {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
|
||||||
|
|
||||||
|
const apiKey = process.env.GOOGLE_API_KEY
|
||||||
|
const sheetId = process.env.GOOGLE_SHEETS_ID
|
||||||
|
if (!apiKey || !sheetId) {
|
||||||
|
return NextResponse.json({ error: 'GOOGLE_API_KEY et GOOGLE_SHEETS_ID requis' }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`
|
||||||
|
const res = await fetch(url, { cache: 'no-store' })
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text()
|
||||||
|
throw new Error(`Google Sheets API error: ${res.status} — ${err}`)
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
const tabs: string[] = (data.sheets as Array<{ properties: { title: string } }>).map(
|
||||||
|
(s) => s.properties.title,
|
||||||
|
)
|
||||||
|
return NextResponse.json({ tabs })
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ import { readSheetProducts } from '@/lib/googleSheets'
|
|||||||
import { fetchAllShopifyProducts } from '@/lib/shopifySync'
|
import { fetchAllShopifyProducts } from '@/lib/shopifySync'
|
||||||
import { computeDiff } from '@/lib/syncDiff'
|
import { computeDiff } from '@/lib/syncDiff'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export async function GET(req: NextRequest) {
|
||||||
export async function GET(_req: NextRequest) {
|
const tab = new URL(req.url).searchParams.get('tab') ?? undefined
|
||||||
try {
|
try {
|
||||||
const [sheetProducts, shopifyProducts] = await Promise.all([
|
const [sheetProducts, shopifyProducts] = await Promise.all([
|
||||||
readSheetProducts(),
|
readSheetProducts(tab),
|
||||||
fetchAllShopifyProducts(),
|
fetchAllShopifyProducts(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSelect: (tab: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FamilySelector({ onSelect }: Props) {
|
||||||
|
const [tabs, setTabs] = useState<string[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/sheets/tabs')
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
setTabs(data.tabs)
|
||||||
|
})
|
||||||
|
.catch((e: Error) => setError(e.message))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-slate-400 text-sm py-8 justify-center">
|
||||||
|
<span className="animate-spin">⏳</span> Chargement des familles…
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-red-900/30 border border-red-700 rounded-lg text-red-300 text-sm">
|
||||||
|
Impossible de charger les familles : {error}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-slate-300 text-sm">
|
||||||
|
Choisissez une famille (onglet Google Sheets) pour travailler uniquement sur celle-ci.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
onClick={() => onSelect(tab)}
|
||||||
|
className="flex items-center gap-2 px-4 py-3 bg-slate-700 hover:bg-indigo-700 border border-slate-600 hover:border-indigo-500 rounded-xl text-sm text-slate-200 hover:text-white font-medium transition-all text-left group"
|
||||||
|
>
|
||||||
|
<span className="text-base group-hover:scale-110 transition-transform">📂</span>
|
||||||
|
<span className="truncate">{tab}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -82,19 +82,25 @@ export function parseTabRows(tab: string, headers: string[], rows: string[][], d
|
|||||||
|
|
||||||
export async function fetchPendingProducts(
|
export async function fetchPendingProducts(
|
||||||
alreadyCreatedRows: Set<string>,
|
alreadyCreatedRows: Set<string>,
|
||||||
|
tabFilter?: string,
|
||||||
): Promise<PendingProduct[]> {
|
): Promise<PendingProduct[]> {
|
||||||
const apiKey = process.env.GOOGLE_API_KEY
|
const apiKey = process.env.GOOGLE_API_KEY
|
||||||
const sheetId = process.env.GOOGLE_SHEETS_ID
|
const sheetId = process.env.GOOGLE_SHEETS_ID
|
||||||
if (!apiKey || !sheetId) throw new Error('GOOGLE_API_KEY et GOOGLE_SHEETS_ID requis')
|
if (!apiKey || !sheetId) throw new Error('GOOGLE_API_KEY et GOOGLE_SHEETS_ID requis')
|
||||||
|
|
||||||
const metaRes = await fetch(
|
let tabs: string[]
|
||||||
`https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`,
|
if (tabFilter) {
|
||||||
)
|
tabs = [tabFilter]
|
||||||
if (!metaRes.ok) throw new Error(`Sheets metadata error: ${metaRes.status}`)
|
} else {
|
||||||
const meta = await metaRes.json()
|
const metaRes = await fetch(
|
||||||
const tabs: string[] = (meta.sheets as Array<{ properties: { title: string } }>).map(
|
`https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`,
|
||||||
s => s.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[] = []
|
const all: PendingProduct[] = []
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
|
|||||||
@@ -52,16 +52,16 @@ export function parseSheetRows(rows: string[][]): SyncProduct[] {
|
|||||||
* Sinon, lit tous les onglets du fichier et combine les produits.
|
* Sinon, lit tous les onglets du fichier et combine les produits.
|
||||||
* Le fichier Sheets doit être partagé en lecture ("Toute personne avec le lien").
|
* Le fichier Sheets doit être partagé en lecture ("Toute personne avec le lien").
|
||||||
*/
|
*/
|
||||||
export async function readSheetProducts(): Promise<SyncProduct[]> {
|
export async function readSheetProducts(tab?: string): Promise<SyncProduct[]> {
|
||||||
const apiKey = process.env.GOOGLE_API_KEY
|
const apiKey = process.env.GOOGLE_API_KEY
|
||||||
const sheetId = process.env.GOOGLE_SHEETS_ID
|
const sheetId = process.env.GOOGLE_SHEETS_ID
|
||||||
const singleTab = process.env.GOOGLE_SHEETS_TAB
|
const envTab = process.env.GOOGLE_SHEETS_TAB
|
||||||
|
|
||||||
if (!apiKey || !sheetId) {
|
if (!apiKey || !sheetId) {
|
||||||
throw new Error('Variables GOOGLE_API_KEY et GOOGLE_SHEETS_ID requises')
|
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(
|
const results = await Promise.all(
|
||||||
sheetNames.map((name) => readOneTab(sheetId, apiKey, name)),
|
sheetNames.map((name) => readOneTab(sheetId, apiKey, name)),
|
||||||
|
|||||||
Reference in New Issue
Block a user