Files
property-match/src/services/agentPresenceService.ts
T
Benjamin Sutter 093a958dcf feat(startseite): Giorgio entfernt, Belegschaft auf die fünf Agenten
Auf ausdrücklichen Wunsch: Giorgio entfällt vollständig. Das weicht bewusst
von §6 des Auftrags ab, der ihn als Personalverwalter und Vorgesetzten
vorsah — festgehalten, damit die Abweichung später nachvollziehbar bleibt.

Entfernt: Rollendefinition `AGENT_SUPERVISOR`, sein gezeichnetes Porträt, sein
Eintrag in der Auslastung und in der Belegschaftsansicht sowie die Erwähnung
in der Produkttour. Die Personalverwaltung bleibt unverändert über den
Menüpunkt «Meine Agenten» erreichbar — sie brauchte nie eine eigene Person am
Bildschirm.

Mit Giorgio entfällt auch die einzige Kennzahl, die aus den Freigabevorgängen
kam; `computeAgentWorkload` braucht `agentWorkItems` deshalb nicht mehr.

Ordner und Komponenten heissen jetzt nach ihrem Inhalt: `components/staff/`,
`AgentStaffOverview`, `StaffBackdrop`. «Meetingraum» war seit dem Umbau auf
Personenkarten irreführend, und ein falscher Name kostet den Nächsten mehr
Zeit als die Umbenennung.

Fünf statt sechs Karten heisst Flexlayout statt Dreierraster: drei oben, zwei
mittig darunter, gleich breit und ohne Lücke. Die Karten geben ihre Höhe nicht
mehr selbst vor — mit `height: 100%` beanspruchte jede die volle Fläche und
die zweite Zeile fiel aus dem sichtbaren Bereich.

Geprüft: 433 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925).
Browser 1280–1920 px und Tablet: fünf Karten vollständig sichtbar, keine
Überlappung, Titel und «Filter & Übersicht» beim Einstieg sichtbar, alle fünf
Klicks auf der richtigen Subpage, «Meine Agenten» weiterhin erreichbar, keine
Konsolenfehler, keine 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:03:18 +02:00

96 lines
3.5 KiB
TypeScript

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<string, AgentWorkload>
}
/**
* @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<string, AgentProtocolEntry>()
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,
}
})
}