import Link from 'next/link'; import { auth } from '@/lib/auth'; import { prisma } from '@/lib/db'; import { deviceHealth, humaniseSeconds } from '@/lib/device/health'; import { toCivilDate } from '@/lib/schedule/civil'; import { loadScheduleContext, loadSettings } from '@/lib/schedule/context'; import { formatDateFr, formatHoursText, formatNextChangeFr } from '@/lib/schedule/format'; import { getCurrentStatus, resolveWeek } from '@/lib/schedule/resolver'; import ScreenPreview from './ScreenPreview'; export const metadata = { title: 'Tableau de bord — ITA ITO' }; export default async function AdminHome() { const now = new Date(); const [session, settings] = await Promise.all([auth(), loadSettings()]); const context = await loadScheduleContext(now, settings); const status = getCurrentStatus(now, context); const week = resolveWeek(now, context); const today = toCivilDate(now, settings.timezone); const [device, failedTranslations, holidaySync] = await Promise.all([ prisma.device.findFirst({ where: { isActive: true }, orderBy: { lastSeenAt: 'desc' } }), prisma.message.count({ where: { translationStatus: 'ERROR' } }), prisma.syncState.findUnique({ where: { key: 'holidays' } }), ]); const health = device ? deviceHealth(device, now) : null; const isOpen = status.status === 'OPEN' || status.status === 'CLOSING_SOON'; const alerts: { text: string; href?: string }[] = []; if (!device) { alerts.push({ text: 'Aucun écran appairé. Suivez la procédure d’appairage du README.' }); } else if (health?.neverSeen) { alerts.push({ text: 'L’écran est enregistré mais ne s’est jamais manifesté.' }); } else if (health?.isOverdue) { const silentFor = humaniseSeconds(secondsSince(device.lastSeenAt, now)); alerts.push({ text: `L’écran n’a pas donné signe de vie ${silentFor}. Vérifiez la batterie et le Wi-Fi.`, }); } if (failedTranslations > 0) { alerts.push({ text: `${failedTranslations} message${failedTranslations > 1 ? 's' : ''} sans traduction anglaise.`, href: '/admin/messages', }); } if (holidaySync?.lastErrorAt) { alerts.push({ text: `La synchronisation des jours fériés a échoué : ${holidaySync.lastError ?? 'raison inconnue'}.`, href: '/admin/feries', }); } return (

Tableau de bord

{/* The header hides the address on a narrow screen to save room, so it belongs here: every change is signed with it in the audit trail, and several people share the shop's phone. */} Connecté en tant que {session?.user?.email} {session?.user?.role === 'admin' ? '' : ' — lecture seule'}.

{alerts.length > 0 ? ( ) : null}
{isOpen ? 'Ouvert' : 'Fermé'} {status.today.slots.length > 0 ? formatHoursText(status.today.slots) : 'Fermé aujourd’hui'} {formatNextChangeFr( status.nextChangeAt ? { kind: isOpen ? 'CLOSES' : 'OPENS', ...status.nextChangeAt } : null, today, ) || 'Aucune réouverture programmée'}

{formatDateFr(today)}

L’écran

Appareil

{device && health ? (
{device.friendlyId} {device.lastSeenAt ? humaniseSeconds(secondsSince(device.lastSeenAt, now)) : 'jamais'} {health.secondsUntilWake === null ? 'inconnu' : health.secondsUntilWake >= 0 ? humaniseSeconds(health.secondsUntilWake) : 'attendu, en retard'} {health.batteryPercent === null ? 'inconnue' : `environ ${health.batteryPercent}%${device.percentCharged === null ? ' (estimée)' : ''}`} {health.signal === null ? 'inconnu' : { good: 'bon', fair: 'moyen', poor: 'faible' }[health.signal]} {device.fwVersion ?? 'inconnu'}
) : (

Aucun écran appairé.

)}

L’écran fonctionne sur batterie et dort entre deux réveils : une modification n’apparaît qu’au réveil suivant. Rien ne peut lui être poussé.

Les sept prochains jours

Modifier les horaires d’aujourd’hui

); } /** Negative, so `humaniseSeconds` phrases it as "il y a …". */ function secondsSince(moment: Date | null, now: Date): number { return moment ? Math.round((moment.getTime() - now.getTime()) / 1000) : 0; } function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); }