093a958dcf
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>
96 lines
3.5 KiB
TypeScript
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,
|
|
}
|
|
})
|
|
}
|