feat(team): Datenschicht für Property On

Property On wird als Funktionsbereich "Teamübersicht" in Property Match
eingebettet — sieben digitale Mitarbeiter, die ausführen, während der Mensch
freigibt, anpasst oder zurückweist.

Durchgehend Provider → Service → Hook, kein übersprungener Layer:
- domain/: teamAgent, agentWorkItem, agentProtocol, agentConnection, agentFilters
- mock-data/: 7 Personalblätter aus dem Agenten-Katalog (Aufgabenzahlen exakt:
  Ferdi 9, Sina 7, Lea 11, Bruno 9, Reto 10, Livia 11, Nora 8), 14 Vorgänge,
  38 Protokolleinträge, 9 Verbindungen
- provider/: vier Mockup-Provider mit versionierter localStorage-Persistenz
- services/: Fachlogik inkl. Freigabe, Zurückweisung, Kennzahlen, Demo-Reset
- hooks/: React Query, Mutationen entwerten Vorgänge, Kennzahlen und Protokoll
  gemeinsam — sonst zeigt die Oberfläche widersprüchliche Zahlen

Namensgebung: Der Entitätstyp heisst TeamAgent, nicht Agent. Im Repo existiert
bereits PowerOn als KI-Backend-Proxy, und Claude Code legt Arbeitskopien unter
agent-* ab; ein nackter Typ Agent wäre in Suchen nicht auffindbar. Alle
Satellitentypen sind Agent*-präfixiert, weil domain/index.ts per export *
bündelt und generische Namen dort kollidieren.

Eigener Protokolltyp statt ActivityEvent: den Namen gibt es im Repo dreifach
und gegenseitig inkompatibel (domain/activityEvent.ts, governanceService.ts,
PropertyActivityLogPanel.tsx). Ein Anschluss hätte den Konflikt zementiert.

Demo-Uhr in lib/teamClock.ts: die Mockdaten sind auf den 20.05.2026 verankert.
Gegen die Systemzeit gerechnet wären die Filter "heute / diese Woche / dieser
Monat" an jedem anderen Kalendertag leer und die Auswertung sähe kaputt aus,
obwohl sie korrekt rechnet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin SutterandClaude Opus 5 committed 2026-08-03 16:55:40 +02:00
1 parent 36ce548169
commit 52dc7de25e
38 files changed
+9055

No files matched your search

+246
View File
@@ -0,0 +1,246 @@
/**
* Property On — «Teamübersicht»: digitale Mitarbeiter.
*
* Namensgebung: Der Entitätstyp heisst `TeamAgent`, nicht `Agent`. Im Repo existiert
* bereits `PowerOn` (AI-Backend-Proxy) und Claude-Code legt Worktrees unter `agent-*`
* ab — ein nackter Typ `Agent` wäre in Suchen praktisch nicht auffindbar. Alle
* Satellitentypen tragen deshalb das Präfix `Agent*`; generische Namen wie `Status`,
* `Priority` oder `Channel` sind tabu, weil `src/domain/index.ts` per `export *`
* bündelt und dort bereits `ReviewStatus`, `SourceType`, `RiskLevel` u. a. liegen.
*
* Fachliche Wahrheit ist der Agenten-Katalog: Rollen, Personalnummern und
* Zuständigkeiten der sieben Kernagenten werden nicht verändert.
*/
// ── Status & Autonomie ────────────────────────────────────────────────────────
export const AgentStatus = {
ACTIVE: 'ACTIVE',
BUILDING: 'BUILDING',
PAUSED: 'PAUSED',
} as const
export type AgentStatus = typeof AgentStatus[keyof typeof AgentStatus]
export const AgentAutonomyLevel = {
AUTONOMOUS: 'AUTONOMOUS',
APPROVAL_REQUIRED: 'APPROVAL_REQUIRED',
PROPOSAL_ONLY: 'PROPOSAL_ONLY',
} as const
export type AgentAutonomyLevel = typeof AgentAutonomyLevel[keyof typeof AgentAutonomyLevel]
// ── Kanäle ────────────────────────────────────────────────────────────────────
export const AgentChannelType = {
EMAIL: 'EMAIL',
TEAMS: 'TEAMS',
WHATSAPP: 'WHATSAPP',
WORKSPACE_CHAT: 'WORKSPACE_CHAT',
CALENDAR: 'CALENDAR',
WEB_FORM: 'WEB_FORM',
PORTAL_LEAD_MAIL: 'PORTAL_LEAD_MAIL',
PHONE_VOICE: 'PHONE_VOICE',
PHONE_NOTE: 'PHONE_NOTE',
AUDIO_OUTPUT: 'AUDIO_OUTPUT',
STRUCTURED_MAIL_FALLBACK: 'STRUCTURED_MAIL_FALLBACK',
} as const
export type AgentChannelType = typeof AgentChannelType[keyof typeof AgentChannelType]
export const AgentChannelDirection = {
INBOUND: 'INBOUND',
OUTBOUND: 'OUTBOUND',
BOTH: 'BOTH',
} as const
export type AgentChannelDirection = typeof AgentChannelDirection[keyof typeof AgentChannelDirection]
export const AgentConnectionStatus = {
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
ROADMAP: 'ROADMAP',
} as const
export type AgentConnectionStatus = typeof AgentConnectionStatus[keyof typeof AgentConnectionStatus]
/** Simulierte Kanalkonfiguration — es werden nie echte Zugangsdaten erfasst. */
export interface AgentChannelConfig {
displayName: string
senderAddress?: string
inboxAddress?: string
defaultRecipients: string[]
autoReplyEnabled: boolean
}
export interface AgentChannel {
id: string
type: AgentChannelType
direction: AgentChannelDirection
description: string
status: AgentConnectionStatus
enabled: boolean
/** Kennzeichnet im Katalog als «optional» geführte Kanäle. */
optional?: boolean
config: AgentChannelConfig
}
// ── Systeme ───────────────────────────────────────────────────────────────────
export const AgentSystemType = {
M365_EXCHANGE: 'M365_EXCHANGE',
OUTLOOK: 'OUTLOOK',
SHAREPOINT: 'SHAREPOINT',
ONEDRIVE: 'ONEDRIVE',
FILESHARE: 'FILESHARE',
DMS: 'DMS',
CALENDAR: 'CALENDAR',
IMMOTOP2: 'IMMOTOP2',
ABACUS_ABAIMMO: 'ABACUS_ABAIMMO',
RIMO_R5: 'RIMO_R5',
GARAIO_REM: 'GARAIO_REM',
CRM: 'CRM',
ZEFIX: 'ZEFIX',
SHAB: 'SHAB',
PUBLIC_WEB: 'PUBLIC_WEB',
PORTAL_LEAD_INBOX: 'PORTAL_LEAD_INBOX',
WORKSPACE: 'WORKSPACE',
} as const
export type AgentSystemType = typeof AgentSystemType[keyof typeof AgentSystemType]
export const AgentAccessLevel = {
READ: 'READ',
WRITE: 'WRITE',
READ_WRITE: 'READ_WRITE',
} as const
export type AgentAccessLevel = typeof AgentAccessLevel[keyof typeof AgentAccessLevel]
export interface AgentSystem {
id: string
type: AgentSystemType
access: AgentAccessLevel
status: AgentConnectionStatus
/** Wofür der Agent dieses System nutzt — in Alltagssprache, ohne KI-Jargon. */
usage: string
/** Klartext zur Berechtigung, z. B. «Schreibt erst nach Freigabe». */
permissionNote: string
}
// ── Aufgaben ──────────────────────────────────────────────────────────────────
export interface AgentTask {
id: string
title: string
description: string
enabled: boolean
/** Zeitplan oder auslösendes Ereignis, z. B. «täglich 06:00» oder «bei Eingang». */
schedule: string
requiresApproval: boolean
dependsOnChannel?: AgentChannelType
dependsOnSystem?: AgentSystemType
}
// ── Einstellungen ─────────────────────────────────────────────────────────────
export const AgentSettingKind = {
TEXT: 'TEXT',
NUMBER: 'NUMBER',
SELECT: 'SELECT',
MULTI_SELECT: 'MULTI_SELECT',
BOOLEAN: 'BOOLEAN',
TIME: 'TIME',
} as const
export type AgentSettingKind = typeof AgentSettingKind[keyof typeof AgentSettingKind]
export const AgentSettingGroup = {
GENERAL: 'GENERAL',
APPROVAL: 'APPROVAL',
DELIVERY: 'DELIVERY',
SPECIFIC: 'SPECIFIC',
} as const
export type AgentSettingGroup = typeof AgentSettingGroup[keyof typeof AgentSettingGroup]
export interface AgentSettingOption {
value: string
label: string
}
export interface AgentSetting {
id: string
label: string
description?: string
kind: AgentSettingKind
group: AgentSettingGroup
value: string | number | boolean | string[]
options?: AgentSettingOption[]
unit?: string
min?: number
max?: number
/** Fachlich nicht abschaltbar — z. B. Sinas Quellenzwang. */
locked?: boolean
lockedReason?: string
}
// ── Kennzahlen & Profil ───────────────────────────────────────────────────────
export interface AgentMetric {
id: string
label: string
value: string
hint?: string
}
/** Informationsbox unter dem Kopfbereich des Personalblatts (§7.6). */
export interface AgentProfile {
purpose: string
input: string[]
coreFlow: string[]
output: string[]
}
// ── Entität ───────────────────────────────────────────────────────────────────
/**
* Farbton des Avatars. Bewusst ein Token-Schlüssel statt eines Hex-Werts —
* `npm run check:tokens` liegt über dem Schwellwert und darf nicht wachsen.
*/
export const AgentAvatarTone = {
BRAND: 'BRAND',
SUCCESS: 'SUCCESS',
INFO: 'INFO',
SIGNAL: 'SIGNAL',
WARNING: 'WARNING',
GOLD: 'GOLD',
SLATE: 'SLATE',
} as const
export type AgentAvatarTone = typeof AgentAvatarTone[keyof typeof AgentAvatarTone]
export interface TeamAgent {
id: string
name: string
role: string
personnelNumber: string
department: string
email: string
avatarTone: AgentAvatarTone
status: AgentStatus
autonomyLevel: AgentAutonomyLevel
/** Ein Satz für Karte und Listeneintrag. */
shortDescription: string
/** Freitext zur Autonomie, wo der Katalog differenziert («autonom beim Erinnern, Freigabe bei Eskalationen»). */
autonomyNote: string
profile: AgentProfile
responsibilities: string[]
tasks: AgentTask[]
channels: AgentChannel[]
systems: AgentSystem[]
settings: AgentSetting[]
metrics: AgentMetric[]
/** Kurzkennzahl für die Agentenkarte auf der Teamübersicht. */
headlineMetricId: string
lastRun?: string
}
// ── Schreib-Inputs ────────────────────────────────────────────────────────────
export interface UpdateTeamAgentInput {
status?: AgentStatus
tasks?: AgentTask[]
channels?: AgentChannel[]
settings?: AgentSetting[]
}