feat: détection drift Sheets sur le dashboard (approche A)
- /api/dashboard/stats appelle Google Drive API pour récupérer modifiedTime du Sheets - Compare avec createdAt de la dernière SyncRun → sheetDrift: boolean - Dashboard affiche un bandeau amber cliquable si Sheets plus récent que la dernière sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ interface LastSync {
|
||||
interface DashboardStats {
|
||||
products: ProductStats
|
||||
lastSync: LastSync | null
|
||||
sheetDrift: boolean
|
||||
sheetModifiedAt: string | null
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
@@ -134,6 +136,22 @@ export default function DashboardPage() {
|
||||
|
||||
<div className="p-6 flex flex-col gap-8 flex-1 overflow-y-auto">
|
||||
|
||||
{/* ── Alerte drift Sheets ───────────────────────────────── */}
|
||||
{!statsLoading && stats?.sheetDrift && (
|
||||
<Link href="/sync" className="flex items-center gap-3 bg-amber-950/40 border border-amber-600/50 rounded-xl px-4 py-3 hover:bg-amber-950/60 transition-colors group">
|
||||
<span className="text-xl">⚠️</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-amber-300">Google Sheets modifié depuis la dernière sync</p>
|
||||
<p className="text-xs text-amber-500 mt-0.5">
|
||||
{stats.sheetModifiedAt
|
||||
? `Dernière modification : ${formatDate(stats.sheetModifiedAt)}`
|
||||
: 'Des changements sont peut-être en attente de synchronisation'}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-amber-400 group-hover:text-amber-300 whitespace-nowrap">Lancer la sync →</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* ── KPIs ─────────────────────────────────────────────── */}
|
||||
<section>
|
||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Vue d'ensemble</h2>
|
||||
|
||||
@@ -43,18 +43,36 @@ async function fetchProductsLight(status: 'active' | 'draft'): Promise<LightProd
|
||||
return all
|
||||
}
|
||||
|
||||
async function fetchSheetModifiedTime(): Promise<string | null> {
|
||||
const apiKey = process.env.GOOGLE_API_KEY
|
||||
const sheetId = process.env.GOOGLE_SHEETS_ID
|
||||
if (!apiKey || !sheetId) return null
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${sheetId}?fields=modifiedTime&key=${apiKey}`,
|
||||
{ cache: 'no-store' },
|
||||
)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
return (data.modifiedTime as string) ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
|
||||
|
||||
try {
|
||||
const [activeProducts, draftProducts, lastSyncRun] = await Promise.all([
|
||||
const [activeProducts, draftProducts, lastSyncRun, sheetModifiedTime] = await Promise.all([
|
||||
fetchProductsLight('active'),
|
||||
fetchProductsLight('draft'),
|
||||
prisma.syncRun.findFirst({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { user: { select: { name: true } } },
|
||||
}),
|
||||
fetchSheetModifiedTime(),
|
||||
])
|
||||
|
||||
const allProducts = [...activeProducts, ...draftProducts]
|
||||
@@ -72,6 +90,12 @@ export async function GET() {
|
||||
}
|
||||
: null
|
||||
|
||||
// Drift : Sheets modifié après la dernière sync réussie
|
||||
const sheetDrift =
|
||||
sheetModifiedTime !== null &&
|
||||
lastSyncRun !== null &&
|
||||
new Date(sheetModifiedTime) > lastSyncRun.createdAt
|
||||
|
||||
return NextResponse.json({
|
||||
products: {
|
||||
active: activeProducts.length,
|
||||
@@ -80,6 +104,8 @@ export async function GET() {
|
||||
withoutImages,
|
||||
},
|
||||
lastSync,
|
||||
sheetDrift,
|
||||
sheetModifiedAt: sheetModifiedTime,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||
|
||||
Reference in New Issue
Block a user