Files
gestion-materiaux-destock/src/app/(dashboard)/page.tsx
T
2026-06-11 22:49:22 +02:00

274 lines
9.8 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
import { Header } from '@/components/layout/Header'
interface ProductStats {
active: number
draft: number
total: number
withoutImages: number
}
interface LastSync {
createdAt: string
userName: string
status: string
created: number
updated: number
deactivated: number
errors: number
}
interface DashboardStats {
products: ProductStats
lastSync: LastSync | null
}
function StatCard({
icon,
label,
value,
sub,
href,
loading,
error,
accent,
}: {
icon: string
label: string
value: string | number | null
sub?: string
href?: string
loading?: boolean
error?: boolean
accent?: 'indigo' | 'amber' | 'green' | 'red'
}) {
const accentClass = {
indigo: 'border-indigo-500/30 bg-indigo-500/5',
amber: 'border-amber-500/30 bg-amber-500/5',
green: 'border-green-500/30 bg-green-500/5',
red: 'border-red-500/30 bg-red-500/5',
}[accent ?? 'indigo']
const content = (
<div className={`rounded-xl border p-5 flex flex-col gap-1 ${accentClass} ${href ? 'hover:brightness-110 transition-all cursor-pointer' : ''}`}>
<span className="text-xl">{icon}</span>
<p className="text-xs text-slate-400 mt-1">{label}</p>
{loading ? (
<p className="text-2xl font-bold text-slate-600 animate-pulse"></p>
) : error ? (
<p className="text-sm text-red-400">Erreur</p>
) : (
<p className="text-2xl font-bold text-white">{value ?? '—'}</p>
)}
{sub && <p className="text-xs text-slate-500">{sub}</p>}
</div>
)
return href ? <Link href={href}>{content}</Link> : content
}
function ActionCard({ href, icon, label, description }: { href: string; icon: string; label: string; description: string }) {
return (
<Link
href={href}
className="flex items-start gap-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl p-4 transition-colors group"
>
<span className="text-2xl mt-0.5">{icon}</span>
<div>
<p className="text-sm font-semibold text-white group-hover:text-indigo-300 transition-colors">{label}</p>
<p className="text-xs text-slate-400 mt-0.5">{description}</p>
</div>
</Link>
)
}
function formatDate(iso: string) {
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit',
}).format(new Date(iso))
}
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [statsError, setStatsError] = useState(false)
const [statsLoading, setStatsLoading] = useState(true)
const [pendingCount, setPendingCount] = useState<number | null>(null)
const [pendingLoading, setPendingLoading] = useState(false)
const [pendingError, setPendingError] = useState(false)
useEffect(() => {
fetch('/api/dashboard/stats')
.then((r) => r.json())
.then((data) => {
if (data.error) throw new Error(data.error)
setStats(data)
})
.catch(() => setStatsError(true))
.finally(() => setStatsLoading(false))
}, [])
const loadPending = useCallback(async () => {
setPendingLoading(true)
setPendingError(false)
try {
const r = await fetch('/api/dashboard/pending')
const data = await r.json()
if (data.error) throw new Error(data.error)
setPendingCount(data.count)
} catch {
setPendingError(true)
} finally {
setPendingLoading(false)
}
}, [])
const lastSync = stats?.lastSync
return (
<>
<Header title="📊 Dashboard" />
<div className="p-6 flex flex-col gap-8 flex-1 overflow-y-auto">
{/* ── KPIs ─────────────────────────────────────────────── */}
<section>
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Vue d&apos;ensemble</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard
icon="📦"
label="Produits actifs"
value={stats?.products.active ?? null}
sub={stats ? `${stats.products.draft} en brouillon` : undefined}
loading={statsLoading}
error={statsError}
accent="green"
href="/produits"
/>
<StatCard
icon="🖼️"
label="Sans image"
value={stats?.products.withoutImages ?? null}
sub={stats ? `sur ${stats.products.total} produits` : undefined}
loading={statsLoading}
error={statsError}
accent={stats && stats.products.withoutImages > 0 ? 'amber' : 'green'}
href="/images"
/>
<StatCard
icon="🔄"
label="Dernière sync"
value={lastSync ? formatDate(lastSync.createdAt) : 'Aucune'}
sub={lastSync ? `par ${lastSync.userName}` : undefined}
loading={statsLoading}
error={statsError}
accent="indigo"
href="/sync"
/>
{/* Pending — chargement lazy */}
<div
className="rounded-xl border border-indigo-500/30 bg-indigo-500/5 p-5 flex flex-col gap-1 cursor-pointer hover:brightness-110 transition-all"
onClick={pendingCount === null && !pendingLoading ? loadPending : undefined}
>
<span className="text-xl"></span>
<p className="text-xs text-slate-400 mt-1">En attente de création</p>
{pendingLoading ? (
<p className="text-2xl font-bold text-slate-600 animate-pulse"></p>
) : pendingError ? (
<p className="text-sm text-red-400">Erreur réessayer</p>
) : pendingCount !== null ? (
<p className="text-2xl font-bold text-white">{pendingCount}</p>
) : (
<button className="text-xs text-indigo-400 hover:text-indigo-300 text-left mt-1">
Calculer
</button>
)}
{pendingCount !== null && (
<p className="text-xs text-slate-500">depuis Google Sheets</p>
)}
</div>
</div>
</section>
{/* ── Dernière sync détail ──────────────────────────────── */}
{!statsLoading && lastSync && (
<section>
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Dernière synchronisation</h2>
<div className="bg-slate-800 border border-slate-700 rounded-xl p-5 flex flex-col gap-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-white">{formatDate(lastSync.createdAt)}</p>
<p className="text-xs text-slate-400">par {lastSync.userName}</p>
</div>
<span className={`text-xs font-medium px-2 py-1 rounded-full border ${
lastSync.status === 'success'
? 'bg-green-900/30 border-green-700 text-green-400'
: 'bg-red-900/30 border-red-700 text-red-400'
}`}>
{lastSync.status === 'success' ? '✅ Succès' : '❌ Erreur'}
</span>
</div>
<div className="grid grid-cols-4 gap-3 pt-2 border-t border-slate-700">
{[
{ label: 'Créations', value: lastSync.created, color: 'text-green-400' },
{ label: 'Mises à jour', value: lastSync.updated, color: 'text-blue-400' },
{ label: 'Désactivés', value: lastSync.deactivated, color: 'text-amber-400' },
{ label: 'Erreurs', value: lastSync.errors, color: 'text-red-400' },
].map(({ label, value, color }) => (
<div key={label} className="text-center">
<p className={`text-xl font-bold ${color}`}>{value}</p>
<p className="text-xs text-slate-500">{label}</p>
</div>
))}
</div>
<div className="flex justify-end">
<Link href="/sync" className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors">
Nouvelle sync
</Link>
</div>
</div>
</section>
)}
{/* ── Actions rapides ───────────────────────────────────── */}
<section>
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Actions rapides</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<ActionCard
href="/images"
icon="🖼️"
label="Uploader des images"
description="Convertir HEIC, détourer et envoyer sur Shopify"
/>
<ActionCard
href="/sync"
icon="🔄"
label="Synchroniser le catalogue"
description="Comparer Google Sheets avec Shopify et appliquer les changements"
/>
<ActionCard
href="/creation"
icon="✨"
label="Créer des produits"
description="Ajouter les produits en attente depuis Google Sheets"
/>
<ActionCard
href="/produits"
icon="📦"
label="Voir le catalogue"
description="Parcourir et filtrer tous les produits Shopify"
/>
</div>
</section>
</div>
</>
)
}