import type { AgentPresence } from '../domain/agentPresence' import type { AgentWorkload } from '../domain/agentWorkload' import type { AgentProtocolEntry } from '../domain/agentProtocol' import type { TeamAgent } from '../domain/teamAgent' import { AgentStatus } from '../domain/teamAgent' import { AGENT_WORKSPACES } from '../lib/agentWorkspaces' import { AGENT_AUTONOMY_LABELS } from '../lib/constants' /** * Setzt die Belegschaftsansicht der Startseite zusammen (Runde 5). * * Reine Geschäftslogik ausserhalb von React (CLAUDE.md §4.2): welcher Eintrag * als «letzte Handlung» gilt und wie eine Zeitangabe in Alltagssprache lautet, * sind fachliche Festlegungen. Der Stichzeitpunkt ist ein Parameter — dieselbe * Datenlage ergibt damit immer dasselbe Ergebnis und lässt sich prüfen. */ const MINUTE = 60 * 1000 const HOUR = 60 * MINUTE const DAY = 24 * HOUR /** * Zeitangabe in Alltagssprache. * * Bewusst grob: «vor 12 Min.» ist für einen Menschen brauchbarer als * «14:32:07». Ab gestern steht die Uhrzeit dabei, weil «vor 19 Stunden» * niemand in einen Arbeitstag einordnen kann. */ export function formatRelativeTime(iso: string, now: number): string { const t = new Date(iso).getTime() if (!Number.isFinite(t)) return '' const diff = now - t if (diff < 0) return 'gerade eben' if (diff < MINUTE) return 'gerade eben' if (diff < HOUR) return `vor ${Math.floor(diff / MINUTE)} Min.` if (diff < DAY) { const hours = Math.floor(diff / HOUR) return `vor ${hours} ${hours === 1 ? 'Stunde' : 'Stunden'}` } const date = new Date(t) const time = date.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) if (diff < 2 * DAY) return `gestern ${time}` return date.toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit' }) + ` ${time}` } export interface AgentPresenceInput { agents: TeamAgent[] protocol: AgentProtocolEntry[] workload: Record } /** * @param now Stichzeitpunkt in Millisekunden — als Parameter, nicht aus der * Systemuhr, damit das Ergebnis reproduzierbar bleibt. */ export function buildAgentPresence( { agents, protocol, workload }: AgentPresenceInput, now: number = Date.now(), ): AgentPresence[] { // Jüngster Protokolleintrag je Mitarbeitendem. Ein Durchgang über die Liste // statt einer Suche je Agent — das Protokoll wächst, die Belegschaft nicht. const latest = new Map() for (const entry of protocol) { if (!entry.agentId) continue const known = latest.get(entry.agentId) if (!known || entry.timestamp > known.timestamp) latest.set(entry.agentId, entry) } const byId = new Map(agents.map(a => [a.id, a])) const roster = AGENT_WORKSPACES return roster.map(member => { const dossier = byId.get(member.id) const action = latest.get(member.id) return { agentId: member.id, name: member.name, role: member.role, department: dossier?.department ?? '', personnelNumber: dossier?.personnelNumber ?? '', path: member.path, // Ohne Personalblatt gilt jemand als im Dienst: die Belegschaftsansicht // soll niemanden fälschlich als pausiert zeigen, nur weil ein Dossier fehlt. onDuty: dossier ? dossier.status === AgentStatus.ACTIVE : true, autonomy: dossier ? (AGENT_AUTONOMY_LABELS[dossier.autonomyLevel] ?? '') : '', workload: workload[member.id], lastAction: action ? { title: action.title, when: formatRelativeTime(action.timestamp, now) } : undefined, } }) }