feat(startseite): digitale Belegschaft mit Präsenz, eigener Stimme und Nachweis
Der nachgebaute Meetingraum entfällt. Eine gezeichnete Kulisse bleibt neben fotografierten Porträts erkennbar gezeichnet — und ein Raum allein macht niemanden menschlich. Menschlich wirkt im echten Büro dasselbe wie hier: dass jemand da ist, etwas zu sagen hat und nachweisbar gearbeitet hat. Die Startseite zeigt deshalb sechs Personenkarten vor einer abgedunkelten Büroaufnahme. Jede Karte trägt, was ein Personalblatt trägt: - Porträt mit Dienstpunkt — ist da oder pausiert - Name, Funktion, Abteilung, Personalnummer - Autonomiestufe: «Autonom», «Freigabe erforderlich», «Nur Vorschlag». Sie sagt, wie viel Verantwortung jemand trägt, und trennt Personal von Automatisierung. - Ein Satz in der ersten Person zur aktuellen Lage, statt einer Kennzahl: «Bei 12 Fristen ist der Termin durch. Fürs Eskalieren brauche ich deine Freigabe.» - Die zuletzt protokollierte Handlung mit Zeitangabe. Diese Zeile ist der Unterschied zwischen Behauptung und Beleg — eine Kennzahl kann jede Software anzeigen. Darüber fasst ein Anriss zusammen, wie viele Mitarbeitende heute eine Entscheidung brauchen, samt Dienststand der Belegschaft. Schichten nach CLAUDE.md §4.2: `agentPresenceService` entscheidet, was als letzte Handlung gilt und wie eine Zeitangabe in Alltagssprache lautet; `agentWorkloadService` hält Zeitfenster, Schwellen und Formulierungen. Beide nehmen den Stichzeitpunkt als Parameter — dieselbe Datenlage ergibt immer dasselbe Ergebnis und ist ohne Renderer prüfbar (22 Tests). Giorgio hat kein Personalblatt; Abteilung, Personalnummer und Stufe kommen aus der zentralen Rollendefinition. Wer kein Dossier hat, gilt als im Dienst — sonst stünde die halbe Belegschaft still, weil Stammdaten fehlen. Bild: Unsplash, freie kommerzielle Lizenz. Herkunft und Austauschhinweis in `src/assets/meeting-room/HERKUNFT.md`. Geprüft: 435 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925). Browser 1280–1920 px und Tablet: sechs Karten ohne Überlappung, Titel und «Filter & Übersicht» beim Einstieg sichtbar, alle sechs Klicks auf der richtigen Subpage, keine Konsolenfehler, keine 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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_SUPERVISOR, 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.
|
||||
*
|
||||
* Giorgio steht bewusst am Ende: er ist Vorgesetzter, kein sechster
|
||||
* Sachbearbeiter, und sein Klick führt nicht auf eine Arbeitsseite, sondern in
|
||||
* die Personalverwaltung.
|
||||
*/
|
||||
|
||||
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, AGENT_SUPERVISOR]
|
||||
|
||||
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,
|
||||
// Personalblatt zuerst; für Giorgio, der keines hat, die Angaben aus
|
||||
// der zentralen Rollendefinition.
|
||||
department: dossier?.department ?? member.department ?? '',
|
||||
personnelNumber: dossier?.personnelNumber ?? member.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,
|
||||
// Giorgio führt die Belegschaft und entscheidet nichts allein über
|
||||
// Objekte — seine Stufe ergibt sich aus der Rolle, nicht aus einem Dossier.
|
||||
autonomy: dossier ? (AGENT_AUTONOMY_LABELS[dossier.autonomyLevel] ?? '') : 'Vorgesetzter',
|
||||
workload: workload[member.id],
|
||||
lastAction: action
|
||||
? { title: action.title, when: formatRelativeTime(action.timestamp, now) }
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user