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'}.
);
}
/** 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 (