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
{alerts.length > 0 ? (
{alerts.map((alert) => (
{alert.href ? (
{alert.text}
) : (
alert.text
)}
))}
) : 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)}
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
{week.map((day, index) => (
{formatDateFr(day.date)}
{index === 0 ? ' (aujourd’hui)' : ''}
{day.slots.length > 0 ? formatHoursText(day.slots) : 'Fermé'}
{day.isException ? (
{day.noteFr ?? 'exception'}
) : null}
))}
Modifier les horaires d’aujourd’hui
{session?.user?.role !== 'admin' ? (
Compte en lecture seule : aucune modification n’est possible.
) : null}
);
}
/** 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}
);
}