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 {
|
interface DashboardStats {
|
||||||
products: ProductStats
|
products: ProductStats
|
||||||
lastSync: LastSync | null
|
lastSync: LastSync | null
|
||||||
|
sheetDrift: boolean
|
||||||
|
sheetModifiedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({
|
function StatCard({
|
||||||
@@ -134,6 +136,22 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<div className="p-6 flex flex-col gap-8 flex-1 overflow-y-auto">
|
<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 ─────────────────────────────────────────────── */}
|
{/* ── KPIs ─────────────────────────────────────────────── */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Vue d'ensemble</h2>
|
<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
|
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() {
|
export async function GET() {
|
||||||
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 })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [activeProducts, draftProducts, lastSyncRun] = await Promise.all([
|
const [activeProducts, draftProducts, lastSyncRun, sheetModifiedTime] = await Promise.all([
|
||||||
fetchProductsLight('active'),
|
fetchProductsLight('active'),
|
||||||
fetchProductsLight('draft'),
|
fetchProductsLight('draft'),
|
||||||
prisma.syncRun.findFirst({
|
prisma.syncRun.findFirst({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: { user: { select: { name: true } } },
|
include: { user: { select: { name: true } } },
|
||||||
}),
|
}),
|
||||||
|
fetchSheetModifiedTime(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const allProducts = [...activeProducts, ...draftProducts]
|
const allProducts = [...activeProducts, ...draftProducts]
|
||||||
@@ -72,6 +90,12 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
: null
|
: 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({
|
return NextResponse.json({
|
||||||
products: {
|
products: {
|
||||||
active: activeProducts.length,
|
active: activeProducts.length,
|
||||||
@@ -80,6 +104,8 @@ export async function GET() {
|
|||||||
withoutImages,
|
withoutImages,
|
||||||
},
|
},
|
||||||
lastSync,
|
lastSync,
|
||||||
|
sheetDrift,
|
||||||
|
sheetModifiedAt: sheetModifiedTime,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
|
|||||||
Reference in New Issue
Block a user