diff --git a/src/domain/agentConnection.ts b/src/domain/agentConnection.ts new file mode 100644 index 0000000..69863af --- /dev/null +++ b/src/domain/agentConnection.ts @@ -0,0 +1,75 @@ +/** + * Property On — Subreiter «Kanäle & Systeme»: die zentralen Verbindungen der + * Organisation. + * + * Bewusst getrennt von `domain/dataSource.ts` (`DataSource`/`ConnectorRun`): + * jenes Modell beschreibt Crawler-Quellen mit AGB-Status und Crawl-Läufen, hier + * geht es um angebundene Kommunikationskanäle und Fachsysteme. Zwei fachlich + * verschiedene Dinge im selben Typ zu führen wäre eine Scheinvereinfachung. + * + * Alle Verbindungen sind Frontend-Simulation — es werden nie echte Zugangsdaten + * erfasst und nie echte Systeme kontaktiert. + */ + +import type { AgentAccessLevel, AgentConnectionStatus } from './teamAgent' + +export const AgentConnectionCategory = { + EMAIL_M365: 'EMAIL_M365', + WHATSAPP: 'WHATSAPP', + TEAMS: 'TEAMS', + CALENDAR: 'CALENDAR', + DOCUMENT_STORE: 'DOCUMENT_STORE', + PHONE_VOICE: 'PHONE_VOICE', + ERP: 'ERP', + CRM: 'CRM', + PUBLIC_SOURCES: 'PUBLIC_SOURCES', +} as const +export type AgentConnectionCategory = typeof AgentConnectionCategory[keyof typeof AgentConnectionCategory] + +/** ERP-Auswahl aus §13.4. */ +export const AgentErpVendor = { + IMMOTOP2: 'IMMOTOP2', + ABACUS_ABAIMMO: 'ABACUS_ABAIMMO', + RIMO_R5: 'RIMO_R5', + GARAIO_REM: 'GARAIO_REM', + OTHER: 'OTHER', +} as const +export type AgentErpVendor = typeof AgentErpVendor[keyof typeof AgentErpVendor] + +export interface AgentConnectionPermission { + id: string + label: string + access: AgentAccessLevel + granted: boolean +} + +export interface AgentConnection { + id: string + category: AgentConnectionCategory + name: string + description: string + status: AgentConnectionStatus + accountCount: number + usedByAgentIds: string[] + lastSyncAt?: string + /** Nur bei `category === ERP` gesetzt. */ + vendor?: AgentErpVendor + /** Frei benannte Verbindung aus dem Assistenten, z. B. «Bewirtschaftung Zürich». */ + connectionLabel?: string + permissions: AgentConnectionPermission[] +} + +/** Ergebnis von «Verbindung testen» — deterministisch, kein Zufall (§19.2). */ +export interface AgentConnectionTestResult { + ok: boolean + message: string + checkedAt: string +} + +export interface CreateAgentConnectionInput { + connectionId: string + vendor?: AgentErpVendor + connectionLabel: string + permissionIds: string[] + agentIds: string[] +} diff --git a/src/domain/agentFilters.ts b/src/domain/agentFilters.ts new file mode 100644 index 0000000..725a6eb --- /dev/null +++ b/src/domain/agentFilters.ts @@ -0,0 +1,64 @@ +/** + * Property On — Filter- und Sortierdimensionen. + * + * Eigene Datei, weil sowohl die Auswertung auf der Teamübersicht (§4.2) als auch + * der Bearbeitungsverlauf (§5.3) und das Protokoll (§12.3) denselben Zeitraum- + * begriff teilen. Ohne gemeinsamen Typ driften die drei Filter auseinander. + */ + +export const AgentPeriod = { + TODAY: 'TODAY', + WEEK: 'WEEK', + MONTH: 'MONTH', + YEAR: 'YEAR', + ALL: 'ALL', +} as const +export type AgentPeriod = typeof AgentPeriod[keyof typeof AgentPeriod] + +export const AgentWorkItemSort = { + NEWEST: 'NEWEST', + OLDEST: 'OLDEST', + PRIORITY: 'PRIORITY', + AGENT: 'AGENT', + OBJECT_ID: 'OBJECT_ID', +} as const +export type AgentWorkItemSort = typeof AgentWorkItemSort[keyof typeof AgentWorkItemSort] + +/** + * Grenze eines Zeitraums, gemessen am Referenzdatum. + * Gibt den frühesten noch enthaltenen Zeitpunkt zurück, oder null für ALL. + */ +export function periodStart(period: AgentPeriod, reference: Date): Date | null { + const d = new Date(reference) + switch (period) { + case AgentPeriod.TODAY: + d.setHours(0, 0, 0, 0) + return d + case AgentPeriod.WEEK: { + // Woche beginnt am Montag — Schweizer Konvention + const day = (d.getDay() + 6) % 7 + d.setDate(d.getDate() - day) + d.setHours(0, 0, 0, 0) + return d + } + case AgentPeriod.MONTH: + d.setDate(1) + d.setHours(0, 0, 0, 0) + return d + case AgentPeriod.YEAR: + d.setMonth(0, 1) + d.setHours(0, 0, 0, 0) + return d + case AgentPeriod.ALL: + default: + return null + } +} + +/** Liegt `isoTimestamp` im gewählten Zeitraum? */ +export function isWithinPeriod(isoTimestamp: string, period: AgentPeriod, reference: Date): boolean { + const start = periodStart(period, reference) + if (!start) return true + const t = new Date(isoTimestamp).getTime() + return !Number.isNaN(t) && t >= start.getTime() +} diff --git a/src/domain/agentProtocol.ts b/src/domain/agentProtocol.ts new file mode 100644 index 0000000..8348d5c --- /dev/null +++ b/src/domain/agentProtocol.ts @@ -0,0 +1,86 @@ +/** + * Property On — Protokolleinträge. + * + * Bewusst ein eigener Typ und nicht `ActivityEvent`: den Namen gibt es im Repo + * bereits dreifach und gegenseitig inkompatibel — in `domain/activityEvent.ts` + * (über das Domain-Barrel exportiert), lokal in `services/governanceService.ts` + * und noch einmal in `components/supply/PropertyActivityLogPanel.tsx`. Ein + * Anschluss an einen dieser Typen würde den Konflikt zementieren. + */ + +import type { AgentChannelType } from './teamAgent' +import type { AgentDetailField, AgentProcessingStep, AgentSourceReference } from './agentWorkItem' + +export const AgentProtocolStatus = { + SUCCESS: 'SUCCESS', + WARNING: 'WARNING', + ERROR: 'ERROR', + INFO: 'INFO', +} as const +export type AgentProtocolStatus = typeof AgentProtocolStatus[keyof typeof AgentProtocolStatus] + +export const AgentProtocolEventType = { + TASK_RUN: 'TASK_RUN', + MESSAGE_PROCESSED: 'MESSAGE_PROCESSED', + REMINDER_SENT: 'REMINDER_SENT', + ESCALATION: 'ESCALATION', + APPROVAL: 'APPROVAL', + REJECTION: 'REJECTION', + EDIT: 'EDIT', + SETTINGS_SAVED: 'SETTINGS_SAVED', + TASKS_SAVED: 'TASKS_SAVED', + AGENT_PAUSED: 'AGENT_PAUSED', + AGENT_RESUMED: 'AGENT_RESUMED', + CHANNEL_TESTED: 'CHANNEL_TESTED', + CHANNEL_TOGGLED: 'CHANNEL_TOGGLED', + CONNECTION_CREATED: 'CONNECTION_CREATED', + CONNECTION_REMOVED: 'CONNECTION_REMOVED', + DEMO_RESET: 'DEMO_RESET', +} as const +export type AgentProtocolEventType = typeof AgentProtocolEventType[keyof typeof AgentProtocolEventType] + +/** Wer den Eintrag ausgelöst hat — Mensch oder Zeitplan/Systemtrigger. */ +export const AgentTriggerSource = { + USER: 'USER', + SYSTEM: 'SYSTEM', +} as const +export type AgentTriggerSource = typeof AgentTriggerSource[keyof typeof AgentTriggerSource] + +export const AgentApprovalDecision = { + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', + EDITED: 'EDITED', +} as const +export type AgentApprovalDecision = typeof AgentApprovalDecision[keyof typeof AgentApprovalDecision] + +export interface AgentProtocolEntry { + id: string + /** + * Verantwortlicher digitaler Mitarbeiter. Fehlt bei organisationsweiten + * Ereignissen — eine Verbindung im Subreiter «Kanäle & Systeme» gehört keinem + * einzelnen Mitarbeiter. Solche Einträge erscheinen im Gesamtprotokoll, nicht + * im Personalblatt. + */ + agentId?: string + /** Nur bei Verbindungsereignissen gesetzt. */ + connectionId?: string + timestamp: string + eventType: AgentProtocolEventType + title: string + description: string + status: AgentProtocolStatus + objectId?: string + channel?: AgentChannelType + triggeredBy: AgentTriggerSource + /** Klarname der auslösenden Person, wenn `triggeredBy === USER`. */ + triggeredByName?: string + approvalDecision?: AgentApprovalDecision + workItemId?: string + // Detailansicht (§12.5) + input?: AgentDetailField[] + processingSteps?: AgentProcessingStep[] + output?: AgentDetailField[] + sourceReferences?: AgentSourceReference[] +} + +export type CreateAgentProtocolEntryInput = Omit diff --git a/src/domain/agentWorkItem.ts b/src/domain/agentWorkItem.ts new file mode 100644 index 0000000..24909e2 --- /dev/null +++ b/src/domain/agentWorkItem.ts @@ -0,0 +1,185 @@ +/** + * Property On — Vorgänge im «Bearbeitungsverlauf». + * + * Ein Vorgang ist entweder bereits autonom erledigt («Erledigte Aufträge») oder + * wartet auf einen Menschen («Pendente Anfragen»). Das Bedienprinzip ist + * human-led, agent-operated: der Agent arbeitet, der Mensch entscheidet. + */ + +import type { AgentChannelType } from './teamAgent' + +export const AgentWorkItemStatus = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', + EDITING: 'EDITING', + COMPLETED: 'COMPLETED', +} as const +export type AgentWorkItemStatus = typeof AgentWorkItemStatus[keyof typeof AgentWorkItemStatus] + +export const AgentWorkItemPriority = { + LOW: 'LOW', + MEDIUM: 'MEDIUM', + HIGH: 'HIGH', + CRITICAL: 'CRITICAL', +} as const +export type AgentWorkItemPriority = typeof AgentWorkItemPriority[keyof typeof AgentWorkItemPriority] + +/** Vorgangstyp — Filterdimension in §5.3. */ +export const AgentWorkItemKind = { + MESSAGE_TRIAGE: 'MESSAGE_TRIAGE', + REMINDER_CREATED: 'REMINDER_CREATED', + CONTRACT_ANSWER: 'CONTRACT_ANSWER', + LOCATION_MODULE: 'LOCATION_MODULE', + INQUIRY_QUALIFIED: 'INQUIRY_QUALIFIED', + AUTO_REJECTION: 'AUTO_REJECTION', + APPOINTMENT: 'APPOINTMENT', + BRIEFING: 'BRIEFING', + RECAP: 'RECAP', + MARKET_SIGNAL: 'MARKET_SIGNAL', + ESCALATION: 'ESCALATION', + DOC_GAP: 'DOC_GAP', + SYSTEM_ISSUE: 'SYSTEM_ISSUE', +} as const +export type AgentWorkItemKind = typeof AgentWorkItemKind[keyof typeof AgentWorkItemKind] + +/** Fachbereich — Filterdimension «Bereich» in §4.2 und §5.3. */ +export const AgentDomainArea = { + CONTRACT_DEADLINES: 'CONTRACT_DEADLINES', + DOCUMENTATION: 'DOCUMENTATION', + INQUIRIES: 'INQUIRIES', + VIEWINGS: 'VIEWINGS', + LOCATION: 'LOCATION', + MARKET: 'MARKET', +} as const +export type AgentDomainArea = typeof AgentDomainArea[keyof typeof AgentDomainArea] + +export const AgentWorkItemAction = { + APPROVE: 'APPROVE', + ONE_CLICK_CONFIRM: 'ONE_CLICK_CONFIRM', + DECIDE: 'DECIDE', + EDIT: 'EDIT', + REJECT: 'REJECT', + ANSWER_QUERY: 'ANSWER_QUERY', + OPEN_SOURCE: 'OPEN_SOURCE', + MARK_DONE: 'MARK_DONE', +} as const +export type AgentWorkItemAction = typeof AgentWorkItemAction[keyof typeof AgentWorkItemAction] + +// ── Bestandteile der Detailansicht ──────────────────────────────────────────── + +/** + * Geordnetes Schlüssel-Wert-Paar. Die Spezifikation nennt + * `Record`; ein Array erhält die fachliche + * Reihenfolge verlässlich und lässt sich direkt rendern. + */ +export interface AgentDetailField { + label: string + value: string | string[] +} + +/** Fundstelle: Dokument, Seite, Zitat — Sinas Quellenzwang hängt daran. */ +export interface AgentSourceReference { + id: string + label: string + documentName?: string + locator?: string + quote?: string +} + +/** Ein Nachrichten- oder Mailverlauf, wie er im Detail gezeigt wird (§5.4). */ +export interface AgentMessageEntry { + id: string + at: string + from: string + to?: string + channel: AgentChannelType + subject?: string + body: string +} + +/** Simulierter Verarbeitungsschritt — bewusst in Alltagssprache. */ +export interface AgentProcessingStep { + id: string + label: string + detail?: string +} + +export interface AgentWorkItemHistoryEntry { + id: string + at: string + actor: string + action: string + note?: string +} + +/** Im «Anpassen»-Dialog editierbares Feld. */ +export interface AgentEditableField { + id: string + label: string + value: string + multiline?: boolean + helperText?: string +} + +// ── Entität ─────────────────────────────────────────────────────────────────── + +export interface AgentWorkItem { + id: string + agentId: string + kind: AgentWorkItemKind + area: AgentDomainArea + title: string + summary: string + objectId?: string + objectLabel?: string + sourceChannel: AgentChannelType + targetChannel?: AgentChannelType + createdAt: string + completedAt?: string + priority: AgentWorkItemPriority + status: AgentWorkItemStatus + /** true → Reiter «Pendente Anfragen», false → «Erledigte Aufträge». */ + requiresDecision: boolean + /** Grund der Rückfrage (§5.5) — nur bei pendenten Vorgängen. */ + escalationReason?: string + /** Die konkrete Frage, die der Mensch beantworten muss. */ + decisionQuestion?: string + inputs: AgentDetailField[] + result: AgentDetailField[] + processingSteps: AgentProcessingStep[] + sourceReferences: AgentSourceReference[] + messageThread?: AgentMessageEntry[] + availableActions: AgentWorkItemAction[] + editableFields?: AgentEditableField[] + history: AgentWorkItemHistoryEntry[] + rejectionReason?: string +} + +// ── Auswertung ──────────────────────────────────────────────────────────────── + +/** + * Die drei Kennzahlen der Teamübersicht (§4.2). + * + * Abgrenzung der beiden pendenten Werte: Ein Vorgang zählt als Freigabeanfrage, + * wenn er dem Menschen eine Entscheidung zur Bestätigung vorlegt (Freigeben, + * 1-Klick-Bestätigung, Entscheidung treffen). Alles andere Pendente — Rückfragen, + * fehlende Angaben, Verarbeitungs- und Systemprobleme — zählt als Nachricht. + */ +export interface AgentWorkloadKpis { + completedTasks: number + pendingMessages: number + pendingApprovals: number +} + +// ── Schreib-Inputs ──────────────────────────────────────────────────────────── + +export interface UpdateAgentWorkItemInput { + status?: AgentWorkItemStatus + result?: AgentDetailField[] + editableFields?: AgentEditableField[] + rejectionReason?: string + completedAt?: string + requiresDecision?: boolean + historyEntry?: Omit +} diff --git a/src/domain/teamAgent.ts b/src/domain/teamAgent.ts new file mode 100644 index 0000000..3b89b80 --- /dev/null +++ b/src/domain/teamAgent.ts @@ -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[] +} diff --git a/src/hooks/useAgentConnections.ts b/src/hooks/useAgentConnections.ts new file mode 100644 index 0000000..cb74455 --- /dev/null +++ b/src/hooks/useAgentConnections.ts @@ -0,0 +1,74 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { agentConnectionService } from '../services/agentConnectionService' +import type { CreateAgentConnectionInput } from '../domain/agentConnection' +import { useToastStore } from '../stores/toastStore' +import { STALE_AGENT_CONNECTIONS } from '../lib/constants' +import { QK_AGENT_PROTOCOL } from './useAgentProtocol' + +export const QK_AGENT_CONNECTIONS = 'agentConnections' + +export function useAgentConnections() { + return useQuery({ + queryKey: [QK_AGENT_CONNECTIONS], + queryFn: () => agentConnectionService.getAll(), + staleTime: STALE_AGENT_CONNECTIONS, + select: (res) => res.data ?? [], + }) +} + +function useConnectionInvalidation() { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries({ queryKey: [QK_AGENT_CONNECTIONS] }) + queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] }) + } +} + +export function useConnectAgentConnection() { + const invalidate = useConnectionInvalidation() + return useMutation({ + mutationFn: (input: CreateAgentConnectionInput) => agentConnectionService.connect(input), + onSuccess: (res) => { + invalidate() + useToastStore + .getState() + .showToast(`${res.data.name} verbunden. Frontend-Simulation, kein echtes System.`, 'success') + }, + onError: () => { + useToastStore.getState().showToast('Verbindung konnte nicht angelegt werden.', 'error') + }, + }) +} + +export function useDisconnectAgentConnection() { + const invalidate = useConnectionInvalidation() + return useMutation({ + mutationFn: (id: string) => agentConnectionService.disconnect(id), + onSuccess: (res) => { + invalidate() + useToastStore.getState().showToast(`${res.data.name} getrennt.`, 'warning') + }, + onError: () => { + useToastStore.getState().showToast('Verbindung konnte nicht getrennt werden.', 'error') + }, + }) +} + +/** + * «Verbindung testen». Ein fehlgeschlagener Test ist ein gültiges fachliches + * Ergebnis, kein Fehler — deshalb landet er in `onSuccess` mit Warnton und wird + * nicht als roter Fehler-Toast ausgegeben. + */ +export function useTestAgentConnection() { + const invalidate = useConnectionInvalidation() + return useMutation({ + mutationFn: (id: string) => agentConnectionService.test(id), + onSuccess: (res) => { + invalidate() + useToastStore.getState().showToast(res.data.message, res.data.ok ? 'success' : 'warning') + }, + onError: () => { + useToastStore.getState().showToast('Test konnte nicht ausgeführt werden.', 'error') + }, + }) +} diff --git a/src/hooks/useAgentProtocol.ts b/src/hooks/useAgentProtocol.ts new file mode 100644 index 0000000..12f8e51 --- /dev/null +++ b/src/hooks/useAgentProtocol.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query' +import { agentProtocolService } from '../services/agentProtocolService' +import type { AgentProtocolFilters } from '../provider/IAgentProtocolProvider' +import { STALE_AGENT_PROTOCOL } from '../lib/constants' + +export const QK_AGENT_PROTOCOL = 'agentProtocol' + +export function useAgentProtocol(filters?: AgentProtocolFilters) { + return useQuery({ + queryKey: [QK_AGENT_PROTOCOL, filters ?? {}], + queryFn: () => agentProtocolService.getAll(filters), + staleTime: STALE_AGENT_PROTOCOL, + select: (res) => res.data ?? [], + }) +} diff --git a/src/hooks/useAgentWorkItems.ts b/src/hooks/useAgentWorkItems.ts new file mode 100644 index 0000000..b005235 --- /dev/null +++ b/src/hooks/useAgentWorkItems.ts @@ -0,0 +1,128 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { agentWorkItemService } from '../services/agentWorkItemService' +import type { AgentWorkItemFilters } from '../provider/IAgentWorkItemProvider' +import type { AgentEditableField } from '../domain/agentWorkItem' +import { useToastStore } from '../stores/toastStore' +import { STALE_AGENT_WORK_ITEMS } from '../lib/constants' +import { QK_AGENT_PROTOCOL } from './useAgentProtocol' + +export const QK_AGENT_WORK_ITEMS = 'agentWorkItems' +export const QK_AGENT_KPIS = 'agentKpis' + +export function useAgentWorkItems(filters?: AgentWorkItemFilters) { + return useQuery({ + queryKey: [QK_AGENT_WORK_ITEMS, filters ?? {}], + queryFn: () => agentWorkItemService.getAll(filters), + staleTime: STALE_AGENT_WORK_ITEMS, + select: (res) => res.data ?? [], + }) +} + +export function useAgentWorkItem(id: string | null) { + return useQuery({ + queryKey: [QK_AGENT_WORK_ITEMS, 'item', id], + queryFn: () => agentWorkItemService.getById(id!), + staleTime: STALE_AGENT_WORK_ITEMS, + enabled: !!id, + select: (res) => res.data ?? null, + }) +} + +/** Die drei Kennzahlen der Auswertung — reagieren auf dieselben Filter wie der Verlauf. */ +export function useAgentKpis(filters?: AgentWorkItemFilters) { + return useQuery({ + queryKey: [QK_AGENT_KPIS, filters ?? {}], + queryFn: () => agentWorkItemService.getKpis(filters), + staleTime: STALE_AGENT_WORK_ITEMS, + select: (res) => res.data, + }) +} + +/** + * Jede Entscheidung verschiebt einen Vorgang zwischen den Reitern, verändert die + * Kennzahlen und schreibt ins Protokoll — alle drei Bestände müssen zusammen + * entwertet werden, sonst zeigt die Oberfläche widersprüchliche Zahlen. + */ +function useWorkItemInvalidation() { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries({ queryKey: [QK_AGENT_WORK_ITEMS] }) + queryClient.invalidateQueries({ queryKey: [QK_AGENT_KPIS] }) + queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] }) + } +} + +export function useApproveWorkItem() { + const invalidate = useWorkItemInvalidation() + return useMutation({ + mutationFn: (id: string) => agentWorkItemService.approve(id), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Vorgang freigegeben.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Freigabe fehlgeschlagen.', 'error') + }, + }) +} + +export function useRejectWorkItem() { + const invalidate = useWorkItemInvalidation() + return useMutation({ + mutationFn: ({ id, reason }: { id: string; reason?: string }) => + agentWorkItemService.reject(id, reason), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Vorgang zurückgewiesen.', 'warning') + }, + onError: () => { + useToastStore.getState().showToast('Zurückweisung fehlgeschlagen.', 'error') + }, + }) +} + +export function useSaveWorkItemEdit() { + const invalidate = useWorkItemInvalidation() + return useMutation({ + mutationFn: ({ id, fields }: { id: string; fields: AgentEditableField[] }) => + agentWorkItemService.saveEdit(id, fields), + onSuccess: () => { + invalidate() + useToastStore + .getState() + .showToast('Änderungen gespeichert. Der Vorgang wartet weiterhin auf die Entscheidung.', 'info') + }, + onError: () => { + useToastStore.getState().showToast('Änderungen konnten nicht gespeichert werden.', 'error') + }, + }) +} + +export function useAnswerWorkItemQuery() { + const invalidate = useWorkItemInvalidation() + return useMutation({ + mutationFn: ({ id, answer }: { id: string; answer: string }) => + agentWorkItemService.answerQuery(id, answer), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Rückfrage beantwortet.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Antwort konnte nicht gespeichert werden.', 'error') + }, + }) +} + +export function useMarkWorkItemDone() { + const invalidate = useWorkItemInvalidation() + return useMutation({ + mutationFn: (id: string) => agentWorkItemService.markDone(id), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Vorgang als erledigt markiert.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Vorgang konnte nicht abgeschlossen werden.', 'error') + }, + }) +} diff --git a/src/hooks/useTeamAgents.ts b/src/hooks/useTeamAgents.ts new file mode 100644 index 0000000..a0c5e01 --- /dev/null +++ b/src/hooks/useTeamAgents.ts @@ -0,0 +1,102 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { teamAgentService } from '../services/teamAgentService' +import type { AgentTask, AgentChannel, AgentSetting } from '../domain/teamAgent' +import { useToastStore } from '../stores/toastStore' +import { STALE_TEAM_AGENTS } from '../lib/constants' +import { QK_AGENT_PROTOCOL } from './useAgentProtocol' + +export const QK_TEAM_AGENTS = 'teamAgents' + +export function useTeamAgents() { + return useQuery({ + queryKey: [QK_TEAM_AGENTS], + queryFn: () => teamAgentService.getAll(), + staleTime: STALE_TEAM_AGENTS, + select: (res) => res.data ?? [], + }) +} + +export function useTeamAgent(id: string | null) { + return useQuery({ + queryKey: [QK_TEAM_AGENTS, id], + queryFn: () => teamAgentService.getById(id!), + staleTime: STALE_TEAM_AGENTS, + enabled: !!id, + select: (res) => res.data ?? null, + }) +} + +/** Jede Mutation berührt auch das Protokoll — beide Bestände zusammen entwerten. */ +function useAgentMutationInvalidation() { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries({ queryKey: [QK_TEAM_AGENTS] }) + queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] }) + } +} + +export function useSetAgentActive() { + const invalidate = useAgentMutationInvalidation() + return useMutation({ + mutationFn: ({ id, active }: { id: string; active: boolean }) => + teamAgentService.setActive(id, active), + onSuccess: (res, variables) => { + invalidate() + useToastStore + .getState() + .showToast( + variables.active + ? `${res.data.name} ist wieder aktiv.` + : `${res.data.name} wurde pausiert.`, + variables.active ? 'success' : 'warning', + ) + }, + onError: () => { + useToastStore.getState().showToast('Status konnte nicht geändert werden.', 'error') + }, + }) +} + +export function useSaveAgentTasks() { + const invalidate = useAgentMutationInvalidation() + return useMutation({ + mutationFn: ({ id, tasks }: { id: string; tasks: AgentTask[] }) => + teamAgentService.saveTasks(id, tasks), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Aufgaben gespeichert.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Aufgaben konnten nicht gespeichert werden.', 'error') + }, + }) +} + +export function useSaveAgentSettings() { + const invalidate = useAgentMutationInvalidation() + return useMutation({ + mutationFn: ({ id, settings }: { id: string; settings: AgentSetting[] }) => + teamAgentService.saveSettings(id, settings), + onSuccess: () => { + invalidate() + useToastStore.getState().showToast('Einstellungen gespeichert.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Einstellungen konnten nicht gespeichert werden.', 'error') + }, + }) +} + +export function useSaveAgentChannels() { + const invalidate = useAgentMutationInvalidation() + return useMutation({ + mutationFn: ({ id, channels }: { id: string; channels: AgentChannel[] }) => + teamAgentService.saveChannels(id, channels), + onSuccess: () => { + invalidate() + }, + onError: () => { + useToastStore.getState().showToast('Kanal konnte nicht geändert werden.', 'error') + }, + }) +} diff --git a/src/hooks/useTeamDemo.ts b/src/hooks/useTeamDemo.ts new file mode 100644 index 0000000..ca456e9 --- /dev/null +++ b/src/hooks/useTeamDemo.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { teamDemoService } from '../services/teamDemoService' +import { useToastStore } from '../stores/toastStore' +import { QK_TEAM_AGENTS } from './useTeamAgents' +import { QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS } from './useAgentWorkItems' +import { QK_AGENT_PROTOCOL } from './useAgentProtocol' +import { QK_AGENT_CONNECTIONS } from './useAgentConnections' + +/** + * «Demo zurücksetzen». Entwertet alle vier Property-On-Bestände, damit die + * Oberfläche unmittelbar den Auslieferungszustand zeigt — ein Reload ist nicht + * nötig. + */ +export function useResetTeamDemo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: () => teamDemoService.reset(), + onSuccess: () => { + for (const key of [QK_TEAM_AGENTS, QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS, QK_AGENT_PROTOCOL, QK_AGENT_CONNECTIONS]) { + queryClient.invalidateQueries({ queryKey: [key] }) + } + useToastStore.getState().showToast('Demo zurückgesetzt. Alle lokalen Änderungen wurden verworfen.', 'success') + }, + onError: () => { + useToastStore.getState().showToast('Demo konnte nicht zurückgesetzt werden.', 'error') + }, + }) +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bf91047..2cb3fcd 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -32,6 +32,10 @@ export const STALE_MATCHES = 2 * 60 * 1000 export const STALE_SIGNALS = 5 * 60 * 1000 export const STALE_MARKET_SIGNALS = 30 * 1000 // aggressive — market leads refresh often export const STALE_REVIEW_QUEUE = 30 * 1000 // aggressive — ops team works in real-time +export const STALE_TEAM_AGENTS = 5 * 60 * 1000 +export const STALE_AGENT_WORK_ITEMS = 15 * 1000 // aggressive — Freigaben ändern die Liste sofort +export const STALE_AGENT_PROTOCOL = 15 * 1000 // aggressive — jede Aktion schreibt einen Eintrag +export const STALE_AGENT_CONNECTIONS = 60 * 1000 // Route paths — single source of truth export const ROUTES = { @@ -42,6 +46,15 @@ export const ROUTES = { MATCH_CENTER: '/supply/match-center', FUTURE_AVAILABILITY: '/supply/future-availability', DATA_QUALITY: '/supply/data-quality', + // Property On — «Teamübersicht». Basispfad bewusst `/supply/team` statt + // `/supply/teamuebersicht`: kürzer, kollidiert nicht mit `PowerOn` in Suchen + // und passt zum Ordner `components/team/`. Hierarchie und Deep-Links bleiben. + TEAM: '/supply/team', + TEAM_PERSONNEL: '/supply/team/personalverwaltung', + TEAM_HISTORY: '/supply/team/bearbeitungsverlauf', + TEAM_HISTORY_DONE: '/supply/team/bearbeitungsverlauf/erledigte-auftraege', + TEAM_HISTORY_PENDING: '/supply/team/bearbeitungsverlauf/pendente-anfragen', + TEAM_CONNECTIONS: '/supply/team/kanaele-systeme', }, DEMAND: { AI_SEARCH: '/demand/ai-search', @@ -132,6 +145,198 @@ export const DATA_QUALITY_LABELS: Record = { INCOMPLETE: 'Unvollständig', } +// ───────────────────────────────────────────────────────────────────────────── +// Property On — «Teamübersicht» +// +// Schweizer Schreibweise, kein Eszett. Sprache der Zielgruppe: Immobilien- +// bewirtschafter, nicht KI-Entwickler — deshalb «Aufgabe», «Freigabe», +// «Systemzugang» statt Prompt, Token, Tool Call. +// ───────────────────────────────────────────────────────────────────────────── + +export const AGENT_STATUS_LABELS: Record = { + ACTIVE: 'Aktiv', + BUILDING: 'Im Aufbau', + PAUSED: 'Pausiert', +} + +export const AGENT_AUTONOMY_LABELS: Record = { + AUTONOMOUS: 'Autonom', + APPROVAL_REQUIRED: 'Freigabe erforderlich', + PROPOSAL_ONLY: 'Nur Vorschlag', +} + +export const AGENT_CHANNEL_LABELS: Record = { + EMAIL: 'E-Mail', + TEAMS: 'Microsoft Teams', + WHATSAPP: 'WhatsApp Business', + WORKSPACE_CHAT: 'Workspace-Chat', + CALENDAR: 'Kalender', + WEB_FORM: 'Website-Formular', + PORTAL_LEAD_MAIL: 'Portal-Lead-Mail', + PHONE_VOICE: 'Telefon / Voice', + PHONE_NOTE: 'Telefonnotiz', + AUDIO_OUTPUT: 'Audioausgabe', + STRUCTURED_MAIL_FALLBACK: 'Strukturierter Mail-Fallback', +} + +export const AGENT_CHANNEL_DIRECTION_LABELS: Record = { + INBOUND: 'Eingang', + OUTBOUND: 'Ausgang', + BOTH: 'Eingang und Ausgang', +} + +export const AGENT_CONNECTION_STATUS_LABELS: Record = { + CONNECTED: 'Verbunden', + DISCONNECTED: 'Nicht verbunden', + ROADMAP: 'Geplant', +} + +export const AGENT_SYSTEM_LABELS: Record = { + M365_EXCHANGE: 'Microsoft 365 / Exchange', + OUTLOOK: 'Outlook', + SHAREPOINT: 'SharePoint', + ONEDRIVE: 'OneDrive', + FILESHARE: 'Windows-Fileshare', + DMS: 'Dokumentenablage (DMS)', + CALENDAR: 'Kalender', + IMMOTOP2: 'ImmoTop2', + ABACUS_ABAIMMO: 'Abacus AbaImmo', + RIMO_R5: 'Rimo R5', + GARAIO_REM: 'Garaio REM', + CRM: 'CRM', + ZEFIX: 'Zefix', + SHAB: 'SHAB', + PUBLIC_WEB: 'Öffentliche Webquellen', + PORTAL_LEAD_INBOX: 'Portal-Lead-Postfach', + WORKSPACE: 'Property-On-Workspace', +} + +export const AGENT_ACCESS_LABELS: Record = { + READ: 'Lesen', + WRITE: 'Schreiben', + READ_WRITE: 'Lesen und schreiben', +} + +export const AGENT_WORK_ITEM_STATUS_LABELS: Record = { + PENDING: 'Offen', + APPROVED: 'Freigegeben', + REJECTED: 'Zurückgewiesen', + EDITING: 'In Bearbeitung', + COMPLETED: 'Abgeschlossen', +} + +export const AGENT_PRIORITY_LABELS: Record = { + LOW: 'Niedrig', + MEDIUM: 'Mittel', + HIGH: 'Hoch', + CRITICAL: 'Kritisch', +} + +export const AGENT_WORK_ITEM_KIND_LABELS: Record = { + MESSAGE_TRIAGE: 'Nachricht kategorisiert', + REMINDER_CREATED: 'Reminder erstellt', + CONTRACT_ANSWER: 'Vertragsauskunft', + LOCATION_MODULE: 'Lagebaustein', + INQUIRY_QUALIFIED: 'Anfrage qualifiziert', + AUTO_REJECTION: 'Automatische Absage', + APPOINTMENT: 'Termin', + BRIEFING: 'Besichtigungs-Briefing', + RECAP: 'Gesprächsprotokoll', + MARKET_SIGNAL: 'Marktsignal', + ESCALATION: 'Eskalation', + DOC_GAP: 'Dokumentationslücke', + SYSTEM_ISSUE: 'Systemproblem', +} + +export const AGENT_DOMAIN_AREA_LABELS: Record = { + CONTRACT_DEADLINES: 'Fristen und Verträge', + DOCUMENTATION: 'Dokumentation', + INQUIRIES: 'Anfragen', + VIEWINGS: 'Besichtigungen', + LOCATION: 'Lage', + MARKET: 'Markt', +} + +export const AGENT_WORK_ITEM_ACTION_LABELS: Record = { + APPROVE: 'Freigeben', + ONE_CLICK_CONFIRM: 'Mit einem Klick bestätigen', + DECIDE: 'Entscheidung treffen', + EDIT: 'Anpassen', + REJECT: 'Zurückweisen', + ANSWER_QUERY: 'Rückfrage beantworten', + OPEN_SOURCE: 'Quelle öffnen', + MARK_DONE: 'Als erledigt markieren', +} + +export const AGENT_PROTOCOL_STATUS_LABELS: Record = { + SUCCESS: 'Erfolgreich', + WARNING: 'Warnung', + ERROR: 'Fehler', + INFO: 'Hinweis', +} + +export const AGENT_PROTOCOL_EVENT_LABELS: Record = { + TASK_RUN: 'Aufgabe ausgeführt', + MESSAGE_PROCESSED: 'Nachricht verarbeitet', + REMINDER_SENT: 'Erinnerung versendet', + ESCALATION: 'Eskalation', + APPROVAL: 'Freigabe', + REJECTION: 'Zurückweisung', + EDIT: 'Anpassung', + SETTINGS_SAVED: 'Einstellungen gespeichert', + TASKS_SAVED: 'Aufgaben gespeichert', + AGENT_PAUSED: 'Mitarbeiter pausiert', + AGENT_RESUMED: 'Mitarbeiter reaktiviert', + CHANNEL_TESTED: 'Verbindung getestet', + CHANNEL_TOGGLED: 'Kanal umgeschaltet', + CONNECTION_CREATED: 'Verbindung angelegt', + CONNECTION_REMOVED: 'Verbindung getrennt', + DEMO_RESET: 'Demo zurückgesetzt', +} + +export const AGENT_CONNECTION_CATEGORY_LABELS: Record = { + EMAIL_M365: 'E-Mail / Microsoft 365', + WHATSAPP: 'WhatsApp Business', + TEAMS: 'Microsoft Teams', + CALENDAR: 'Kalender', + DOCUMENT_STORE: 'Dokumentenablage', + PHONE_VOICE: 'Telefon / Voice', + ERP: 'ERP', + CRM: 'CRM', + PUBLIC_SOURCES: 'Öffentliche Datenquellen', +} + +export const AGENT_ERP_VENDOR_LABELS: Record = { + IMMOTOP2: 'ImmoTop2', + ABACUS_ABAIMMO: 'Abacus AbaImmo', + RIMO_R5: 'Rimo R5', + GARAIO_REM: 'Garaio REM', + OTHER: 'Anderes ERP', +} + +export const AGENT_SETTING_GROUP_LABELS: Record = { + GENERAL: 'Allgemein', + APPROVAL: 'Freigabe und Eskalation', + DELIVERY: 'Zustellung', + SPECIFIC: 'Fachliche Einstellungen', +} + +/** Zeitraumfilter für Auswertung und Bearbeitungsverlauf (§4.2, §5.3). */ +export const AGENT_PERIOD_LABELS: Record = { + TODAY: 'Heute', + WEEK: 'Diese Woche', + MONTH: 'Dieser Monat', + YEAR: 'Dieses Jahr', + ALL: 'Gesamter Zeitraum', +} + +/** Simulierte Ladezeit für Demo-Aktionen (§19.1: 300–800 ms). */ +export const AGENT_SIM_DELAY_MS = 420 +export const AGENT_SIM_DELAY_SLOW_MS = 760 + +/** Hinweis, der auf jeder Property-On-Seite dezent mitläuft (§19.3). */ +export const AGENT_DEMO_NOTICE = 'Demo-Modus – keine echten Daten oder Systemverbindungen.' + // Swiss CRB/BKP fit-out investment benchmarks (CHF/m², tenant's cost) export const FIT_OUT_COST_CHF_PER_SQM: Record = { SHELL: { min: 800, max: 1500 }, diff --git a/src/lib/teamClock.ts b/src/lib/teamClock.ts new file mode 100644 index 0000000..0abd728 --- /dev/null +++ b/src/lib/teamClock.ts @@ -0,0 +1,71 @@ +/** + * Property On — Demo-Uhr. + * + * Die Mockdaten sind fachlich auf den 20.05.2026 verankert. Würden die + * Zeitraumfilter «heute / diese Woche / dieser Monat» gegen die echte Systemzeit + * rechnen, wären sie an jedem anderen Kalendertag leer und die Auswertung sähe + * kaputt aus, obwohl sie korrekt arbeitet. + * + * Deshalb läuft der gesamte Property-On-Bereich gegen eine Demo-Uhr: sie startet + * am Anker und schreitet ab Anwendungsstart in Echtzeit fort. Damit landet jede + * Freigabe, die der Nutzer während der Demo auslöst, verlässlich im Bucket + * «heute» — und die Seed-Daten liegen dort, wo der Katalog sie erwartet. + * + * Ausschliesslich für Property On. Der Rest der Anwendung rechnet unverändert + * mit `new Date()`. + */ + +import { MOCK_TODAY } from '../mock-data/teamAgents' + +const ANCHOR_MS = new Date(MOCK_TODAY).getTime() +const BOOT_MS = Date.now() + +/** Aktueller Zeitpunkt der Demo-Uhr. */ +export function teamNow(): Date { + return new Date(ANCHOR_MS + (Date.now() - BOOT_MS)) +} + +/** Aktueller Zeitpunkt als ISO-String — für Zeitstempel in Vorgängen und Protokoll. */ +export function teamNowIso(): string { + return teamNow().toISOString() +} + +/** Datum und Uhrzeit, de-CH: «20.05.2026, 08:15». */ +export function formatTeamDateTime(iso: string): string { + return new Intl.DateTimeFormat('de-CH', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(iso)) +} + +/** Nur die Uhrzeit, de-CH: «08:15». */ +export function formatTeamTime(iso: string): string { + return new Intl.DateTimeFormat('de-CH', { hour: '2-digit', minute: '2-digit' }).format(new Date(iso)) +} + +/** + * Relative Angabe gegen die Demo-Uhr. + * + * `formatRelativeDate` aus `lib/utils.ts` rechnet gegen `Date.now()` und würde + * für die auf den 20.05.2026 verankerten Bestände durchgehend Abstände von + * Monaten ausgeben — jeder Vorgang sähe veraltet aus. + */ +export function formatTeamRelative(iso: string): string { + const diff = teamNow().getTime() - new Date(iso).getTime() + if (diff < 0) return formatTeamDateTime(iso) + + const minutes = Math.floor(diff / 60_000) + if (minutes < 1) return 'gerade eben' + if (minutes < 60) return `vor ${minutes} Minute${minutes === 1 ? '' : 'n'}` + + const hours = Math.floor(minutes / 60) + if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? '' : 'n'}` + + const days = Math.floor(hours / 24) + if (days < 7) return `vor ${days} Tag${days === 1 ? '' : 'en'}` + + return formatTeamDateTime(iso) +} diff --git a/src/mock-data/agentConnections.ts b/src/mock-data/agentConnections.ts new file mode 100644 index 0000000..73a7181 --- /dev/null +++ b/src/mock-data/agentConnections.ts @@ -0,0 +1,436 @@ +/** + * Property On — Subreiter «Kanäle & Systeme»: die zentralen Verbindungen der + * Organisation. + * + * Genau neun Einträge, einer je `AgentConnectionCategory`, in der Reihenfolge, in + * der die Seite sie rendert. Zwei Verbindungen stehen bewusst auf + * `DISCONNECTED` — Teams und das Bewirtschaftungssystem — damit der + * Verbindungsassistent eine echte Aufgabe hat; die Telefonie ist als + * `ROADMAP` ausgewiesen. + * + * `usedByAgentIds` ist aus den Kanälen und Systemen der sieben Personalblätter + * abgeleitet und nicht frei gesetzt: E-Mail nutzen alle, den Kalender die vier + * Mitarbeiter mit Terminbezug, die Dokumentenablage alle mit Aktenbezug. + * Reihenfolge alphabetisch nach Vorname, wie in `mockTeamAgents`. + * + * Alle Verbindungen sind Frontend-Simulation — es werden nie echte Zugangsdaten + * erfasst und nie echte Systeme kontaktiert. Zeitanker: 20.05.2026. + */ + +import type { AgentConnection } from '../domain/agentConnection' +import { AgentConnectionCategory, AgentErpVendor } from '../domain/agentConnection' +import { AgentAccessLevel, AgentConnectionStatus } from '../domain/teamAgent' +import { AGENT_IDS } from './teamAgents' + +export const mockAgentConnections: AgentConnection[] = [ + // ── E-Mail ────────────────────────────────────────────────────────────────── + { + id: 'conn-email-m365', + category: AgentConnectionCategory.EMAIL_M365, + name: 'Microsoft 365 — gemeinsame Postfächer', + description: + 'Die geteilten Postfächer von Bewirtschaftung und Vermarktung: Fristenmeldungen, Anfragen von Interessenten, Rückfragen, Absagen und Terminvorschläge laufen über diese vier Adressen. Eingehende Post wird gelesen und dem richtigen Objekt zugeordnet; ausgehende Post folgt der Regel des jeweiligen Mitarbeiters — Erinnerungen gehen selbständig hinaus, Eskalationen erst nach Freigabe.', + status: AgentConnectionStatus.CONNECTED, + accountCount: 4, + usedByAgentIds: [ + AGENT_IDS.BRUNO, + AGENT_IDS.FERDI, + AGENT_IDS.LEA, + AGENT_IDS.LIVIA, + AGENT_IDS.NORA, + AGENT_IDS.RETO, + AGENT_IDS.SINA, + ], + lastSyncAt: '2026-05-20T08:42:00.000Z', + connectionLabel: 'Postfächer Zürich, Winterthur, Basel', + permissions: [ + { + id: 'conn-email-m365-perm-01', + label: 'Postfach lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-email-m365-perm-02', + label: 'Nachrichten senden', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-email-m365-perm-03', + label: 'Entwürfe zur Freigabe ablegen', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-email-m365-perm-04', + label: 'Anhänge öffnen und ablegen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-email-m365-perm-05', + label: 'Ordner und Regeln des Postfachs ändern', + access: AgentAccessLevel.READ_WRITE, + granted: false, + }, + ], + }, + + // ── WhatsApp ──────────────────────────────────────────────────────────────── + { + id: 'conn-whatsapp', + category: AgentConnectionCategory.WHATSAPP, + name: 'WhatsApp Business — Diensthandys', + description: + 'Der kurze Draht zu den Personen unterwegs: Meldung aufs Diensthandy, wenn eine Frist in weniger als 30 Tagen abläuft und am Arbeitsplatz niemand erreichbar ist, sowie Eingang der Sprachmemos direkt nach einem Aussentermin — etwa nach der Begehung an der Bahnhofstrasse 44 in Zürich (OBJ-2451).', + status: AgentConnectionStatus.CONNECTED, + accountCount: 2, + usedByAgentIds: [AGENT_IDS.FERDI, AGENT_IDS.RETO], + lastSyncAt: '2026-05-19T17:35:00.000Z', + connectionLabel: 'Diensthandys Bewirtschaftung und Vermarktung', + permissions: [ + { + id: 'conn-whatsapp-perm-01', + label: 'Eingehende Nachrichten lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-whatsapp-perm-02', + label: 'Kurzmeldung senden', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-whatsapp-perm-03', + label: 'Sprachmemos abholen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-whatsapp-perm-04', + label: 'Kontaktliste des Geräts lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + ], + }, + + // ── Teams ─────────────────────────────────────────────────────────────────── + { + id: 'conn-teams', + category: AgentConnectionCategory.TEAMS, + name: 'Microsoft Teams — Kanal Marktbeobachtung', + description: + 'Vorgesehen für die Kurzmeldung in den Teamkanal, sobald ein Marktsignal mit hoher Konfidenz auftaucht — zum Beispiel eine angekündigte Filialschliessung in Basel. Die Verbindung ist noch nicht eingerichtet; bis dahin erscheinen die Signale ausschliesslich in der Wochen-Mail vom Dienstag.', + status: AgentConnectionStatus.DISCONNECTED, + accountCount: 0, + usedByAgentIds: [AGENT_IDS.NORA], + permissions: [ + { + id: 'conn-teams-perm-01', + label: 'Kanalbeiträge lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-teams-perm-02', + label: 'In Kanal schreiben', + access: AgentAccessLevel.WRITE, + granted: false, + }, + { + id: 'conn-teams-perm-03', + label: 'Kanalmitglieder lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-teams-perm-04', + label: 'Dateien im Kanal ablegen', + access: AgentAccessLevel.WRITE, + granted: false, + }, + ], + }, + + // ── Kalender ──────────────────────────────────────────────────────────────── + { + id: 'conn-calendar', + category: AgentConnectionCategory.CALENDAR, + name: 'Kalender der Standortteams', + description: + 'Fristenkalender der Bewirtschaftung, Besichtigungskalender der Vermarktung und der Terminkalender für Aussentermine. Gelesen werden freie Zeitfenster und bestehende Termine; geschrieben werden bestätigte Fristen mit ihren Vorlaufmarken, vorläufige Reservationen für Terminvorschläge und die bestätigte Besichtigung samt Erinnerung. Fremde Termine werden nie verschoben.', + status: AgentConnectionStatus.CONNECTED, + accountCount: 3, + usedByAgentIds: [AGENT_IDS.BRUNO, AGENT_IDS.FERDI, AGENT_IDS.LEA, AGENT_IDS.RETO], + lastSyncAt: '2026-05-20T08:15:00.000Z', + connectionLabel: 'Fristen-, Besichtigungs- und Terminkalender', + permissions: [ + { + id: 'conn-calendar-perm-01', + label: 'Termine lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-calendar-perm-02', + label: 'Verfügbarkeiten und freie Zeitfenster abfragen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-calendar-perm-03', + label: 'In Kalender schreiben', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-calendar-perm-04', + label: 'Zeitfenster vorläufig reservieren', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-calendar-perm-05', + label: 'Fremde Termine verschieben oder löschen', + access: AgentAccessLevel.READ_WRITE, + granted: false, + }, + ], + }, + + // ── Dokumentenablage ──────────────────────────────────────────────────────── + { + id: 'conn-document-store', + category: AgentConnectionCategory.DOCUMENT_STORE, + name: 'Dokumentenablage und Standortbibliotheken', + description: + 'Mietverträge, Nachträge, Zusatzvereinbarungen, Übergabeprotokolle, Hausordnungen und Objektunterlagen — aus der zentralen Ablage, den Standortbibliotheken von Zürich, Winterthur und St. Gallen sowie den historischen Vertragsordnern der übernommenen Mandate. Jede Aussage aus einem Dokument wird mit Dateiname, Seite und Zitat belegt.', + status: AgentConnectionStatus.CONNECTED, + accountCount: 4, + usedByAgentIds: [ + AGENT_IDS.BRUNO, + AGENT_IDS.FERDI, + AGENT_IDS.LIVIA, + AGENT_IDS.RETO, + AGENT_IDS.SINA, + ], + lastSyncAt: '2026-05-20T05:30:00.000Z', + connectionLabel: 'Ablage Bewirtschaftung und Standortbibliotheken', + permissions: [ + { + id: 'conn-document-store-perm-01', + label: 'Dokumente lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-document-store-perm-02', + label: 'Ordnerstruktur und Objektzuordnung lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-document-store-perm-03', + label: 'Fundstellen und Seitenverweise festhalten', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-document-store-perm-04', + label: 'Protokolle in der Fristenliste ablegen', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-document-store-perm-05', + label: 'Dokumente verschieben oder löschen', + access: AgentAccessLevel.READ_WRITE, + granted: false, + }, + ], + }, + + // ── Telefonie ─────────────────────────────────────────────────────────────── + { + id: 'conn-phone-voice', + category: AgentConnectionCategory.PHONE_VOICE, + name: 'Telefonanlage und Diktatlinie', + description: + 'Geplante Anbindung der Telefonanlage: Anrufe von Interessenten am Empfang in Zürich und Winterthur sowie die Diktatlinie für Sprachmemos nach einem Aussentermin. Bis zur Freigabe erfasst der Empfang die Telefonnotizen von Hand, und die Sprachmemos kommen über das Diensthandy herein.', + status: AgentConnectionStatus.ROADMAP, + accountCount: 0, + usedByAgentIds: [AGENT_IDS.LEA, AGENT_IDS.RETO], + permissions: [ + { + id: 'conn-phone-voice-perm-01', + label: 'Anrufliste lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-phone-voice-perm-02', + label: 'Sprachmemos abholen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-phone-voice-perm-03', + label: 'Gesprächsnotiz zum Vorgang ablegen', + access: AgentAccessLevel.WRITE, + granted: false, + }, + { + id: 'conn-phone-voice-perm-04', + label: 'Ansage für den Empfang abspielen', + access: AgentAccessLevel.WRITE, + granted: false, + }, + ], + }, + + // ── Bewirtschaftungssystem ────────────────────────────────────────────────── + { + id: 'conn-erp-immotop2', + category: AgentConnectionCategory.ERP, + name: 'Bewirtschaftungssystem ImmoTop2', + description: + 'Objekt- und Mieterstammdaten, Vertragslaufzeiten, Verfügbarkeiten, Must-Kriterien der ausgeschriebenen Flächen und die Terminexporte. Der Zugang ist noch nicht eingerichtet: bis dahin stützen sich Fristen, Vertragsauskünfte und Anfragenprüfung auf die Excel-Terminlisten und die Unterlagen der Standortteams. Der Verbindungsassistent richtet den Lesezugang ein.', + status: AgentConnectionStatus.DISCONNECTED, + accountCount: 0, + usedByAgentIds: [ + AGENT_IDS.FERDI, + AGENT_IDS.LEA, + AGENT_IDS.LIVIA, + AGENT_IDS.RETO, + AGENT_IDS.SINA, + ], + vendor: AgentErpVendor.IMMOTOP2, + permissions: [ + { + id: 'conn-erp-immotop2-perm-01', + label: 'Objektstammdaten lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-erp-immotop2-perm-02', + label: 'Mieter- und Vertragsdaten lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-erp-immotop2-perm-03', + label: 'Terminexporte abholen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-erp-immotop2-perm-04', + label: 'Verfügbarkeiten und Flächenangaben lesen', + access: AgentAccessLevel.READ, + granted: false, + }, + { + id: 'conn-erp-immotop2-perm-05', + label: 'Stammdaten ändern', + access: AgentAccessLevel.WRITE, + granted: false, + }, + ], + }, + + // ── CRM ───────────────────────────────────────────────────────────────────── + { + id: 'conn-crm', + category: AgentConnectionCategory.CRM, + name: 'CRM der Vermarktung', + description: + 'Führt Interessenten, Bedarfsprofile, Anfrageverlauf und Terminstatus je Fläche und erkennt, wenn dieselbe Firma bereits über Portal, Formular und Telefon angefragt hat. Hier landen auch die freigegebenen Leads der Marktbeobachtung — zuletzt die Expansion der Vollenweider Gastro AG Richtung Winterthur.', + status: AgentConnectionStatus.CONNECTED, + accountCount: 1, + usedByAgentIds: [AGENT_IDS.LEA, AGENT_IDS.NORA, AGENT_IDS.RETO], + lastSyncAt: '2026-05-19T19:05:00.000Z', + connectionLabel: 'Vermarktung Zürich', + permissions: [ + { + id: 'conn-crm-perm-01', + label: 'Kontakte und Firmen lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-crm-perm-02', + label: 'Anfragen und Notizen schreiben', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-crm-perm-03', + label: 'Statuswechsel setzen', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-crm-perm-04', + label: 'Freigegebene Leads übergeben', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-crm-perm-05', + label: 'Datensätze löschen', + access: AgentAccessLevel.WRITE, + granted: false, + }, + ], + }, + + // ── Öffentliche Quellen ───────────────────────────────────────────────────── + { + id: 'conn-public-sources', + category: AgentConnectionCategory.PUBLIC_SOURCES, + name: 'Öffentliche Quellen — SHAB, Zefix und Web', + description: + 'Amtliche Meldungen zu Gründungen, Sitzverlegungen und Liquidationen, der öffentliche Handelsregisterindex und frei zugängliche Firmenwebseiten, Standortmeldungen und Medienberichte aus Zürich, Winterthur, Zug, Basel, Bern, Luzern und St. Gallen. Grundlage für Lagebeschreibungen, Besichtigungs-Briefings und Marktsignale — jede Angabe wird mit Quelle und Datum ausgewiesen.', + status: AgentConnectionStatus.CONNECTED, + accountCount: 3, + usedByAgentIds: [AGENT_IDS.BRUNO, AGENT_IDS.LIVIA, AGENT_IDS.NORA], + lastSyncAt: '2026-05-20T06:00:00.000Z', + connectionLabel: 'Marktbeobachtung Deutschschweiz', + permissions: [ + { + id: 'conn-public-sources-perm-01', + label: 'Amtliche Meldungen lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-public-sources-perm-02', + label: 'Handelsregisterindex abfragen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-public-sources-perm-03', + label: 'Frei zugängliche Webseiten lesen', + access: AgentAccessLevel.READ, + granted: true, + }, + { + id: 'conn-public-sources-perm-04', + label: 'Quellenangabe mit Datum festhalten', + access: AgentAccessLevel.WRITE, + granted: true, + }, + { + id: 'conn-public-sources-perm-05', + label: 'Formulare ausfüllen oder Inhalte veröffentlichen', + access: AgentAccessLevel.WRITE, + granted: false, + }, + ], + }, +] diff --git a/src/mock-data/agentProtocol.ts b/src/mock-data/agentProtocol.ts new file mode 100644 index 0000000..e82d4a4 --- /dev/null +++ b/src/mock-data/agentProtocol.ts @@ -0,0 +1,1192 @@ +/** + * Property On — Protokoll der sieben digitalen Mitarbeiter. + * + * Jeder Eintrag hält fest, was ein Agent getan hat, wer es ausgelöst hat und mit + * welchem Ergebnis. Die Liste ist absteigend nach Zeitstempel sortiert; die + * Auswertung filtert nach «heute», «diese Woche», «dieser Monat» und «dieses + * Jahr» und findet deshalb in jedem Zeitraum Einträge. + * + * Zeitanker sämtlicher Property-On-Mockdaten ist der 20.05.2026 (`MOCK_TODAY`). + * Kein Zeitstempel liegt danach. Bedienende Person ist durchgängig Martina + * Brunner; sie erteilt Freigaben, weist zurück, passt an und speichert + * Einstellungen. + */ + +import type { AgentProtocolEntry } from '../domain/agentProtocol' +import { + AgentApprovalDecision, + AgentProtocolEventType, + AgentProtocolStatus, + AgentTriggerSource, +} from '../domain/agentProtocol' +import { AgentChannelType } from '../domain/teamAgent' +import { AGENT_IDS } from './teamAgents' + +/** Klarname der bedienenden Person in allen von Hand ausgelösten Einträgen. */ +const OPERATOR = 'Martina Brunner' + +export const mockAgentProtocol: AgentProtocolEntry[] = [ + // ── Heute, 20.05.2026 ─────────────────────────────────────────────────────── + { + id: 'proto-001', + agentId: AGENT_IDS.RETO, + timestamp: '2026-05-20T16:42:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Sprachmemo zur Besichtigung Technoparkstrasse 9 verarbeitet', + description: + 'Memo von 4 Minuten 12 Sekunden übertragen, bereinigt und in Kurzfazit, Feedback, Einwände, Zusagen und offene Punkte gegliedert. Protokoll, Gesprächsnotiz und drei Folgeaufgaben liegen zur 1-Klick-Freigabe bereit.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2478', + channel: AgentChannelType.WHATSAPP, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Eingang', value: 'Sprachmemo, 4 Min. 12 Sek., Diktatlinie Vermarktung' }, + { + label: 'Termin', + value: 'Besichtigung 20.05.2026, 15:00–15:45, Technoparkstrasse 9, Winterthur', + }, + { label: 'Interessent', value: 'Neuhegi Medtech AG, Winterthur — S. Zollinger, Leitung Betrieb' }, + { label: 'Objektbezug', value: 'OBJ-2478, Büroetage 3. OG, 640 m²' }, + ], + processingSteps: [ + { + id: 'proto-001-ps-1', + label: 'Memo entgegengenommen und dem Termin zugeordnet', + detail: 'Zuordnung über Eingangszeit und Objektbezug im Kalendereintrag', + }, + { + id: 'proto-001-ps-2', + label: 'Gesprochenes in Text übertragen', + detail: 'Zwei unsichere Stellen markiert statt geraten', + }, + { id: 'proto-001-ps-3', label: 'Füllwörter, Versprecher und Wiederholungen entfernt' }, + { + id: 'proto-001-ps-4', + label: 'Aussagen in Feedback, Einwände, Zusagen und offene Punkte sortiert', + }, + { + id: 'proto-001-ps-5', + label: 'Folgeaufgaben mit Frist und zuständiger Person abgeleitet', + }, + { id: 'proto-001-ps-6', label: 'Paket zur 1-Klick-Freigabe vorgelegt' }, + ], + output: [ + { + label: 'Kurzfazit', + value: + 'Fläche und Ausbaustandard überzeugen. Offen bleiben der Mietzins pro m² und der Bezugstermin per 01.10.2026.', + }, + { + label: 'Einwände', + value: [ + 'Mietzins rund 8 % über dem internen Budget des Interessenten', + 'Anlieferung nur über den Hinterhof möglich', + 'Parkplätze am Standort erst ab 2027 gesichert', + ], + }, + { + label: 'Folgeaufgaben', + value: [ + 'Grundrisse 3. OG bis 22.05.2026 zustellen — Marco Steinegger', + 'Mietzins intern klären bis 26.05.2026 — Nadine Brunner', + 'Zweitbesichtigung mit der Technikleitung ansetzen', + ], + }, + ], + sourceReferences: [ + { + id: 'proto-001-src-1', + label: 'Kalendereintrag Besichtigung', + documentName: 'Terminkalender Vermarktung Winterthur', + locator: 'Eintrag 20.05.2026, 15:00', + }, + { + id: 'proto-001-src-2', + label: 'Objektdossier OBJ-2478', + documentName: 'Ausbaubeschrieb Technoparkstrasse 9.pdf', + locator: 'Seite 4', + quote: '«Nutzfläche 3. OG: 640 m², Ausbaustandard erhöht, Bezug nach Vereinbarung»', + }, + ], + }, + { + id: 'proto-002', + agentId: AGENT_IDS.RETO, + timestamp: '2026-05-20T16:05:00.000Z', + eventType: AgentProtocolEventType.APPROVAL, + title: 'Recap-Paket zur Besichtigung Industriestrasse 14 freigegeben', + description: + 'Protokoll, Gesprächsnotiz und vier Folgeaufgaben wurden mit einem Klick bestätigt. Danach ist das Paket ins Objektdossier, ins Kontaktjournal und in die Aufgabenliste übertragen worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2588', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.APPROVED, + }, + { + id: 'proto-003', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-20T15:20:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: 'Portal-Anfrage zur Hardturmstrasse 118 qualifiziert', + description: + 'Lead-Mail einer Plattform erfasst, in ein einheitliches Bedarfsprofil überführt und gegen die Must-Kriterien der Fläche geprüft. Alle fünf Kriterien sind erfüllt; das Kurzprofil liegt zur Entscheidung bereit.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2512', + channel: AgentChannelType.PORTAL_LEAD_MAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Herkunftskanal', value: 'Portal-Lead-Mail, Eingang 20.05.2026, 15:12' }, + { label: 'Interessent', value: 'Limmat Robotics AG, Zürich — R. Amrein, Geschäftsleitung' }, + { + label: 'Anfragetext', + value: + '«Wir suchen per Herbst 2026 rund 500 m² Bürofläche in Zürich West, gerne mit Werkstattanteil im Erdgeschoss.»', + }, + { label: 'Objektbezug', value: 'OBJ-2512, Hardturmstrasse 118, Zürich Kreis 5' }, + ], + processingSteps: [ + { + id: 'proto-003-ps-1', + label: 'Lead-Mail erfasst und der ausgeschriebenen Fläche zugeordnet', + }, + { + id: 'proto-003-ps-2', + label: 'Freitext in ein Bedarfsprofil überführt', + detail: 'Fläche, Nutzungsart, Wunschtermin, Budgetrahmen und Vertragsdauer übernommen', + }, + { + id: 'proto-003-ps-3', + label: 'Abgleich über die letzten 90 Tage auf Mehrfachanfragen', + detail: 'Keine Übereinstimmung über Portal, Formular oder Telefon gefunden', + }, + { id: 'proto-003-ps-4', label: 'Bedarfsprofil gegen die Must-Kriterien der Fläche geprüft' }, + { id: 'proto-003-ps-5', label: 'Kurzprofil zur Entscheidung vorgelegt' }, + ], + output: [ + { label: 'Bedarfsprofil', value: '480–560 m², Büro mit Werkstattanteil, Bezug ab 01.10.2026' }, + { + label: 'Kriterienprüfung', + value: [ + 'Flächenbedarf im ausgeschriebenen Rahmen — erfüllt', + 'Nutzungsart für das Objekt zulässig — erfüllt', + 'Wunschtermin passt zur Verfügbarkeit — erfüllt', + 'Budgetrahmen erreicht die Mietpreisvorstellung — erfüllt', + 'Bonitätsnachweis kann beigebracht werden — erfüllt', + ], + }, + { label: 'Nächster Schritt', value: 'Freigabe für Terminvorschläge einholen' }, + ], + sourceReferences: [ + { + id: 'proto-003-src-1', + label: 'Must-Kriterien der Fläche', + documentName: 'Vermarktungsauftrag Hardturmstrasse 118.pdf', + locator: 'Seite 2, Abschnitt «Zielmieterschaft»', + quote: '«Mindestvertragsdauer fünf Jahre, Nutzung Büro und stiller Werkstattbetrieb zulässig»', + }, + { + id: 'proto-003-src-2', + label: 'Flächenstammdaten OBJ-2512', + documentName: 'Bewirtschaftungssystem — Objektblatt OBJ-2512', + locator: 'Verfügbarkeit ab 01.09.2026', + }, + ], + }, + { + id: 'proto-004', + agentId: AGENT_IDS.BRUNO, + timestamp: '2026-05-20T14:35:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Briefing zum Aussentermin Aeschenvorstadt 27 mit Lücken zugestellt', + description: + 'Das Briefing ging 95 Minuten vor dem Termin hinaus. Nebenkosten und Parkplatzschlüssel sind älter als der hinterlegte Zeitraum und deshalb sichtbar als fehlende Information gekennzeichnet statt geschätzt.', + status: AgentProtocolStatus.WARNING, + objectId: 'OBJ-2536', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-005', + agentId: AGENT_IDS.NORA, + timestamp: '2026-05-20T13:10:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Stündlicher Quellenlauf ohne verifiziertes Signal', + description: + 'Elf neue Meldungen aus den freigegebenen Quellen gesichtet und eingeordnet. Keine erreicht die Mindestkonfidenz von 70 %; vier Meldungen liegen zur Sichtung in der Review-Queue der Marktbeobachtung.', + status: AgentProtocolStatus.INFO, + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-006', + agentId: AGENT_IDS.BRUNO, + timestamp: '2026-05-20T12:00:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Briefing samt Audiofassung für die Baarerstrasse 61 erstellt', + description: + 'Zwei Stunden vor dem Termin selbständig angelaufen: Objektkennzahlen, Einordnung des Gegenübers, drei Verkaufsargumente und drei erwartbare Einwände auf einer Seite gebündelt und mit gesprochener Fassung für die Anfahrt zugestellt.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2547', + channel: AgentChannelType.AUDIO_OUTPUT, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Termin', value: 'Aussentermin 20.05.2026, 14:00, Baarerstrasse 61, Zug' }, + { + label: 'Gegenüber', + value: 'Perrin & Fils Sàrl, Lausanne — Verlegung des Sitzes nach Zug geplant', + }, + { label: 'Objektbezug', value: 'OBJ-2547, Büroetage 4. OG, 410 m², frei ab 01.09.2026' }, + { + label: 'Teilnehmende', + value: ['Nadine Brunner, Vermarktung Zürich', 'J.-M. Perrin, Geschäftsführung'], + }, + ], + processingSteps: [ + { id: 'proto-006-ps-1', label: 'Termin aus dem Kalender eingelesen und Objektbezug erkannt' }, + { + id: 'proto-006-ps-2', + label: 'Objektinformationen zusammengetragen', + detail: 'Fläche, Ausbau, Mietzins, Nebenkosten, Verfügbarkeit, Erschliessung, Vermarktungshistorie', + }, + { + id: 'proto-006-ps-3', + label: 'Gegenüber über öffentlich zugängliche Quellen eingeordnet', + detail: 'Handelsregistereintrag, amtliche Meldungen und Firmenwebsite, je mit Quellenangabe', + }, + { id: 'proto-006-ps-4', label: 'Drei Verkaufsargumente auf das Gegenüber zugeschnitten' }, + { id: 'proto-006-ps-5', label: 'Drei erwartbare Einwände mit belegter Antwort hinterlegt' }, + { + id: 'proto-006-ps-6', + label: 'Einseitiges Briefing und Audiofassung zugestellt', + detail: 'Audiofassung 3 Min. 05 Sek., identischer Aufbau wie das Dokument', + }, + ], + output: [ + { + label: 'Verkaufsargumente', + value: [ + 'Vier Gehminuten zum Bahnhof Zug — passt zur angekündigten Pendlerbelegschaft aus der Westschweiz', + 'Etage im Rohbau übernehmbar, Ausbau nach eigenem Bedarf ohne Rückbaupflicht', + 'Nachbarschaft mit drei Unternehmen derselben Branche im selben Quartier', + ], + }, + { + label: 'Erwartbare Einwände', + value: [ + 'Mietzins über dem Lausanner Vergleichsniveau — Antwort mit Steuervergleich Kanton Zug', + 'Bezugstermin 01.09.2026 zu früh — Staffelung der Übernahme möglich', + 'Nur sechs Parkplätze — Zusatzplätze in der Einstellhalle nebenan verfügbar', + ], + }, + { label: 'Markierte Lücken', value: 'Keine — alle Objektangaben sind aktuell.' }, + ], + sourceReferences: [ + { + id: 'proto-006-src-1', + label: 'Handelsregisterauszug Gegenüber', + documentName: 'Zefix — Perrin & Fils Sàrl', + locator: 'Abruf 20.05.2026', + quote: '«Sitz: Lausanne. Zweck: Handel mit Präzisionsbauteilen. Gründung 2009.»', + }, + { + id: 'proto-006-src-2', + label: 'Konditionenblatt OBJ-2547', + documentName: 'Konditionen Baarerstrasse 61.pdf', + locator: 'Seite 1', + quote: '«Nettomietzins 410 m² Büro, Bezug ab 01.09.2026, Ausbau nach Mieterwunsch»', + }, + { + id: 'proto-006-src-3', + label: 'Firmenwebsite Gegenüber', + documentName: 'Öffentliche Firmenseite — Standorte', + locator: 'Abruf 20.05.2026', + }, + ], + }, + { + id: 'proto-007', + agentId: AGENT_IDS.SINA, + timestamp: '2026-05-20T11:15:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: 'Frage zur Indexklausel Zürcherstrasse 204 mit Fundstelle beantwortet', + description: + 'Frage aus dem Teamkanal der Bewirtschaftung Winterthur in 12 Sekunden beantwortet — mit wörtlichem Zitat, Dokumentname, Stand und Seitenzahl. Ohne belegte Fundstelle wäre keine Antwort ausgegeben worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2574', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { + label: 'Frage', + value: + '«Ab wann darf beim Mietvertrag der Töss Präzision AG die Indexanpassung geltend gemacht werden?»', + }, + { label: 'Fragestellerin', value: 'Bewirtschaftung Winterthur, Teamkanal' }, + { label: 'Objektbezug', value: 'OBJ-2574, Zürcherstrasse 204, Winterthur' }, + ], + processingSteps: [ + { + id: 'proto-007-ps-1', + label: 'Richtige Akte und gültigen Vertragsstand bestimmt', + detail: 'Vertrag vom 01.03.2022 samt Nachtrag 2 vom 14.11.2024', + }, + { + id: 'proto-007-ps-2', + label: 'Massgebende Stellen in den erlaubten Ablagen gesucht', + detail: 'Dokumentensystem, Objektbibliothek und Laufwerksablage', + }, + { id: 'proto-007-ps-3', label: 'Wörtliches Zitat und Seitenzahl übernommen' }, + { id: 'proto-007-ps-4', label: 'Auskunft mit Frage, Quelle und Zeitpunkt protokolliert' }, + ], + output: [ + { + label: 'Antwort', + value: + 'Die Indexanpassung ist erstmals per 01.03.2027 möglich, frühestens fünf Jahre nach Mietbeginn und jeweils auf einen Zinstermin mit einer Ankündigungsfrist von vier Monaten.', + }, + { label: 'Fundstelle', value: 'Mietvertrag Töss Präzision AG, Ziffer 5.2, Seite 7' }, + { label: 'Stand des Dokuments', value: 'Fassung inkl. Nachtrag 2 vom 14.11.2024' }, + ], + sourceReferences: [ + { + id: 'proto-007-src-1', + label: 'Indexklausel', + documentName: 'Mietvertrag Töss Präzision AG, Zürcherstrasse 204.pdf', + locator: 'Seite 7, Ziffer 5.2', + quote: + '«Der Mietzins wird erstmals fünf Jahre nach Mietbeginn und danach jährlich dem Landesindex der Konsumentenpreise angepasst, jeweils auf einen Zinstermin und unter Einhaltung einer Ankündigungsfrist von vier Monaten.»', + }, + { + id: 'proto-007-src-2', + label: 'Nachtrag 2', + documentName: 'Nachtrag 2 Töss Präzision AG.pdf', + locator: 'Seite 1, Ziffer 2', + quote: '«Ziffer 5.2 des Mietvertrages bleibt unverändert bestehen.»', + }, + ], + }, + { + id: 'proto-008', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-05-20T09:55:00.000Z', + eventType: AgentProtocolEventType.ESCALATION, + title: 'Eskalation zur Optionsfrist Bahnhofstrasse 44 zur Freigabe vorgelegt', + description: + 'Erinnerung vom 05.03.2026 und Zweitmeldung vom 12.03.2026 blieben ohne Reaktion. Die Eskalation an die Leitung Bewirtschaftung Zürich liegt zur Freigabe bereit und geht erst danach hinaus.', + status: AgentProtocolStatus.WARNING, + objectId: 'OBJ-2451', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Objekt', value: 'OBJ-2451, Bahnhofstrasse 44, Zürich — Logistikfläche EG und UG' }, + { label: 'Mieterschaft', value: 'Sihlfeld Logistik GmbH, Zürich' }, + { label: 'Fristart', value: 'Optionsfrist, Stichtag 05.06.2026' }, + { + label: 'Meldeverlauf', + value: [ + '05.03.2026 — Erinnerung Meldeschwelle «3 Monate», keine Reaktion', + '12.03.2026 — Zweitmeldung zusätzlich im Team-Kanal Zürich, keine Reaktion', + '20.05.2026 — Eskalation vorbereitet', + ], + }, + ], + processingSteps: [ + { id: 'proto-008-ps-1', label: 'Terminbestand gegen den Kalender abgeglichen' }, + { + id: 'proto-008-ps-2', + label: 'Ausbleibende Reaktion auf die Zweitmeldung festgestellt', + detail: 'Weder Rückmeldung im Fristen-Postfach noch im Team-Kanal Zürich', + }, + { id: 'proto-008-ps-3', label: 'Fundstelle der Optionsklausel im Vertrag bestätigt' }, + { id: 'proto-008-ps-4', label: 'Folgen einer versäumten Frist zusammengestellt' }, + { id: 'proto-008-ps-5', label: 'Eskalation den Eskalationsempfängern zur Freigabe vorgelegt' }, + ], + output: [ + { + label: 'Eskalationstext', + value: + 'Die Optionsfrist für die Logistikfläche an der Bahnhofstrasse 44 läuft am 05.06.2026 ab. Zwei Meldungen blieben unbeantwortet. Ohne Ausübung verlängert sich der Vertrag um weitere fünf Jahre zu den bestehenden Konditionen.', + }, + { + label: 'Empfänger', + value: [ + 'Andrea Hersche, Leitung Bewirtschaftung Zürich', + 'Thomas Bühlmann, Winterthur (Stellvertretung)', + 'Mandatsleitung Basel (in Kopie)', + ], + }, + { label: 'Versandstatus', value: 'Zurückgehalten — wartet auf Freigabe' }, + ], + sourceReferences: [ + { + id: 'proto-008-src-1', + label: 'Optionsklausel', + documentName: 'Mietvertrag Sihlfeld Logistik GmbH, Bahnhofstrasse 44.pdf', + locator: 'Seite 5, Ziffer 3.4', + quote: + '«Die Mieterschaft verfügt über eine Option auf Verlängerung um fünf Jahre. Die Ausübung ist bis spätestens zwölf Monate vor Ablauf der Vertragsdauer schriftlich anzuzeigen.»', + }, + { + id: 'proto-008-src-2', + label: 'Meldeprotokoll zur Frist', + documentName: 'Fristenliste Bewirtschaftung Zürich', + locator: 'Eintrag OBJ-2451, Optionsfrist 05.06.2026', + }, + ], + }, + { + id: 'proto-009', + agentId: AGENT_IDS.LIVIA, + timestamp: '2026-05-20T09:10:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Lagebeschrieb Neuhegistrasse 12 in Kurz- und Langfassung erstellt', + description: + 'Mikrolage im Radius von 800 Metern, Makrolage Winterthur Neuhegi, Erreichbarkeit und geplante Umfeldentwicklung ausgewertet. Kurztext 574 Zeichen, Langtext 2 280 Zeichen, jede Aussage mit Quelle und Abrufdatum.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2503', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Objektadresse', value: 'Neuhegistrasse 12, 8404 Winterthur' }, + { label: 'Quartier', value: 'Winterthur Neuhegi' }, + { label: 'Analyseradius', value: '800 Meter' }, + { label: 'Tonalität', value: 'sachlich' }, + ], + processingSteps: [ + { + id: 'proto-009-ps-1', + label: 'Adresse gegen die amtlichen Adressdaten geprüft und dem Quartier zugeordnet', + }, + { + id: 'proto-009-ps-2', + label: 'Mikrolage abgegangen', + detail: 'Nachbarschaft, Erdgeschossnutzungen, Immissionslage, Publikumsfrequenz, Parkierung', + }, + { + id: 'proto-009-ps-3', + label: 'Makrolage eingeordnet', + detail: 'Beschäftigtenentwicklung, Branchenmix und Bedeutung im Wirtschaftsraum Winterthur', + }, + { + id: 'proto-009-ps-4', + label: 'Erreichbarkeit und Nahversorgung mit Distanz und Wegzeit erfasst', + }, + { id: 'proto-009-ps-5', label: 'Amtliche Publikationen im Umfeld gesichtet' }, + { id: 'proto-009-ps-6', label: 'Kurztext und Langtext geschrieben, Quellen angehängt' }, + ], + output: [ + { + label: 'Kurztext für Inserat', + value: + 'Die Fläche liegt in Winterthur Neuhegi, einem in den vergangenen zehn Jahren umgebauten Arbeitsplatzquartier mit Wohn-, Büro- und Produktionsnutzung. Die Haltestelle Neuhegi ist in vier Gehminuten erreichbar, der Bahnhof Winterthur Grüze in neun Minuten mit dem Bus. Einkaufen, Verpflegung und Kinderbetreuung befinden sich innerhalb von 400 Metern.', + }, + { + label: 'Abschnitte des Langtexts', + value: [ + 'Mikrolage — Nachbarschaft und Erdgeschossnutzungen', + 'Makrolage — Wirtschaftsraum Winterthur', + 'Erreichbarkeit — öffentlicher Verkehr, Autobahn A1, Velowege', + 'Nahversorgung — Einkaufen, Verpflegung, Betreuung, Gesundheit', + 'Umfeldentwicklung — Arealentwicklung nördlich der Neuhegistrasse', + ], + }, + { label: 'Belegte Aussagen', value: '31 von 31 Aussagen mit Quelle und Abrufdatum' }, + ], + sourceReferences: [ + { + id: 'proto-009-src-1', + label: 'Amtliche Publikation zur Arealentwicklung', + documentName: 'Amtliche Meldung — Gestaltungsplan Neuhegi Nord', + locator: 'Publikation vom 24.04.2026', + quote: + '«Der Gestaltungsplan für das Areal nördlich der Neuhegistrasse wurde bewilligt; die Bauarbeiten beginnen im ersten Quartal 2027.»', + }, + { + id: 'proto-009-src-2', + label: 'Fahrplandaten Haltestelle Neuhegi', + documentName: 'Öffentliche Verkehrsdaten Winterthur', + locator: 'Abruf 20.05.2026', + }, + { + id: 'proto-009-src-3', + label: 'Objektstammdaten OBJ-2503', + documentName: 'Bewirtschaftungssystem — Objektblatt OBJ-2503', + locator: 'Adresse und Nutzungsart', + }, + ], + }, + { + id: 'proto-010', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-20T08:20:00.000Z', + eventType: AgentProtocolEventType.REJECTION, + title: 'Absageentwurf an die Säntis Retail AG zurückgewiesen', + description: + 'Der vorbereitete Absagetext wurde nicht freigegeben: Der Flächenbedarf liegt nur knapp unter dem ausgeschriebenen Rahmen, der Fall gilt damit als Grenzfall. Die Anfrage ist zurück in die Entscheidung gelegt worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2568', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.REJECTED, + }, + { + id: 'proto-011', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-05-20T06:12:00.000Z', + eventType: AgentProtocolEventType.REMINDER_SENT, + title: 'Fristerinnerungen der Meldeschwelle «6 Monate» versandt', + description: + 'Der bestätigte Terminbestand wurde gegen den Kalender abgeglichen. Neun Fristen haben die Sechs-Monats-Schwelle erreicht; die Erinnerungen sind an die zuständigen Personen in Zürich, Winterthur und Basel hinausgegangen.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Geprüfter Bestand', value: '1 316 bestätigte Termine über alle Mandate' }, + { label: 'Erreichte Meldeschwelle', value: '6 Monate vor Stichtag' }, + { + label: 'Einbezogene Fristarten', + value: [ + 'Kündigungsfristen', + 'Optionsfristen', + 'Break-Klauseln', + 'Indexanpassungen', + 'Staffelmieten', + 'Nebenkostenabrechnung', + ], + }, + ], + processingSteps: [ + { id: 'proto-011-ps-1', label: 'Terminbestand gegen Datum und Kalender abgeglichen' }, + { + id: 'proto-011-ps-2', + label: 'Fällige Meldeschwellen ermittelt', + detail: 'Neun Treffer auf der Schwelle «6 Monate», keine überschrittene Frist', + }, + { + id: 'proto-011-ps-3', + label: 'Verschobene und erledigte Termine ausgesondert', + detail: 'Zwei Termine als erledigt erkannt und aus der Meldung genommen', + }, + { id: 'proto-011-ps-4', label: 'Erinnerungen je zuständiger Person zusammengefasst' }, + { id: 'proto-011-ps-5', label: 'Meldungen protokolliert' }, + ], + output: [ + { label: 'Versandte Erinnerungen', value: '9 Meldungen an 6 zuständige Personen' }, + { + label: 'Betroffene Standorte', + value: ['Zürich — 4 Fristen', 'Winterthur — 3 Fristen', 'Basel — 2 Fristen'], + }, + { + label: 'Nächste kritische Frist', + value: 'Optionsfrist OBJ-2451, Bahnhofstrasse 44, Zürich — 05.06.2026', + }, + ], + sourceReferences: [ + { + id: 'proto-011-src-1', + label: 'Terminbestand', + documentName: 'Fristenliste Bewirtschaftung — Gesamtbestand', + locator: 'Stand 20.05.2026, 06:00', + }, + { + id: 'proto-011-src-2', + label: 'Terminexport Bewirtschaftungssystem', + documentName: 'Bewirtschaftungssystem — Vertragslaufzeiten', + locator: 'Export vom 20.05.2026', + }, + ], + }, + + // ── Laufende Woche, 18.–19.05.2026 ────────────────────────────────────────── + { + id: 'proto-012', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-19T17:05:00.000Z', + eventType: AgentProtocolEventType.APPROVAL, + title: 'Terminvorschläge für die Limmat Robotics AG freigegeben', + description: + 'Nach der Freigabe sind drei Zeitfenster im Besichtigungskalender vorläufig reserviert und dem Interessenten zugestellt worden. Termindauer 45 Minuten, Pufferzeit 15 Minuten vor und nach dem Termin.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2512', + channel: AgentChannelType.CALENDAR, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.APPROVED, + }, + { + id: 'proto-013', + agentId: AGENT_IDS.NORA, + timestamp: '2026-05-19T15:40:00.000Z', + eventType: AgentProtocolEventType.REJECTION, + title: 'Marktsignal zur Rheinbogen Gastro GmbH verworfen', + description: + 'Das Signal «Filialschliessung Basel» stützte sich auf einen einzelnen Medienbericht ohne zweite Quelle. Es wurde in der Review-Queue verworfen und erscheint weder in der Wochen-Mail noch in der Lead-Liste.', + status: AgentProtocolStatus.INFO, + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.REJECTED, + }, + { + id: 'proto-014', + agentId: AGENT_IDS.RETO, + timestamp: '2026-05-19T14:12:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Kein Systemzugang zum Kontaktjournal — Mail-Fallback gegriffen', + description: + 'Die freigegebene Gesprächsnotiz liess sich nicht ins eingestellte Zielsystem übertragen: Für das Kontaktjournal lag kein gültiger Systemzugang vor. Das Paket ging als strukturierte E-Mail an die zuständige Person, der Arbeitsablauf wurde nicht unterbrochen.', + status: AgentProtocolStatus.WARNING, + objectId: 'OBJ-2596', + channel: AgentChannelType.STRUCTURED_MAIL_FALLBACK, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Freigegebenes Paket', value: 'Protokoll, Gesprächsnotiz und zwei Folgeaufgaben' }, + { label: 'Zielsystem', value: 'Kontaktjournal der Interessentenbetreuung' }, + { label: 'Objektbezug', value: 'OBJ-2596, Pilatusstrasse 18, Luzern' }, + { label: 'Freigabe erteilt am', value: '19.05.2026, 14:05' }, + ], + processingSteps: [ + { id: 'proto-014-ps-1', label: 'Freigegebenes Paket zur Übertragung angenommen' }, + { + id: 'proto-014-ps-2', + label: 'Übertragung ins Zielsystem versucht', + detail: 'Zugang zum Kontaktjournal nicht verfügbar, zweimal wiederholt', + }, + { + id: 'proto-014-ps-3', + label: 'Eingestellter Fallback ausgelöst', + detail: 'Strukturierter Mail-Fallback an die zuständige Person', + }, + { id: 'proto-014-ps-4', label: 'Protokoll im Objektdossier abgelegt' }, + { id: 'proto-014-ps-5', label: 'Systemstörung an die Bewirtschaftung gemeldet' }, + ], + output: [ + { label: 'Ergebnis', value: 'Gesprächsnotiz als strukturierte E-Mail zugestellt' }, + { label: 'Empfängerin', value: 'Nadine Brunner, Vermarktung Zürich' }, + { + label: 'Offener Punkt', + value: + 'Die Notiz muss von Hand ins Kontaktjournal übernommen werden, sobald der Systemzugang wieder besteht.', + }, + ], + sourceReferences: [ + { + id: 'proto-014-src-1', + label: 'Fallback-Einstellung', + documentName: 'Einstellungen Reto — Fallback-Ziel', + locator: 'Wert «Strukturierter Mail-Fallback an die zuständige Person»', + }, + { + id: 'proto-014-src-2', + label: 'Objektdossier OBJ-2596', + documentName: 'Dossier Pilatusstrasse 18, Luzern', + locator: 'Ordner «Besichtigungsprotokolle»', + }, + ], + }, + { + id: 'proto-015', + agentId: AGENT_IDS.SINA, + timestamp: '2026-05-19T11:30:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: '«nicht dokumentiert» zur Frage nach dem Untermietrecht ausgegeben', + description: + 'Zur Frage nach einer Untermietklausel im Vertrag der Töss Präzision AG liess sich keine belegte Fundstelle finden. Sina hat «nicht dokumentiert» ausgegeben, die fehlende Unterlage benannt und die Lücke gemeldet.', + status: AgentProtocolStatus.INFO, + objectId: 'OBJ-2574', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { + label: 'Frage', + value: '«Darf die Töss Präzision AG einen Teil der Fläche untervermieten?»', + }, + { label: 'Fragesteller', value: 'Bewirtschaftung Winterthur, Teamkanal' }, + { + label: 'Durchsuchte Ablagen', + value: ['Dokumentensystem', 'Objektbibliothek Winterthur', 'Laufwerksablage'], + }, + ], + processingSteps: [ + { id: 'proto-015-ps-1', label: 'Vertrag, Nachträge und Zusatzvereinbarungen durchsucht' }, + { + id: 'proto-015-ps-2', + label: 'Keine massgebende Stelle zur Untermiete gefunden', + detail: 'Weder im Hauptvertrag noch in den beiden Nachträgen', + }, + { id: 'proto-015-ps-3', label: '«nicht dokumentiert» ausgegeben statt eine Annahme zu treffen' }, + { id: 'proto-015-ps-4', label: 'Fehlende Unterlage als Dokumentationslücke gemeldet' }, + ], + output: [ + { + label: 'Antwort', + value: + 'nicht dokumentiert — in den hinterlegten Unterlagen zu OBJ-2574 findet sich keine Regelung zur Untermiete.', + }, + { + label: 'Fehlende Unterlage', + value: 'Zusatzvereinbarung zur Untermiete, im Übergabeprotokoll vom 12.04.2022 erwähnt', + }, + { label: 'Gemeldet an', value: 'Bewirtschaftung Winterthur' }, + ], + sourceReferences: [ + { + id: 'proto-015-src-1', + label: 'Geprüfter Hauptvertrag', + documentName: 'Mietvertrag Töss Präzision AG, Zürcherstrasse 204.pdf', + locator: 'Seiten 1–14, keine Regelung zur Untermiete', + }, + { + id: 'proto-015-src-2', + label: 'Hinweis auf die fehlende Vereinbarung', + documentName: 'Übergabeprotokoll Zürcherstrasse 204, 12.04.2022.pdf', + locator: 'Seite 2, Ziffer 4', + quote: '«Die Untermiete an Dritte wird in einer separaten Vereinbarung geregelt.»', + }, + ], + }, + { + id: 'proto-016', + agentId: AGENT_IDS.NORA, + timestamp: '2026-05-19T08:00:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Wochen-Mail als Vorschlag vorgelegt', + description: + '17 verifizierte Signale aus der Woche vom 12. bis 18.05.2026 nach Region und Segment gegliedert, je mit Konfidenz und Quellenangabe. Der Entwurf wartet auf die Freigabe der Marktbeobachtung und geht erst danach hinaus.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-017', + agentId: AGENT_IDS.BRUNO, + timestamp: '2026-05-18T16:50:00.000Z', + eventType: AgentProtocolEventType.CHANNEL_TESTED, + title: 'Kanal «Audiofassung Briefing» getestet', + description: + 'Testlauf der gesprochenen Briefing-Fassung durchgeführt. Die Zustellung an die hinterlegte Empfängerin war erfolgreich, Länge 3 Minuten 05 Sekunden, Aufbau identisch zum Dokument.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.AUDIO_OUTPUT, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + }, + { + id: 'proto-018', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-05-18T14:25:00.000Z', + eventType: AgentProtocolEventType.EDIT, + title: 'Eskalationstext zur Break-Klausel Aeschenvorstadt 27 angepasst', + description: + 'Der Meldeverlauf im Eskalationstext wurde gekürzt und der Hinweis auf die Folgen einer versäumten Frist geschärft. Die angepasste Fassung ist anschliessend freigegeben und versandt worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2536', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.EDITED, + }, + { + id: 'proto-019', + agentId: AGENT_IDS.LIVIA, + timestamp: '2026-05-18T10:05:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: 'Auftrag für den Lagebeschrieb Wankdorffeldstrasse 8 entgegengenommen', + description: + 'Die Adresse wurde gegen die amtlichen Adressdaten geprüft und dem Entwicklungsgebiet Bern Wankdorf zugeordnet. Der Arbeitsauftrag ist eingereiht; die Umfeldanalyse läuft im Radius von 800 Metern.', + status: AgentProtocolStatus.INFO, + objectId: 'OBJ-2559', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-020', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-05-18T07:00:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Fristen-Pipeline der nächsten zwölf Monate zugestellt', + description: + '218 Fristen nach Standort, Meldeschwelle und Bearbeitungsstand gegliedert und in die Team-Kanäle Zürich, Winterthur und Basel gestellt. 31 davon haben ihren Stichtag innerhalb der nächsten 90 Tage.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-021', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-18T06:40:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: 'Telefonnotiz aus dem Empfang Winterthur nur teilweise auswertbar', + description: + 'Aus der Notiz liessen sich Name und Firma übernehmen, Flächenbedarf und Wunschtermin fehlen. Ein Rückfrageentwurf mit den fehlenden Mindestangaben liegt zur Freigabe bereit; die Anfrage bleibt bis dahin offen.', + status: AgentProtocolStatus.WARNING, + objectId: 'OBJ-2568', + channel: AgentChannelType.PHONE_NOTE, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + + // ── Laufender Monat, 01.–15.05.2026 ───────────────────────────────────────── + { + id: 'proto-022', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-15T15:45:00.000Z', + eventType: AgentProtocolEventType.SETTINGS_SAVED, + title: 'Must-Kriterien und Termindauer gespeichert', + description: + 'Das Kriterium «Vertragsdauer mindestens fünf Jahre» wurde ergänzt und die Termindauer von 60 auf 45 Minuten gesetzt. Die Änderung gilt ab dem nächsten Anfrageeingang.', + status: AgentProtocolStatus.SUCCESS, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + }, + { + id: 'proto-023', + agentId: AGENT_IDS.SINA, + timestamp: '2026-05-13T09:30:00.000Z', + eventType: AgentProtocolEventType.ESCALATION, + title: 'Dokumentationslücken Winterthur an die Bewirtschaftung eskaliert', + description: + 'Zu neun Liegenschaften in Winterthur fehlen unterzeichnete Nachträge, in drei Fällen liegt nur eine unsignierte Fassung vor. Solange die Unterlagen fehlen, bleiben Auskünfte zu Laufzeiten und Indexierung unbelegt.', + status: AgentProtocolStatus.WARNING, + objectId: 'OBJ-2574', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-024', + agentId: AGENT_IDS.BRUNO, + timestamp: '2026-05-12T18:05:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Kein Systemzugang zum Bewirtschaftungssystem — Briefing unvollständig', + description: + 'Der Lesezugang zum Bewirtschaftungssystem war während des Laufs nicht verfügbar. Konditionen, Nebenkosten und Verfügbarkeit konnten nicht gelesen werden; das Briefing ging mit vier ausgewiesenen Lücken und 40 Minuten verspätet hinaus.', + status: AgentProtocolStatus.ERROR, + objectId: 'OBJ-2547', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Termin', value: 'Aussentermin 12.05.2026, 19:30, Baarerstrasse 61, Zug' }, + { label: 'Objektbezug', value: 'OBJ-2547, Büroetage 4. OG' }, + { label: 'Vorlaufzeit', value: '2 Stunden' }, + ], + processingSteps: [ + { id: 'proto-024-ps-1', label: 'Termin eingelesen und Vorbereitung gestartet' }, + { + id: 'proto-024-ps-2', + label: 'Zugriff auf das Bewirtschaftungssystem gescheitert', + detail: 'Systemzugang nicht verfügbar, drei Versuche im Abstand von fünf Minuten', + }, + { + id: 'proto-024-ps-3', + label: 'Verfügbare Angaben aus Dossier und Teamablage zusammengetragen', + }, + { + id: 'proto-024-ps-4', + label: 'Nicht lesbare Angaben als Lücke gekennzeichnet', + detail: 'Keine Schätzwerte eingesetzt', + }, + { id: 'proto-024-ps-5', label: 'Briefing mit Hinweis auf die Störung zugestellt' }, + ], + output: [ + { + label: 'Markierte Lücken', + value: [ + 'Nettomietzins — Stand nicht lesbar', + 'Nebenkosten — Stand nicht lesbar', + 'Verfügbarkeit — Stand nicht lesbar', + 'Parkplatzschlüssel — Stand nicht lesbar', + ], + }, + { label: 'Zustellung', value: '12.05.2026, 18:05 — 40 Minuten nach dem Zielzeitpunkt' }, + { + label: 'Empfohlener nächster Schritt', + value: 'Konditionen vor dem Termin telefonisch bei der Bewirtschaftung Zug bestätigen lassen', + }, + ], + sourceReferences: [ + { + id: 'proto-024-src-1', + label: 'Protokoll des Verbindungsversuchs', + documentName: 'Systemprotokoll Bruno — Zugriffe 12.05.2026', + locator: 'Einträge 17:25, 17:30, 17:35', + }, + { + id: 'proto-024-src-2', + label: 'Zuletzt bekannter Stand aus dem Dossier', + documentName: 'Konditionen Baarerstrasse 61.pdf', + locator: 'Seite 1, Fassung vom 14.01.2026', + }, + ], + }, + { + id: 'proto-025', + agentId: AGENT_IDS.NORA, + timestamp: '2026-05-12T08:35:00.000Z', + eventType: AgentProtocolEventType.APPROVAL, + title: 'Wochen-Mail vom 12.05.2026 freigegeben und versandt', + description: + '23 verifizierte Signale aus sechs Regionen wurden freigegeben. Der Versand ging an Marktbeobachtung, Akquisition Zürich und Bewirtschaftung Winterthur; vier unsichere Signale blieben in der Review-Queue.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.APPROVED, + }, + { + id: 'proto-026', + agentId: AGENT_IDS.SINA, + timestamp: '2026-05-11T07:00:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Wöchentliche Lückenmeldung erstellt', + description: + '27 offene Dokumentationslücken je Liegenschaft zusammengestellt, davon neun in Winterthur und sechs in Basel. Gegenüber der Vorwoche sind vier Lücken geschlossen worden.', + status: AgentProtocolStatus.INFO, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-027', + agentId: AGENT_IDS.RETO, + timestamp: '2026-05-08T16:30:00.000Z', + eventType: AgentProtocolEventType.REMINDER_SENT, + title: 'Erinnerung an fehlendes Diktat nach der Besichtigung Industriestrasse 14', + description: + 'Drei Stunden nach Terminende lag kein Memo vor. Die Erinnerung mit Objekt und Interessent ging auf demselben Weg hinaus, auf dem sonst diktiert wird; das Memo traf 26 Minuten später ein.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2588', + channel: AgentChannelType.WHATSAPP, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-028', + agentId: AGENT_IDS.LIVIA, + timestamp: '2026-05-07T10:15:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Umfeldänderung Winterthur Neuhegi gemeldet', + description: + 'Die bewilligte Arealentwicklung nördlich der Neuhegistrasse verändert den Abschnitt «Umfeldentwicklung» in vier Lagebeschrieben. Die betroffenen Textabschnitte sind markiert, die Quelle mit Publikationsdatum hinterlegt.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2503', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-029', + agentId: AGENT_IDS.LEA, + timestamp: '2026-05-06T13:40:00.000Z', + eventType: AgentProtocolEventType.REMINDER_SENT, + title: 'Terminreminder 24 Stunden vor der Besichtigung versandt', + description: + 'Interessent und Ansprechperson wurden an die Besichtigung vom 07.05.2026, 09:30 Uhr erinnert — mit Treffpunkt, Dauer und Ansprechperson vor Ort. Beide haben den Termin bestätigt.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2512', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-030', + agentId: AGENT_IDS.NORA, + timestamp: '2026-05-04T07:15:00.000Z', + eventType: AgentProtocolEventType.CHANNEL_TESTED, + title: 'Teamkanal-Verbindung getestet — Zustellung verzögert', + description: + 'Die Testmeldung erreichte den Teamkanal der Marktbeobachtung erst nach 4 Minuten 20 Sekunden. Die Verbindung steht, Kurzmeldungen zu Signalen mit hoher Konfidenz gehen jedoch spürbar verzögert ein.', + status: AgentProtocolStatus.WARNING, + channel: AgentChannelType.TEAMS, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + }, + { + id: 'proto-031', + agentId: AGENT_IDS.SINA, + timestamp: '2026-05-01T06:00:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Monatlicher Lückenreport an die Standorte zugestellt', + description: + 'Offene Dokumentationslücken je Liegenschaft und Portfolio zusammengefasst und an Zürich, Winterthur und Basel zugestellt. Im April sind 18 220 Dokumente durchsuchbar gehalten worden.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + + // ── Früher im Jahr 2026 ───────────────────────────────────────────────────── + { + id: 'proto-032', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-04-14T11:25:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Abgleich abgebrochen — Systemzugang zum Bewirtschaftungssystem abgelaufen', + description: + 'Der tägliche Abgleich des Terminbestands gegen die Terminexporte brach ab: Der Lesezugang zum Bewirtschaftungssystem war abgelaufen. 1 284 Fristen konnten nicht gegengeprüft werden, die Meldeschwellen des Tages wurden nicht ausgelöst.', + status: AgentProtocolStatus.ERROR, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Geplanter Lauf', value: 'Täglicher Abgleich des Terminbestands, 06:00' }, + { label: 'Betroffener Bestand', value: '1 284 bestätigte Termine' }, + { label: 'Benötigter Zugang', value: 'Lesezugriff auf Vertragslaufzeiten und Terminexporte' }, + ], + processingSteps: [ + { id: 'proto-032-ps-1', label: 'Lauf planmässig gestartet' }, + { + id: 'proto-032-ps-2', + label: 'Zugriff auf die Terminexporte verweigert', + detail: 'Der hinterlegte Systemzugang ist am 13.04.2026 abgelaufen', + }, + { + id: 'proto-032-ps-3', + label: 'Lauf abgebrochen statt mit unvollständigem Bestand fortzufahren', + }, + { id: 'proto-032-ps-4', label: 'Störung an die Bewirtschaftung Zürich gemeldet' }, + { id: 'proto-032-ps-5', label: 'Abbruch mit Zeitpunkt und Grund protokolliert' }, + ], + output: [ + { label: 'Ergebnis', value: 'Abgleich ohne Resultat abgebrochen' }, + { label: 'Nicht ausgelöste Meldungen', value: '6 Erinnerungen der Schwellen «12» und «9 Monate»' }, + { + label: 'Behebung', + value: + 'Zugang am 15.04.2026 erneuert; der Abgleich wurde nachgeholt und alle offenen Meldungen sind am selben Tag hinausgegangen.', + }, + ], + sourceReferences: [ + { + id: 'proto-032-src-1', + label: 'Protokoll des Abgleichlaufs', + documentName: 'Systemprotokoll Ferdi — Lauf 14.04.2026', + locator: 'Eintrag 06:00 bis 06:04', + quote: '«Zugriff auf Terminexport verweigert — hinterlegter Zugang abgelaufen.»', + }, + { + id: 'proto-032-src-2', + label: 'Meldung an die Bewirtschaftung', + documentName: 'Fristen-Postfach Bewirtschaftung', + locator: 'Nachricht vom 14.04.2026, 06:12', + }, + ], + }, + { + id: 'proto-033', + agentId: AGENT_IDS.RETO, + timestamp: '2026-04-08T09:45:00.000Z', + eventType: AgentProtocolEventType.EDIT, + title: 'Besichtigungsprotokoll vor der Freigabe angepasst', + description: + 'Zwei als unsicher markierte Stellen wurden korrigiert und eine Folgeaufgabe gestrichen. Das angepasste Paket ist anschliessend freigegeben und ins Dossier übertragen worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2478', + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.EDITED, + }, + { + id: 'proto-034', + agentId: AGENT_IDS.LIVIA, + timestamp: '2026-04-01T05:30:00.000Z', + eventType: AgentProtocolEventType.TASK_RUN, + title: 'Quartalslauf: 184 Lagebeschriebe gegen die Quellen geprüft', + description: + '162 Lagebausteine wurden bestätigt, 14 Abschnitte als veraltet markiert und erneuert, 9 Objekte für den nächsten Lauf vorgemerkt. Jede Prüfung ist je Objekt nachvollziehbar protokolliert.', + status: AgentProtocolStatus.SUCCESS, + channel: AgentChannelType.WORKSPACE_CHAT, + triggeredBy: AgentTriggerSource.SYSTEM, + }, + { + id: 'proto-035', + agentId: AGENT_IDS.FERDI, + timestamp: '2026-03-17T14:10:00.000Z', + eventType: AgentProtocolEventType.APPROVAL, + title: 'Eskalation zur Kündigungsfrist Wankdorffeldstrasse 8 freigegeben', + description: + 'Nach zwei erfolglosen Meldungen wurde der Versand an die Eskalationsempfänger freigegeben. Die Rückmeldung der Mandatsleitung traf am Folgetag ein, die Frist ist fristgerecht behandelt worden.', + status: AgentProtocolStatus.SUCCESS, + objectId: 'OBJ-2559', + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + approvalDecision: AgentApprovalDecision.APPROVED, + }, + { + id: 'proto-036', + agentId: AGENT_IDS.LIVIA, + timestamp: '2026-03-05T08:50:00.000Z', + eventType: AgentProtocolEventType.SETTINGS_SAVED, + title: 'Analyseradius und Quellentypen gespeichert', + description: + 'Der Radius der Umfeldanalyse wurde von 500 auf 800 Meter erhöht und «Medienberichte» als Quellentyp abgewählt. Bestehende Lagebeschriebe werden im nächsten Quartalslauf nachgeführt.', + status: AgentProtocolStatus.SUCCESS, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + }, + { + id: 'proto-037', + agentId: AGENT_IDS.LEA, + timestamp: '2026-02-19T16:35:00.000Z', + eventType: AgentProtocolEventType.MESSAGE_PROCESSED, + title: 'Anfragen-Postfach nicht erreichbar — 14 Anfragen unbearbeitet', + description: + 'Das Anfragen-Postfach der Vermarktung liess sich über zwei Stunden nicht öffnen. 14 eingegangene Anfragen wurden weder erfasst noch beantwortet; die durchschnittliche Erstreaktionszeit stieg an diesem Tag auf 3 Stunden 40 Minuten.', + status: AgentProtocolStatus.ERROR, + channel: AgentChannelType.EMAIL, + triggeredBy: AgentTriggerSource.SYSTEM, + input: [ + { label: 'Betroffenes Postfach', value: 'Anfragen-Postfach Vermarktung' }, + { label: 'Ausfallzeitraum', value: '19.02.2026, 14:20 bis 16:30' }, + { + label: 'Betroffene Kanäle', + value: ['Direktmails von Interessenten', 'Website-Formular «Fläche anfragen»'], + }, + ], + processingSteps: [ + { id: 'proto-037-ps-1', label: 'Abruf des Postfachs planmässig gestartet' }, + { + id: 'proto-037-ps-2', + label: 'Postfach nicht erreichbar', + detail: 'Acht Versuche im Abstand von fünfzehn Minuten, alle ohne Verbindung', + }, + { + id: 'proto-037-ps-3', + label: 'Automatische Absagen für den Zeitraum ausgesetzt', + detail: 'Ohne vollständiges Bedarfsprofil wird nie abgesagt', + }, + { id: 'proto-037-ps-4', label: 'Störung an die Vermarktung gemeldet' }, + { id: 'proto-037-ps-5', label: 'Nachlauf nach Wiederherstellung eingeplant' }, + ], + output: [ + { label: 'Nicht erfasste Anfragen', value: '14, davon 9 über das Website-Formular' }, + { label: 'Erstreaktionszeit an diesem Tag', value: '3 Std. 40 Min. statt 11 Minuten' }, + { + label: 'Behebung', + value: + 'Nach Wiederherstellung um 16:30 wurden alle 14 Anfragen nacherfasst, dedupliziert und bis 18:10 qualifiziert.', + }, + ], + sourceReferences: [ + { + id: 'proto-037-src-1', + label: 'Protokoll der Abrufversuche', + documentName: 'Systemprotokoll Lea — Postfachabrufe 19.02.2026', + locator: 'Einträge 14:20 bis 16:20', + }, + { + id: 'proto-037-src-2', + label: 'Störungsmeldung an die Vermarktung', + documentName: 'Team-Kanal Vermarktung Zürich', + locator: 'Meldung vom 19.02.2026, 14:52', + }, + ], + }, + { + id: 'proto-038', + agentId: AGENT_IDS.BRUNO, + timestamp: '2026-01-27T10:00:00.000Z', + eventType: AgentProtocolEventType.SETTINGS_SAVED, + title: 'Vorlaufzeit und Audiofassung gespeichert', + description: + 'Die Vorlaufzeit vor einem Aussentermin wurde von 3 auf 2 Stunden gesetzt und die Audiofassung dauerhaft aktiviert. Die Einstellung gilt ab dem nächsten Aussentermin der Vermarktung.', + status: AgentProtocolStatus.SUCCESS, + triggeredBy: AgentTriggerSource.USER, + triggeredByName: OPERATOR, + }, +] diff --git a/src/mock-data/agentWorkItems.ts b/src/mock-data/agentWorkItems.ts new file mode 100644 index 0000000..592be5a --- /dev/null +++ b/src/mock-data/agentWorkItems.ts @@ -0,0 +1,2202 @@ +/** + * Property On — Vorgänge im «Bearbeitungsverlauf». + * + * Vierzehn Vorgänge, je zwei pro digitalem Mitarbeiter: einer bereits erledigt, + * einer pendent. Erledigte Vorgänge tragen `requiresDecision: false` und ein + * `completedAt`; pendente Vorgänge tragen `requiresDecision: true`, den Anlass der + * Rückfrage (§5.5) und die konkrete Frage an den Menschen. + * + * Zeitanker ist der 20.05.2026 (`MOCK_TODAY`). Die Zeitstempel sind bewusst über + * heute, diese Woche, diesen Monat und dieses Jahr verteilt, damit jeder Filter + * der Auswertung Daten findet: + * + * heute (20.05.) ferdi-01, ferdi-02, sina-01, lea-02, bruno-02 + * diese Woche (18.–19.) lea-01, reto-02, sina-02 + * dieser Monat (01.–17.) reto-01, livia-02, nora-02 + * dieses Jahr (Jan–Apr) bruno-01, nora-01, livia-01 + * + * Sämtliche Objekte, Firmen und Personen sind erfunden. Es werden keine echten + * Systeme angesprochen und keine echten Zugangsdaten geführt. + */ + +import type { AgentWorkItem } from '../domain/agentWorkItem' +import { + AgentWorkItemStatus, + AgentWorkItemPriority, + AgentWorkItemKind, + AgentDomainArea, + AgentWorkItemAction, +} from '../domain/agentWorkItem' +import { AgentChannelType } from '../domain/teamAgent' +import { AGENT_IDS } from './teamAgents' + +export const mockAgentWorkItems: AgentWorkItem[] = [ + // ── Ferdi — erledigt ─────────────────────────────────────────────────────── + { + id: 'awi-ferdi-01', + agentId: AGENT_IDS.FERDI, + kind: AgentWorkItemKind.REMINDER_CREATED, + area: AgentDomainArea.CONTRACT_DEADLINES, + title: 'Erinnerung 6 Monate vor Optionsfrist versandt — Sihlfeld Logistik GmbH', + summary: + 'Die Morgenprüfung des Terminbestands hat die Meldeschwelle «6 Monate» für die Optionsfrist an der Bahnhofstrasse 44 erreicht. Die Erinnerung ging an die zuständige Person der Bewirtschaftung Zürich, der Vorlauf im Fristenkalender wurde nachgeführt.', + objectId: 'OBJ-2451', + objectLabel: 'Bahnhofstrasse 44, 8001 Zürich — Büro- und Logistikfläche, 1 840 m²', + sourceChannel: AgentChannelType.CALENDAR, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-20T06:04:00.000Z', + completedAt: '2026-05-20T06:07:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + inputs: [ + { + label: 'Bestätigter Termin im Bestand', + value: 'Optionsfrist Sihlfeld Logistik GmbH, Stichtag 20.11.2026, bestätigt am 14.01.2026', + }, + { + label: 'Grundlage', + value: [ + 'Mietvertrag vom 01.09.2019, Ziffer 4.2 «Optionsrecht»', + 'Nachtrag 2 vom 12.03.2023, Ziffer 1 «Verlängerung Optionsfrist»', + ], + }, + { + label: 'Erreichte Meldeschwelle', + value: '6 Monate vor Stichtag — ausgelöst durch die Prüfung des Terminbestands um 06:00 Uhr', + }, + { + label: 'Zuständige Person', + value: 'Andrea Hersche, Leitung Bewirtschaftung Zürich', + }, + ], + result: [ + { label: 'Fristart', value: 'Optionsfrist (Ausübung durch die Mieterschaft)' }, + { label: 'Stichtag', value: '20.11.2026 — Ausübungserklärung spätestens 12 Monate vor Vertragsende' }, + { + label: 'Versandte Meldung', + value: 'E-Mail «Optionsfrist in 6 Monaten — OBJ-2451» an andrea.hersche@property-on.ch, 20.05.2026, 06:07 Uhr', + }, + { + label: 'Erwartete Reaktion', + value: 'Rückmeldung bis 27.05.2026; ohne Reaktion folgt die Zweitmeldung nach fünf Arbeitstagen', + }, + { + label: 'Kalender nachgeführt', + value: 'Vorlaufmarke «3 Monate» auf den 20.08.2026 gesetzt, Fristenkalender Bewirtschaftung', + }, + { label: 'Nächste Meldeschwelle', value: '3 Monate vor Stichtag, ausgelöst am 20.08.2026' }, + ], + processingSteps: [ + { + id: 'awi-ferdi-01-step-1', + label: 'Terminbestand gegen das heutige Datum geprüft', + detail: '1 316 bestätigte Termine durchgesehen, 4 Meldeschwellen waren heute fällig.', + }, + { + id: 'awi-ferdi-01-step-2', + label: 'Fundstelle im Vertrag nachgeschlagen', + detail: 'Ziffer 4.2 des Mietvertrags und Ziffer 1 des Nachtrags 2 als massgebend bestätigt.', + }, + { + id: 'awi-ferdi-01-step-3', + label: 'Zuständige Person aus dem Bewirtschaftungssystem ermittelt', + detail: 'Objektzuständigkeit OBJ-2451: Bewirtschaftung Zürich, Andrea Hersche.', + }, + { + id: 'awi-ferdi-01-step-4', + label: 'Erinnerung versandt und Kalendermarke gesetzt', + detail: 'Versand über das Fristen-Postfach, anschliessend Vorlaufmarke für die nächste Schwelle eingetragen.', + }, + ], + sourceReferences: [ + { + id: 'awi-ferdi-01-src-1', + label: 'Mietvertrag Sihlfeld Logistik GmbH', + documentName: 'Mietvertrag_OBJ-2451_Sihlfeld-Logistik_2019-09-01.pdf', + locator: 'Seite 7, Ziffer 4.2', + quote: + '«Die Mieterin verfügt über ein einmaliges Optionsrecht zur Verlängerung um fünf Jahre. Die Ausübung ist der Vermieterschaft spätestens zwölf Monate vor Ablauf der festen Vertragsdauer schriftlich anzuzeigen.»', + }, + { + id: 'awi-ferdi-01-src-2', + label: 'Nachtrag 2 zum Mietvertrag', + documentName: 'Nachtrag-2_OBJ-2451_2023-03-12.pdf', + locator: 'Seite 2, Ziffer 1', + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-ferdi-01-hist-1', + at: '2026-01-14T08:20:00.000Z', + actor: 'Andrea Hersche', + action: 'Termin bestätigt', + note: 'Einmalige Bestätigung des aus dem Vertrag gelesenen Stichtags — Aufnahme in den überwachten Terminbestand.', + }, + { + id: 'awi-ferdi-01-hist-2', + at: '2026-05-20T06:07:00.000Z', + actor: 'Ferdi', + action: 'Erinnerung versandt', + note: 'Meldeschwelle «6 Monate» erreicht, Versand über das Fristen-Postfach der Bewirtschaftung.', + }, + ], + }, + + // ── Ferdi — pendent: Freigabe für das weitere Vorgehen ───────────────────── + { + id: 'awi-ferdi-02', + agentId: AGENT_IDS.FERDI, + kind: AgentWorkItemKind.ESCALATION, + area: AgentDomainArea.CONTRACT_DEADLINES, + title: 'Eskalation zur Freigabe — Kündigungsfrist Alpstein Feinbau AG ohne Reaktion', + summary: + 'Erinnerung und Zweitmeldung zur Kündigungsfrist an der Technoparkstrasse 7 blieben unbeantwortet. Der Eskalationsentwurf an die Leitung Bewirtschaftung liegt vollständig vor und wartet auf die Freigabe.', + objectId: 'OBJ-2288', + objectLabel: 'Technoparkstrasse 7, 8406 Winterthur — Werk- und Bürofläche, 2 260 m²', + sourceChannel: AgentChannelType.EMAIL, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-20T06:11:00.000Z', + priority: AgentWorkItemPriority.CRITICAL, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Freigabe für das weitere Vorgehen: Eskalationen gehen ausschliesslich nach ausdrücklicher Freigabe hinaus. Erinnerung vom 30.04.2026 und Zweitmeldung vom 08.05.2026 sind ohne Rückmeldung geblieben.', + decisionQuestion: + 'Soll die vorbereitete Eskalation an Andrea Hersche und Thomas Bühlmann versandt werden, oder wird die Kündigungsfrist innerhalb der Bewirtschaftung Winterthur anders behandelt?', + inputs: [ + { + label: 'Überwachter Termin', + value: 'Kündigungsfrist Alpstein Feinbau AG, Stichtag 30.06.2026, bestätigt am 09.10.2025', + }, + { + label: 'Meldeverlauf', + value: [ + 'Erinnerung «3 Monate» am 30.04.2026 an fabian.ochsner@property-on.ch', + 'Zweitmeldung am 08.05.2026 zusätzlich im Team-Kanal «#fristen-winterthur»', + 'Keine Rückmeldung bis 20.05.2026, 06:00 Uhr', + ], + }, + { + label: 'Hinterlegte Eskalationsempfänger', + value: 'Andrea Hersche (Leitung Bewirtschaftung Zürich), Thomas Bühlmann (Stellvertretung Winterthur)', + }, + { + label: 'Verbleibende Reaktionszeit', + value: '41 Tage bis zum Stichtag — die schriftliche Kündigung muss bis 30.06.2026 zugestellt sein', + }, + ], + result: [ + { label: 'Stand', value: 'Eskalationsentwurf vollständig, Versand angehalten bis zur Freigabe' }, + { + label: 'Betreff des Entwurfs', + value: 'Eskalation Kündigungsfrist OBJ-2288 — Stichtag 30.06.2026, zwei Meldungen ohne Reaktion', + }, + { + label: 'Folge einer versäumten Frist', + value: + 'Ohne fristgerechte Kündigung verlängert sich der Vertrag gemäss Ziffer 3.4 automatisch um weitere drei Jahre zu unveränderten Konditionen.', + }, + { + label: 'Im Entwurf enthalten', + value: [ + 'Objekt, Mieterschaft, Fristart und Stichtag', + 'Vollständiger Meldeverlauf mit Datum und Kanal', + 'Fundstelle der Kündigungsklausel im Vertrag', + 'Vorschlag für das weitere Vorgehen', + ], + }, + { label: 'Betroffene Jahresmiete', value: 'CHF 412 000 netto, Nebenkosten akonto CHF 38 400' }, + ], + processingSteps: [ + { + id: 'awi-ferdi-02-step-1', + label: 'Ausbleibende Reaktion erkannt', + detail: 'Fünf Arbeitstage nach der Zweitmeldung ist im Fristen-Postfach keine Antwort eingegangen.', + }, + { + id: 'awi-ferdi-02-step-2', + label: 'Meldeverlauf zusammengestellt', + detail: 'Alle Meldungen, Kanäle und Zeitpunkte aus dem Protokoll übernommen.', + }, + { + id: 'awi-ferdi-02-step-3', + label: 'Vertragsfolgen nachgeschlagen', + detail: 'Ziffer 3.4 des Mietvertrags als massgebend für die automatische Verlängerung bestätigt.', + }, + { + id: 'awi-ferdi-02-step-4', + label: 'Eskalationsentwurf erstellt und angehalten', + detail: 'Der Entwurf liegt versandbereit im Fristen-Postfach und wird ohne Freigabe nicht ausgelöst.', + }, + ], + sourceReferences: [ + { + id: 'awi-ferdi-02-src-1', + label: 'Mietvertrag Alpstein Feinbau AG', + documentName: 'Mietvertrag_OBJ-2288_Alpstein-Feinbau_2021-07-01.pdf', + locator: 'Seite 5, Ziffer 3.4', + quote: + '«Wird der Vertrag nicht spätestens sechs Monate vor Ablauf der festen Vertragsdauer schriftlich gekündigt, verlängert er sich jeweils stillschweigend um drei Jahre.»', + }, + { + id: 'awi-ferdi-02-src-2', + label: 'Meldeprotokoll Frist OBJ-2288', + documentName: 'Fristenprotokoll_OBJ-2288_2026.xlsx', + locator: 'Zeilen 14–16', + }, + ], + messageThread: [ + { + id: 'awi-ferdi-02-msg-1', + at: '2026-04-30T06:05:00.000Z', + from: 'ferdi@property-on.ch', + to: 'fabian.ochsner@property-on.ch', + channel: AgentChannelType.EMAIL, + subject: 'Erinnerung: Kündigungsfrist OBJ-2288 läuft am 30.06.2026 ab', + body: `Guten Morgen Herr Ochsner + +Für die Werk- und Bürofläche an der Technoparkstrasse 7 in Winterthur (OBJ-2288, Alpstein Feinbau AG) läuft die Kündigungsfrist am 30. Juni 2026 ab. + +Ohne fristgerechte schriftliche Kündigung verlängert sich der Vertrag gemäss Ziffer 3.4 stillschweigend um drei Jahre. Bitte teilen Sie mir bis am 7. Mai 2026 mit, ob eine Kündigung vorgesehen ist. + +Freundliche Grüsse +Ferdi — Fristen-Wächter +Property On, Bewirtschaftung`, + }, + { + id: 'awi-ferdi-02-msg-2', + at: '2026-05-08T06:05:00.000Z', + from: 'ferdi@property-on.ch', + to: 'fabian.ochsner@property-on.ch, #fristen-winterthur', + channel: AgentChannelType.EMAIL, + subject: 'Zweitmeldung: Kündigungsfrist OBJ-2288 — noch keine Rückmeldung', + body: `Guten Morgen Herr Ochsner + +Zu meiner Erinnerung vom 30. April 2026 ist bisher keine Rückmeldung eingegangen. Die Kündigungsfrist für OBJ-2288 endet am 30. Juni 2026. + +Ich habe diese Meldung zusätzlich im Team-Kanal «Fristen Winterthur» abgelegt. Bleibt eine Reaktion aus, lege ich der Leitung Bewirtschaftung eine Eskalation zur Freigabe vor. + +Freundliche Grüsse +Ferdi — Fristen-Wächter +Property On, Bewirtschaftung`, + }, + { + id: 'awi-ferdi-02-msg-3', + at: '2026-05-20T06:11:00.000Z', + from: 'ferdi@property-on.ch', + to: 'andrea.hersche@property-on.ch, thomas.buehlmann@property-on.ch', + channel: AgentChannelType.EMAIL, + subject: 'ENTWURF — Eskalation Kündigungsfrist OBJ-2288, Stichtag 30.06.2026', + body: `Guten Tag Frau Hersche, guten Tag Herr Bühlmann + +Für OBJ-2288 (Technoparkstrasse 7, Winterthur — Alpstein Feinbau AG) liegen zwei Meldungen ohne Reaktion vor: + + 30.04.2026 Erinnerung «3 Monate» an Fabian Ochsner + 08.05.2026 Zweitmeldung, zusätzlich im Team-Kanal Winterthur + +Die Kündigungsfrist endet am 30. Juni 2026. Ohne fristgerechte Kündigung verlängert sich der Vertrag um drei Jahre zu unveränderten Konditionen (Jahresmiete CHF 412 000 netto). + +Ich bitte um Entscheid zum weiteren Vorgehen. + +Freundliche Grüsse +Ferdi — Fristen-Wächter +Property On, Bewirtschaftung + +— Dieser Entwurf ist angehalten und geht erst nach Ihrer Freigabe hinaus. —`, + }, + ], + availableActions: [ + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.EDIT, + AgentWorkItemAction.REJECT, + AgentWorkItemAction.OPEN_SOURCE, + ], + editableFields: [ + { + id: 'awi-ferdi-02-field-recipients', + label: 'Empfänger der Eskalation', + value: 'andrea.hersche@property-on.ch, thomas.buehlmann@property-on.ch', + helperText: 'Mehrere Adressen mit Komma trennen. Die Mandatsleitung Basel steht standardmässig in Kopie.', + }, + { + id: 'awi-ferdi-02-field-subject', + label: 'Betreff', + value: 'Eskalation Kündigungsfrist OBJ-2288 — Stichtag 30.06.2026, zwei Meldungen ohne Reaktion', + }, + { + id: 'awi-ferdi-02-field-body', + label: 'Text der Eskalation', + value: + 'Für OBJ-2288 (Technoparkstrasse 7, Winterthur — Alpstein Feinbau AG) liegen zwei Meldungen ohne Reaktion vor. Die Kündigungsfrist endet am 30. Juni 2026. Ohne fristgerechte Kündigung verlängert sich der Vertrag um drei Jahre zu unveränderten Konditionen.', + multiline: true, + helperText: 'Meldeverlauf und Fundstelle werden automatisch angehängt.', + }, + { + id: 'awi-ferdi-02-field-deadline', + label: 'Gewünschte Rückmeldung bis', + value: '27.05.2026', + helperText: 'Nach diesem Datum meldet Ferdi den Vorgang erneut.', + }, + ], + history: [ + { + id: 'awi-ferdi-02-hist-1', + at: '2026-04-30T06:05:00.000Z', + actor: 'Ferdi', + action: 'Erinnerung versandt', + note: 'Meldeschwelle «3 Monate» erreicht.', + }, + { + id: 'awi-ferdi-02-hist-2', + at: '2026-05-08T06:05:00.000Z', + actor: 'Ferdi', + action: 'Zweitmeldung versandt', + note: 'Fünf Arbeitstage ohne Reaktion — zusätzlich im Team-Kanal Winterthur.', + }, + { + id: 'awi-ferdi-02-hist-3', + at: '2026-05-20T06:11:00.000Z', + actor: 'Ferdi', + action: 'Eskalation zur Freigabe vorgelegt', + note: 'Versand angehalten — wartet auf die Entscheidung der Leitung Bewirtschaftung.', + }, + ], + }, + + // ── Sina — erledigt ──────────────────────────────────────────────────────── + { + id: 'awi-sina-01', + agentId: AGENT_IDS.SINA, + kind: AgentWorkItemKind.CONTRACT_ANSWER, + area: AgentDomainArea.DOCUMENTATION, + title: 'Auskunft zur Indexklausel — Voltera Systems AG, Baarerstrasse 82', + summary: + 'Frage aus dem Teamkanal der Bewirtschaftung zur nächstmöglichen Indexanpassung. Die Antwort wurde in 14 Sekunden mit wörtlichem Zitat, Dokumentname und Seitenangabe zurückgegeben.', + objectId: 'OBJ-2094', + objectLabel: 'Baarerstrasse 82, 6300 Zug — Bürofläche 3. Obergeschoss, 720 m²', + sourceChannel: AgentChannelType.WORKSPACE_CHAT, + targetChannel: AgentChannelType.WORKSPACE_CHAT, + createdAt: '2026-05-20T08:12:31.000Z', + completedAt: '2026-05-20T08:12:45.000Z', + priority: AgentWorkItemPriority.LOW, + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + inputs: [ + { + label: 'Frage', + value: '«Ab wann können wir bei Voltera an der Baarerstrasse 82 das nächste Mal indexieren?»', + }, + { label: 'Fragende Person', value: 'Petra Lüthi, Bewirtschaftung — Teamkanal «bewirtschaftung-zuerich»' }, + { + label: 'Durchsuchte Ablagen', + value: ['Dokumentensystem (Vertragsakte OBJ-2094)', 'ImmoTop2-Belege zum Mietverhältnis'], + }, + ], + result: [ + { + label: 'Antwort', + value: + 'Eine Indexanpassung ist erstmals per 01.10.2026 möglich, danach jährlich jeweils auf den 1. Oktober. Massgebend ist der Landesindex der Konsumentenpreise, Basis Stand September 2021.', + }, + { label: 'Nächstmöglicher Anpassungstermin', value: '01.10.2026 — Ankündigung spätestens 30 Tage vorher' }, + { label: 'Weitergabesatz', value: '100 % der Indexveränderung gemäss Ziffer 6.1 des Mietvertrags' }, + { label: 'Fundstellen belegt', value: 'ja — Direktzitat, Dokumentname und Seitenzahl mitgeliefert' }, + { label: 'Antwortzeit', value: '14 Sekunden von der Frage bis zur belegten Auskunft' }, + ], + processingSteps: [ + { + id: 'awi-sina-01-step-1', + label: 'Richtige Vertragsakte bestimmt', + detail: 'Über Objektnummer und Mietverhältnis die gültige Akte OBJ-2094 / Voltera Systems AG geöffnet.', + }, + { + id: 'awi-sina-01-step-2', + label: 'Massgebende Stellen gesucht', + detail: 'Mietvertrag und beide Nachträge auf Regelungen zur Indexierung durchgesehen.', + }, + { + id: 'awi-sina-01-step-3', + label: 'Gültigen Stand geprüft', + detail: 'Nachtrag 1 ersetzt die ursprüngliche Ziffer 6.1 — der Nachtrag ist massgebend.', + }, + { + id: 'awi-sina-01-step-4', + label: 'Antwort mit Zitat und Fundstelle ausgegeben', + detail: 'Antwort im Teamkanal, Frage und Quelle im Auskunftsprotokoll festgehalten.', + }, + ], + sourceReferences: [ + { + id: 'awi-sina-01-src-1', + label: 'Mietvertrag Voltera Systems AG', + documentName: 'Mietvertrag_OBJ-2094_Voltera-Systems_2021-10-01.pdf', + locator: 'Seite 9, Ziffer 6.1', + quote: + '«Der Mietzins ist an den Landesindex der Konsumentenpreise gebunden. Eine Anpassung kann erstmals fünf Jahre nach Mietbeginn und danach jährlich auf den 1. Oktober verlangt werden, jeweils zu 100 Prozent der Indexveränderung.»', + }, + { + id: 'awi-sina-01-src-2', + label: 'Nachtrag 1 zum Mietvertrag', + documentName: 'Nachtrag-1_OBJ-2094_2023-05-15.pdf', + locator: 'Seite 1, Ziffer 2', + quote: + '«Die Ankündigung einer Indexanpassung hat der Mieterin spätestens 30 Tage vor dem Anpassungstermin schriftlich zuzugehen. Ziffer 6.1 des Mietvertrags bleibt im Übrigen unverändert.»', + }, + ], + messageThread: [ + { + id: 'awi-sina-01-msg-1', + at: '2026-05-20T08:12:31.000Z', + from: 'Petra Lüthi', + to: 'Sina — Vertragsauskunft', + channel: AgentChannelType.WORKSPACE_CHAT, + body: 'Sina, kurze Frage: Ab wann können wir bei Voltera an der Baarerstrasse 82 das nächste Mal indexieren? Die Mieterin hat heute Morgen angerufen.', + }, + { + id: 'awi-sina-01-msg-2', + at: '2026-05-20T08:12:45.000Z', + from: 'Sina — Vertragsauskunft', + to: 'Petra Lüthi', + channel: AgentChannelType.WORKSPACE_CHAT, + body: `Guten Morgen Petra + +Eine Indexanpassung ist erstmals per 1. Oktober 2026 möglich, danach jährlich auf den 1. Oktober. Weitergegeben werden 100 Prozent der Indexveränderung. + +Fundstelle: Mietvertrag_OBJ-2094_Voltera-Systems_2021-10-01.pdf, Seite 9, Ziffer 6.1 — «Der Mietzins ist an den Landesindex der Konsumentenpreise gebunden. Eine Anpassung kann erstmals fünf Jahre nach Mietbeginn und danach jährlich auf den 1. Oktober verlangt werden, jeweils zu 100 Prozent der Indexveränderung.» + +Ergänzend Nachtrag-1_OBJ-2094_2023-05-15.pdf, Seite 1, Ziffer 2: Die Ankündigung muss der Mieterin spätestens 30 Tage vorher schriftlich zugehen. + +Freundliche Grüsse +Sina — Vertragsauskunft`, + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-sina-01-hist-1', + at: '2026-05-20T08:12:45.000Z', + actor: 'Sina', + action: 'Auskunft mit Fundstelle erteilt', + note: 'Zwei Fundstellen belegt, Eintrag im Auskunftsprotokoll erstellt.', + }, + { + id: 'awi-sina-01-hist-2', + at: '2026-05-20T08:19:00.000Z', + actor: 'Petra Lüthi', + action: 'Auskunft als hilfreich markiert', + note: 'Rückmeldung im Teamkanal: «Perfekt, danke — genau das habe ich gebraucht.»', + }, + ], + }, + + // ── Sina — pendent: fehlende oder widersprüchliche Informationen ─────────── + { + id: 'awi-sina-02', + agentId: AGENT_IDS.SINA, + kind: AgentWorkItemKind.DOC_GAP, + area: AgentDomainArea.DOCUMENTATION, + title: 'Widersprüchliche Flächenangaben — Aeschenvorstadt 26, Basel', + summary: + 'Zur Frage nach der abrechnungsrelevanten Nutzfläche liegen zwei hinterlegte Dokumente mit unterschiedlichen Werten vor. Ohne Klärung, welches Dokument massgebend ist, kann keine belegte Auskunft erteilt werden.', + objectId: 'OBJ-2617', + objectLabel: 'Aeschenvorstadt 26, 4051 Basel — Büro- und Ladenfläche, Erdgeschoss bis 2. Obergeschoss', + sourceChannel: AgentChannelType.EMAIL, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-18T07:02:00.000Z', + priority: AgentWorkItemPriority.HIGH, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Fehlende oder widersprüchliche Informationen: Der Flächenausweis von 2018 und der Nachtrag 3 von 2024 nennen unterschiedliche Nutzflächen (1 284 m² gegenüber 1 341 m²). Eine Auskunft ohne eindeutige Fundstelle wird nicht ausgegeben.', + decisionQuestion: + 'Welches Dokument gilt für die Nebenkostenabrechnung 2026 als massgebend — der Flächenausweis vom 14.06.2018 oder der Nachtrag 3 vom 02.02.2024 — oder soll zuerst eine Neuvermessung beauftragt werden?', + inputs: [ + { + label: 'Frage', + value: + '«Wie gross ist die abrechnungsrelevante Nutzfläche an der Aeschenvorstadt 26? Ich brauche den Wert für die Nebenkostenabrechnung 2026.»', + }, + { label: 'Fragende Person', value: 'Petra Lüthi, Bewirtschaftung Basel — E-Mail an sina@property-on.ch' }, + { + label: 'Durchsuchte Ablagen', + value: [ + 'Dokumentensystem (Vertragsakte OBJ-2617)', + 'SharePoint-Objektbibliothek «Liegenschaften Basel»', + 'Laufwerksablage Rheinknie — Altbestand vor 2019', + ], + }, + { label: 'Gefundene Dokumente mit Flächenangabe', value: '3 Dokumente, davon 2 mit abweichenden Werten' }, + ], + result: [ + { label: 'Stand', value: '«nicht dokumentiert» — es liegt keine eindeutig massgebende Fundstelle vor' }, + { label: 'Wert A', value: '1 284 m² Nutzfläche — Flächenausweis vom 14.06.2018' }, + { label: 'Wert B', value: '1 341 m² Nutzfläche — Nachtrag 3 zum Mietvertrag vom 02.02.2024' }, + { label: 'Differenz', value: '57 m² beziehungsweise 4,4 Prozent — relevant für den Verteilschlüssel' }, + { + label: 'Erkannte Lücke', + value: + 'Zum Umbau des Erdgeschosses 2023 fehlt ein unterzeichneter Flächenausweis; der Nachtrag 3 verweist auf eine Beilage, die in keiner freigegebenen Ablage auffindbar ist.', + }, + { + label: 'Vorgemerkt für den Lückenreport', + value: 'ja — Eintrag im Lückenreport per 01.06.2026, Standort Basel', + }, + ], + processingSteps: [ + { + id: 'awi-sina-02-step-1', + label: 'Alle Ablagen zum Objekt durchsucht', + detail: 'Dokumentensystem, Objektbibliothek Basel und historische Laufwerksablage durchgesehen.', + }, + { + id: 'awi-sina-02-step-2', + label: 'Drei Dokumente mit Flächenangaben gefunden', + detail: 'Flächenausweis 2018, Mietvertrag 2019 und Nachtrag 3 von 2024.', + }, + { + id: 'awi-sina-02-step-3', + label: 'Widerspruch festgestellt', + detail: 'Der Nachtrag nennt eine höhere Nutzfläche als der Flächenausweis, ohne die Änderung zu erklären.', + }, + { + id: 'awi-sina-02-step-4', + label: 'Verwiesene Beilage gesucht — nicht auffindbar', + detail: 'Die im Nachtrag 3 erwähnte «Beilage 2, Flächenaufstellung nach Umbau» liegt in keiner Ablage vor.', + }, + { + id: 'awi-sina-02-step-5', + label: 'Auskunft angehalten und Lücke gemeldet', + detail: 'Statt einer Schätzung wurde «nicht dokumentiert» ausgegeben und die Rückfrage vorgelegt.', + }, + ], + sourceReferences: [ + { + id: 'awi-sina-02-src-1', + label: 'Flächenausweis 2018', + documentName: 'Flaechenausweis_OBJ-2617_2018-06-14.pdf', + locator: 'Seite 2, Tabelle «Zusammenzug»', + quote: + '«Total vermietbare Nutzfläche Erdgeschoss bis 2. Obergeschoss: 1 284 m², ermittelt nach SIA 416, Stand 14. Juni 2018.»', + }, + { + id: 'awi-sina-02-src-2', + label: 'Nachtrag 3 zum Mietvertrag', + documentName: 'Nachtrag-3_OBJ-2617_2024-02-02.pdf', + locator: 'Seite 1, Ziffer 1', + quote: + '«Die Parteien halten fest, dass die vermietete Nutzfläche nach dem Umbau des Erdgeschosses neu 1 341 m² beträgt. Massgebend ist die Flächenaufstellung gemäss Beilage 2.»', + }, + { + id: 'awi-sina-02-src-3', + label: 'Mietvertrag Birsfeld Logistik AG', + documentName: 'Mietvertrag_OBJ-2617_Birsfeld-Logistik_2019-04-01.pdf', + locator: 'Seite 3, Ziffer 2.1', + quote: + '«Die Nebenkosten werden nach dem Verhältnis der vermieteten Nutzfläche zur Gesamtnutzfläche der Liegenschaft verteilt.»', + }, + ], + availableActions: [ + AgentWorkItemAction.DECIDE, + AgentWorkItemAction.ANSWER_QUERY, + AgentWorkItemAction.OPEN_SOURCE, + ], + history: [ + { + id: 'awi-sina-02-hist-1', + at: '2026-05-18T07:02:00.000Z', + actor: 'Petra Lüthi', + action: 'Frage gestellt', + note: 'E-Mail an das Postfach der Vertragsauskunft.', + }, + { + id: 'awi-sina-02-hist-2', + at: '2026-05-18T07:03:00.000Z', + actor: 'Sina', + action: '«nicht dokumentiert» ausgegeben und Rückfrage vorgelegt', + note: 'Zwei widersprüchliche Fundstellen, verwiesene Beilage fehlt.', + }, + { + id: 'awi-sina-02-hist-3', + at: '2026-05-19T14:30:00.000Z', + actor: 'Petra Lüthi', + action: 'Rückfrage an die Eigentümerschaft weitergeleitet', + note: 'Anfrage zur Beilage 2 an die Rheinknie Immobilien AG, Antwort ausstehend.', + }, + ], + }, + + // ── Lea — erledigt ───────────────────────────────────────────────────────── + { + id: 'awi-lea-01', + agentId: AGENT_IDS.LEA, + kind: AgentWorkItemKind.MESSAGE_TRIAGE, + area: AgentDomainArea.INQUIRIES, + title: 'Nachrichteneingang sortiert — 14 Anfragen zugeordnet, ein Duplikat zusammengeführt', + summary: + 'Die Vormittagssichtung hat 14 eingegangene Nachrichten den ausgeschriebenen Flächen zugeordnet. Eine Portal-Lead-Mail und eine Telefonnotiz betrafen dieselbe Person und wurden zu einem Vorgang zusammengeführt.', + objectId: 'OBJ-2365', + objectLabel: 'Hardturmstrasse 118, 8005 Zürich — Bürofläche 640 m², bezugsbereit ab 01.10.2026', + sourceChannel: AgentChannelType.PORTAL_LEAD_MAIL, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-19T10:41:00.000Z', + completedAt: '2026-05-19T10:52:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + inputs: [ + { + label: 'Eingegangene Nachrichten', + value: [ + '9 Portal-Lead-Mails der Immobilienplattformen', + '3 Direktmails an anfragen@property-on.ch', + '1 Website-Formular «Fläche anfragen»', + '1 Telefonnotiz des Empfangs Zürich', + ], + }, + { label: 'Abgleichzeitraum für Duplikate', value: 'Anfragen der letzten 90 Tage, Stand 19.05.2026' }, + { label: 'Betroffene Flächen', value: '6 ausgeschriebene Flächen in Zürich, Winterthur und Zug' }, + ], + result: [ + { label: 'Zugeordnete Anfragen', value: '14 von 14 — keine Nachricht blieb ohne Flächenbezug' }, + { + label: 'Zusammengeführtes Duplikat', + value: + 'Marcel Sennhauser, Limmatgrund Treuhand AG — Portal-Lead-Mail vom 19.05.2026, 09:14 Uhr und Telefonnotiz vom 19.05.2026, 10:02 Uhr betreffen dieselbe Fläche OBJ-2365.', + }, + { label: 'Unvollständige Anfragen', value: '2 — Rückfrageentwürfe für die Sammelvorlage 08:30 Uhr erstellt' }, + { label: 'Eingangsbestätigungen versandt', value: '13 (das Duplikat wurde nur einmal bestätigt)' }, + { label: 'Durchschnittliche Erstreaktionszeit', value: '9 Minuten' }, + ], + processingSteps: [ + { + id: 'awi-lea-01-step-1', + label: 'Nachrichten aus allen Kanälen eingesammelt', + detail: 'Portal-Postfach, Anfragen-Postfach, Website-Formular und Telefonnotizen abgeholt.', + }, + { + id: 'awi-lea-01-step-2', + label: 'Jede Nachricht einer ausgeschriebenen Fläche zugeordnet', + detail: 'Zuordnung über Objektnummer, Adresse oder Inseratsbezug in der Nachricht.', + }, + { + id: 'awi-lea-01-step-3', + label: 'Doppelte Anfragen erkannt', + detail: 'Abgleich von Name, Firma, Telefonnummer und E-Mail über die letzten 90 Tage.', + }, + { + id: 'awi-lea-01-step-4', + label: 'Duplikat zu einem Vorgang zusammengeführt', + detail: 'Telefonnotiz als zweiter Kontaktpunkt am bestehenden Vorgang vermerkt.', + }, + { + id: 'awi-lea-01-step-5', + label: 'Eingangsbestätigungen versandt und protokolliert', + detail: 'Je Interessent eine Bestätigung, Verlauf im Anfrage-Log abgelegt.', + }, + ], + sourceReferences: [ + { + id: 'awi-lea-01-src-1', + label: 'Anfrage-Log Fläche OBJ-2365', + documentName: 'Anfrage-Log_OBJ-2365_2026.csv', + locator: 'Zeilen 41–48', + }, + { + id: 'awi-lea-01-src-2', + label: 'Telefonnotiz Empfang Zürich', + documentName: 'Telefonnotiz_2026-05-19_Empfang-Zuerich.txt', + locator: 'Eintrag 10:02 Uhr', + }, + ], + messageThread: [ + { + id: 'awi-lea-01-msg-1', + at: '2026-05-19T09:14:00.000Z', + from: 'lead-service@plattform-immobilien.ch', + to: 'portal-leads@property-on.ch', + channel: AgentChannelType.PORTAL_LEAD_MAIL, + subject: 'Neue Anfrage zu Ihrem Inserat «Bürofläche 640 m², Zürich West»', + body: `Guten Tag + +Zu Ihrem Inserat ist eine neue Anfrage eingegangen. + + Inserat: Bürofläche 640 m², Hardturmstrasse 118, 8005 Zürich + Name: Marcel Sennhauser + Firma: Limmatgrund Treuhand AG, Zürich + Telefon: +41 44 288 51 20 + E-Mail: m.sennhauser@limmatgrund-treuhand.ch + + Nachricht: «Wir suchen per Herbst 2026 rund 600 m² Bürofläche in Zürich West + für 42 Mitarbeitende. Ist eine Besichtigung möglich? Sechs Parkplätze wären + wichtig.» + +Freundliche Grüsse +Plattform Immobilien — Lead-Service`, + }, + { + id: 'awi-lea-01-msg-2', + at: '2026-05-19T10:02:00.000Z', + from: 'Empfang Zürich', + to: 'telefonnotiz@property-on.ch', + channel: AgentChannelType.PHONE_NOTE, + subject: 'Telefonnotiz — Herr Sennhauser, Limmatgrund Treuhand', + body: `Anruf 10:02 Uhr. Herr Sennhauser von der Limmatgrund Treuhand AG hat sich nach der Bürofläche an der Hardturmstrasse erkundigt. Er habe bereits über das Portal angefragt und wolle sichergehen, dass die Anfrage angekommen sei. Rückruf erbeten auf +41 44 288 51 20.`, + }, + { + id: 'awi-lea-01-msg-3', + at: '2026-05-19T10:52:00.000Z', + from: 'lea@property-on.ch', + to: 'm.sennhauser@limmatgrund-treuhand.ch', + channel: AgentChannelType.EMAIL, + subject: 'Ihre Anfrage zur Bürofläche Hardturmstrasse 118, Zürich — Eingang bestätigt', + body: `Guten Tag Herr Sennhauser + +Besten Dank für Ihre Anfrage zur Bürofläche an der Hardturmstrasse 118 in Zürich sowie für Ihren Anruf von heute Vormittag. Beide Kontakte betreffen denselben Vorgang; wir führen sie unter einer Anfrage. + +Ihre Angaben sind vollständig erfasst: + Flächenbedarf rund 600 m² + Nutzung Büro, 42 Arbeitsplätze + Mietbeginn Herbst 2026 + Parkplätze 6 + +Die zuständige Person der Vermarktung meldet sich innerhalb von zwei Arbeitstagen bei Ihnen. + +Freundliche Grüsse +Lea — Anfragen-Managerin +Property On, Vermarktung`, + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-lea-01-hist-1', + at: '2026-05-19T10:41:00.000Z', + actor: 'Lea', + action: 'Sichtung gestartet', + note: '14 Nachrichten aus vier Kanälen übernommen.', + }, + { + id: 'awi-lea-01-hist-2', + at: '2026-05-19T10:52:00.000Z', + actor: 'Lea', + action: 'Sichtung abgeschlossen', + note: 'Ein Duplikat zusammengeführt, 13 Eingangsbestätigungen versandt.', + }, + ], + }, + + // ── Lea — pendent: Entscheidung durch den Bewirtschafter ─────────────────── + { + id: 'awi-lea-02', + agentId: AGENT_IDS.LEA, + kind: AgentWorkItemKind.INQUIRY_QUALIFIED, + area: AgentDomainArea.INQUIRIES, + title: 'Grenzfall zur Entscheidung — Nordstern Gastro GmbH, Zürcherstrasse 204', + summary: + 'Die Anfrage erfüllt fünf von sieben Must-Kriterien. Budget und Mietbeginn liegen knapp ausserhalb des Rahmens — ein Grenzfall wird nie automatisch abgesagt, sondern zur Entscheidung vorgelegt.', + objectId: 'OBJ-2842', + objectLabel: 'Zürcherstrasse 204, 8406 Winterthur Neuhegi — Retail- und Gastrofläche, 310 m²', + sourceChannel: AgentChannelType.PORTAL_LEAD_MAIL, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-20T07:36:00.000Z', + priority: AgentWorkItemPriority.HIGH, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Entscheidung durch den Bewirtschafter: Zwei Must-Kriterien sind knapp verfehlt — Budgetrahmen um 8 Prozent unter der Mietpreisvorstellung, Mietbeginn zwei Monate nach der Verfügbarkeit. Bei Grenzfällen ist die automatische Absage gesperrt.', + decisionQuestion: + 'Soll die Anfrage der Nordstern Gastro GmbH trotz Budgetabweichung von 8 Prozent und zwei Monaten Leerstand zur Besichtigung zugelassen werden, oder folgt die Absage mit der hinterlegten Vorlage?', + inputs: [ + { + label: 'Bedarfsprofil', + value: [ + 'Fläche: 280 bis 340 m², Erdgeschoss mit Schaufenster', + 'Nutzung: Gastronomie mit Take-away, keine Nachtnutzung', + 'Mietbeginn: 01.03.2027', + 'Budget: CHF 285 pro m² und Jahr, netto', + 'Vertragsdauer: 8 Jahre fest, Option auf 5 Jahre', + ], + }, + { + label: 'Must-Kriterien der Fläche', + value: [ + 'Flächenbedarf 250–350 m² — erfüllt', + 'Nutzungsart Gastronomie zulässig — erfüllt', + 'Mietbeginn ab 01.01.2027 — knapp verfehlt (01.03.2027)', + 'Mietpreisvorstellung CHF 310 pro m² und Jahr — knapp verfehlt (CHF 285)', + 'Bonitätsnachweis beibringbar — erfüllt', + 'Vertragsdauer mindestens 5 Jahre — erfüllt', + 'Parkplatzbedarf gedeckt — erfüllt', + ], + }, + { label: 'Herkunftskanal', value: 'Portal-Lead-Mail vom 20.05.2026, 07:12 Uhr, ergänzt per Direktmail 07:31 Uhr' }, + { + label: 'Bisheriger Kontakt', + value: 'Kein früherer Vorgang in den letzten 90 Tagen — kein Duplikat', + }, + ], + result: [ + { label: 'Qualifizierung', value: '5 von 7 Must-Kriterien erfüllt, 2 knapp verfehlt, 0 offen' }, + { + label: 'Interessent', + value: 'Nordstern Gastro GmbH, Winterthur — Frau Sabina Rüegg, Geschäftsführerin, drei Betriebe in Winterthur und Frauenfeld', + }, + { + label: 'Wirtschaftliche Wirkung', + value: + 'Budgetabweichung CHF 7 750 pro Jahr gegenüber der Mietpreisvorstellung; zwei Monate Leerstand entsprechen rund CHF 16 000 Mietzinsausfall.', + }, + { + label: 'Argument für die Zulassung', + value: + 'Die Fläche steht seit 14 Monaten im Angebot; es liegen in diesem Zeitraum nur zwei weitere ernsthafte Anfragen vor, beide ohne Bonitätsnachweis.', + }, + { label: 'Stand', value: 'Kein Schritt Richtung Termin ausgelöst — wartet auf die Entscheidung der Vermarktung' }, + { label: 'Vorbereitet', value: 'Drei Zeitfenster am 26., 27. und 28.05.2026, Reservation noch nicht gesetzt' }, + ], + processingSteps: [ + { + id: 'awi-lea-02-step-1', + label: 'Anfrage aus zwei Nachrichten zusammengeführt', + detail: 'Portal-Lead-Mail und nachgereichte Direktmail derselben Person zu einem Vorgang verbunden.', + }, + { + id: 'awi-lea-02-step-2', + label: 'Freitext in ein Bedarfsprofil überführt', + detail: 'Fläche, Nutzung, Mietbeginn, Budget und Vertragsdauer strukturiert erfasst.', + }, + { + id: 'awi-lea-02-step-3', + label: 'Gegen die Must-Kriterien der Fläche geprüft', + detail: 'Je Kriterium erfüllt, nicht erfüllt oder offen festgehalten.', + }, + { + id: 'awi-lea-02-step-4', + label: 'Als Grenzfall eingestuft', + detail: 'Beide Abweichungen liegen innerhalb der Grenzfall-Bandbreite — automatische Absage gesperrt.', + }, + { + id: 'awi-lea-02-step-5', + label: 'Kurzprofil zur Entscheidung vorgelegt', + detail: 'Freie Zeitfenster vorsorglich ermittelt, aber nicht reserviert.', + }, + ], + sourceReferences: [ + { + id: 'awi-lea-02-src-1', + label: 'Must-Kriterien Fläche OBJ-2842', + documentName: 'Vermarktungsauftrag_OBJ-2842_2025-03-01.pdf', + locator: 'Seite 2, Abschnitt «Grundvoraussetzungen»', + quote: + '«Mietbeginn ab 1. Januar 2027; Mietzinsvorstellung CHF 310 pro Quadratmeter und Jahr netto; Vertragsdauer mindestens fünf Jahre.»', + }, + { + id: 'awi-lea-02-src-2', + label: 'Anfrage-Log Fläche OBJ-2842', + documentName: 'Anfrage-Log_OBJ-2842_2026.csv', + locator: 'Zeile 12', + }, + ], + messageThread: [ + { + id: 'awi-lea-02-msg-1', + at: '2026-05-20T07:12:00.000Z', + from: 'lead-service@plattform-immobilien.ch', + to: 'portal-leads@property-on.ch', + channel: AgentChannelType.PORTAL_LEAD_MAIL, + subject: 'Neue Anfrage zu Ihrem Inserat «Retail-/Gastrofläche 310 m², Winterthur Neuhegi»', + body: `Guten Tag + +Zu Ihrem Inserat ist eine neue Anfrage eingegangen. + + Inserat: Retail- und Gastrofläche 310 m², Zürcherstrasse 204, 8406 Winterthur + Name: Sabina Rüegg + Firma: Nordstern Gastro GmbH, Winterthur + Telefon: +41 52 214 77 30 + E-Mail: s.rueegg@nordstern-gastro.ch + + Nachricht: «Wir betreiben drei Lokale in Winterthur und Frauenfeld und suchen + für unser viertes Konzept eine Erdgeschossfläche mit Schaufenster. Die Fläche + in Neuhegi passt sehr gut. Wir würden per März 2027 starten.» + +Freundliche Grüsse +Plattform Immobilien — Lead-Service`, + }, + { + id: 'awi-lea-02-msg-2', + at: '2026-05-20T07:31:00.000Z', + from: 's.rueegg@nordstern-gastro.ch', + to: 'anfragen@property-on.ch', + channel: AgentChannelType.EMAIL, + subject: 'Ergänzung zu unserer Anfrage — Zürcherstrasse 204, Winterthur', + body: `Guten Tag + +Ergänzend zu unserer Anfrage über die Plattform von heute Morgen die noch fehlenden Angaben: + + Nutzung Gastronomie mit Take-away, keine Nachtnutzung + Mietbeginn 1. März 2027 (Umbau des bisherigen Standorts läuft bis Februar) + Budget CHF 285 pro Quadratmeter und Jahr, netto + Vertragsdauer 8 Jahre fest, mit Option auf weitere 5 Jahre + Bonität Jahresabschlüsse 2023–2025 und Betreibungsregisterauszug liegen bereit + +Über eine Besichtigung würden wir uns sehr freuen. + +Freundliche Grüsse +Sabina Rüegg +Geschäftsführerin, Nordstern Gastro GmbH +Winterthur`, + }, + { + id: 'awi-lea-02-msg-3', + at: '2026-05-20T07:34:00.000Z', + from: 'lea@property-on.ch', + to: 's.rueegg@nordstern-gastro.ch', + channel: AgentChannelType.EMAIL, + subject: 'Ihre Anfrage zur Fläche Zürcherstrasse 204, Winterthur — Eingang bestätigt', + body: `Guten Tag Frau Rüegg + +Besten Dank für Ihre Anfrage und die rasch nachgereichten Angaben zur Retail- und Gastrofläche an der Zürcherstrasse 204 in Winterthur. + +Ihre Unterlagen sind vollständig. Die zuständige Person der Vermarktung prüft Ihr Anliegen und meldet sich innerhalb von zwei Arbeitstagen bei Ihnen. + +Freundliche Grüsse +Lea — Anfragen-Managerin +Property On, Vermarktung`, + }, + { + id: 'awi-lea-02-msg-4', + at: '2026-05-20T07:36:00.000Z', + from: 'lea@property-on.ch', + to: 'nadine.brunner@property-on.ch', + channel: AgentChannelType.EMAIL, + subject: 'ENTSCHEID NÖTIG — Grenzfall Nordstern Gastro GmbH, OBJ-2842', + body: `Guten Morgen Frau Brunner + +Zur Fläche OBJ-2842 (Zürcherstrasse 204, Winterthur Neuhegi) liegt ein Grenzfall vor: + + erfüllt Fläche, Nutzung, Bonität, Vertragsdauer, Parkplätze + knapp verfehlt Budget CHF 285 statt CHF 310 pro m² und Jahr (−8 %) + knapp verfehlt Mietbeginn 01.03.2027 statt ab 01.01.2027 (2 Monate Leerstand) + +Die Fläche steht seit 14 Monaten im Angebot. Ich habe nichts Richtung Termin ausgelöst und warte auf Ihren Entscheid. Drei Zeitfenster am 26., 27. und 28. Mai stünden bereit. + +Freundliche Grüsse +Lea — Anfragen-Managerin +Property On, Vermarktung`, + }, + ], + availableActions: [ + AgentWorkItemAction.DECIDE, + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.EDIT, + AgentWorkItemAction.REJECT, + AgentWorkItemAction.OPEN_SOURCE, + ], + editableFields: [ + { + id: 'awi-lea-02-field-slots', + label: 'Vorgeschlagene Besichtigungsfenster', + value: '26.05.2026 10:00, 27.05.2026 14:00, 28.05.2026 09:30', + helperText: 'Termindauer 45 Minuten, Pufferzeit 15 Minuten vor und nach dem Termin.', + }, + { + id: 'awi-lea-02-field-contact', + label: 'Ansprechperson vor Ort', + value: 'Marco Steinegger, Vermarktung Winterthur', + }, + { + id: 'awi-lea-02-field-note', + label: 'Hinweis an den Interessenten', + value: + 'Der Mietzins ist Verhandlungssache; ein Mietbeginn per März 2027 ist grundsätzlich prüfbar. Bitte bringen Sie die Jahresabschlüsse 2023 bis 2025 an die Besichtigung mit.', + multiline: true, + helperText: 'Wird der Terminbestätigung angehängt, sobald die Freigabe erteilt ist.', + }, + { + id: 'awi-lea-02-field-reject-reason', + label: 'Begründung bei Absage', + value: 'Die Mietpreisvorstellung und der gewünschte Mietbeginn liegen ausserhalb der Grundvoraussetzungen dieses Objekts.', + multiline: true, + helperText: 'Wird in die hinterlegte Absagevorlage eingesetzt, falls Sie ablehnen.', + }, + ], + history: [ + { + id: 'awi-lea-02-hist-1', + at: '2026-05-20T07:12:00.000Z', + actor: 'Lea', + action: 'Anfrage erfasst', + note: 'Portal-Lead-Mail der Plattform, Fläche OBJ-2842 zugeordnet.', + }, + { + id: 'awi-lea-02-hist-2', + at: '2026-05-20T07:33:00.000Z', + actor: 'Lea', + action: 'Bedarfsprofil vervollständigt', + note: 'Nachgereichte Direktmail zusammengeführt, alle Mindestangaben vorhanden.', + }, + { + id: 'awi-lea-02-hist-3', + at: '2026-05-20T07:36:00.000Z', + actor: 'Lea', + action: 'Als Grenzfall zur Entscheidung vorgelegt', + note: 'Automatische Absage gesperrt, keine Kalenderreservation gesetzt.', + }, + ], + }, + + // ── Bruno — erledigt ─────────────────────────────────────────────────────── + { + id: 'awi-bruno-01', + agentId: AGENT_IDS.BRUNO, + kind: AgentWorkItemKind.BRIEFING, + area: AgentDomainArea.VIEWINGS, + title: 'Briefing zum Aussentermin — Ostwind Retail AG, St. Leonhard-Strasse 35', + summary: + 'Zwei Stunden vor dem Vor-Ort-Termin in St. Gallen wurde das einseitige Briefing erstellt und zugestellt: Objektdaten, Einordnung des Gegenübers, drei Verkaufsargumente, drei erwartbare Einwände sowie zwei markierte Lücken.', + objectId: 'OBJ-2510', + objectLabel: 'St. Leonhard-Strasse 35, 9000 St. Gallen — Ladenfläche Erdgeschoss, 480 m²', + sourceChannel: AgentChannelType.CALENDAR, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-02-17T07:00:00.000Z', + completedAt: '2026-02-17T07:12:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + inputs: [ + { + label: 'Kalendertermin', + value: 'Vor-Ort-Termin 17.02.2026, 09:00–10:00 Uhr, St. Leonhard-Strasse 35, St. Gallen', + }, + { label: 'Teilnehmende', value: 'Rahel Studer (Vermarktung), Beat Iten (Ostwind Retail AG, Leiter Expansion)' }, + { + label: 'Herangezogene Unterlagen', + value: [ + 'Objektdossier OBJ-2510 aus der Dokumentenablage', + 'Stammdaten und Konditionen aus dem Bewirtschaftungssystem', + 'Öffentlicher Handelsregistereintrag und Firmenwebsite der Ostwind Retail AG', + ], + }, + ], + result: [ + { + label: 'Objektdaten', + value: + '480 m² Ladenfläche, Ausbaustandard 2019, Mietzins CHF 340 pro m² und Jahr netto, Nebenkosten CHF 42 akonto, verfügbar ab 01.06.2026, 4 Parkplätze in der Einstellhalle', + }, + { + label: 'Gegenüber', + value: + 'Ostwind Retail AG, Sitz St. Gallen, gegründet 2011, 22 Filialen in der Ostschweiz, letzte Standorteröffnung Frauenfeld im November 2025 (Quelle: Firmenwebsite, abgerufen 17.02.2026)', + }, + { + label: 'Drei Verkaufsargumente', + value: [ + 'Passantenfrequenz an der St. Leonhard-Strasse: direkte Achse zwischen Bahnhof und Altstadt', + 'Ausbaustandard 2019 — Ladenbau kann weitgehend übernommen werden, kein Rückbau nötig', + 'Vier eigene Parkplätze in der Einstellhalle, in der Innenstadt selten verfügbar', + ], + }, + { + label: 'Drei erwartbare Einwände mit Antwort', + value: [ + '«Der Mietzins liegt über unserem Rahmen.» — Vergleichbare Erdgeschossflächen an der Achse liegen bei CHF 355 bis 390 pro m² und Jahr.', + '«480 m² sind uns zu gross.» — Eine Abtrennung des hinteren Drittels ist baulich vorgesehen und im Grundriss ausgewiesen.', + '«Wir brauchen früher als Juni.» — Die Vormieterin zieht bereits im April aus; ein früherer Bezug ist prüfbar.', + ], + }, + { + label: 'Markierte Lücken', + value: [ + 'Nebenkostenabrechnung 2025 liegt noch nicht vor — letzter Stand 2024', + 'Kein aktuelles Fotomaterial der Fläche nach dem Auszug der Vormieterin', + ], + }, + { + label: 'Zustellung', + value: 'E-Mail an rahel.studer@property-on.ch am 17.02.2026, 07:12 Uhr, inklusive Audiofassung für die Anfahrt', + }, + ], + processingSteps: [ + { + id: 'awi-bruno-01-step-1', + label: 'Termin im Kalender erkannt', + detail: 'Zwei Stunden Vorlauf erreicht — Ort, Objektbezug und Teilnehmende aus dem Eintrag übernommen.', + }, + { + id: 'awi-bruno-01-step-2', + label: 'Objektinformationen zusammengetragen', + detail: 'Fläche, Ausbau, Konditionen, Verfügbarkeit und Vermarktungshistorie aus Dossier und Bewirtschaftungssystem.', + }, + { + id: 'awi-bruno-01-step-3', + label: 'Gegenüber über öffentliche Quellen eingeordnet', + detail: 'Handelsregistereintrag und Firmenwebsite ausgewertet, je Angabe die Quelle mitgeführt.', + }, + { + id: 'awi-bruno-01-step-4', + label: 'Lücken markiert statt geschätzt', + detail: 'Zwei fehlende Angaben sichtbar als Lücke ausgewiesen.', + }, + { + id: 'awi-bruno-01-step-5', + label: 'Briefing zugestellt', + detail: 'Eine Seite per E-Mail, dazu die Audiofassung für die Fahrt nach St. Gallen.', + }, + ], + sourceReferences: [ + { + id: 'awi-bruno-01-src-1', + label: 'Objektdossier OBJ-2510', + documentName: 'Objektdossier_OBJ-2510_St-Leonhard-Strasse-35.pdf', + locator: 'Seiten 1–4', + }, + { + id: 'awi-bruno-01-src-2', + label: 'Handelsregistereintrag Ostwind Retail AG', + documentName: 'Handelsregisterauszug_Ostwind-Retail-AG.pdf', + locator: 'abgerufen am 17.02.2026', + }, + { + id: 'awi-bruno-01-src-3', + label: 'Vermarktungshistorie OBJ-2510', + documentName: 'Vermarktungshistorie_OBJ-2510_2024-2026.xlsx', + locator: 'Blatt «Besichtigungen», Zeilen 3–11', + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-bruno-01-hist-1', + at: '2026-02-17T07:12:00.000Z', + actor: 'Bruno', + action: 'Briefing erstellt und zugestellt', + note: 'Eine Seite plus Audiofassung, zwei Lücken markiert.', + }, + { + id: 'awi-bruno-01-hist-2', + at: '2026-02-17T10:35:00.000Z', + actor: 'Rahel Studer', + action: 'Termin durchgeführt', + note: 'Rückmeldung: «Die drei Einwände kamen genau so — das Briefing hat gesessen.»', + }, + ], + }, + + // ── Bruno — pendent: technischer Fehler / fehlender Systemzugang ─────────── + { + id: 'awi-bruno-02', + agentId: AGENT_IDS.BRUNO, + kind: AgentWorkItemKind.SYSTEM_ISSUE, + area: AgentDomainArea.VIEWINGS, + title: 'Kein Lesezugriff auf die Objektbibliothek Luzern — Briefing unvollständig', + summary: + 'Für den Aussentermin um 11:00 Uhr in Luzern fehlen die Objektunterlagen: Die Objektbibliothek weist den Lesezugriff seit dem 19.05.2026 zurück. Das Briefing enthält damit nur die Angaben aus dem Bewirtschaftungssystem.', + objectId: 'OBJ-2178', + objectLabel: 'Pilatusstrasse 18, 6003 Luzern — Bürofläche 2. Obergeschoss, 390 m²', + sourceChannel: AgentChannelType.CALENDAR, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-20T06:58:00.000Z', + priority: AgentWorkItemPriority.CRITICAL, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Technischer Fehler beziehungsweise fehlender Systemzugang: Die SharePoint-Objektbibliothek «Liegenschaften Luzern» weist den Lesezugriff seit dem 19.05.2026, 22:14 Uhr zurück. Drei Zugriffsversuche sind fehlgeschlagen; Objektdossier, Grundriss und Konditionenblatt fehlen im Briefing.', + decisionQuestion: + 'Soll das Briefing mit den vorhandenen Angaben und sichtbar markierten Lücken um 09:00 Uhr versandt werden, oder wird der Termin verschoben, bis der Lesezugriff auf die Objektbibliothek Luzern wiederhergestellt ist?', + inputs: [ + { + label: 'Kalendertermin', + value: 'Vor-Ort-Termin 20.05.2026, 11:00–12:00 Uhr, Pilatusstrasse 18, Luzern', + }, + { label: 'Teilnehmende', value: 'Rahel Studer (Vermarktung), Maison Verdier Sarl (Frau Céline Verdier)' }, + { + label: 'Verfügbare Quellen', + value: [ + 'Bewirtschaftungssystem — erreichbar, Stammdaten und Konditionen gelesen', + 'Öffentlicher Handelsregistereintrag — erreichbar', + 'SharePoint-Objektbibliothek «Liegenschaften Luzern» — nicht erreichbar', + ], + }, + { + label: 'Fehlgeschlagene Zugriffe', + value: '3 Versuche: 19.05.2026 22:14 Uhr, 20.05.2026 05:30 Uhr, 20.05.2026 06:55 Uhr', + }, + ], + result: [ + { label: 'Stand', value: 'Briefing zu 60 Prozent erstellt — Versand angehalten' }, + { + label: 'Vorhanden', + value: [ + 'Fläche 390 m², Mietzins CHF 295 pro m² und Jahr netto, verfügbar ab 01.09.2026', + 'Einordnung Maison Verdier Sarl: Sitz Lausanne, gegründet 2016, erste Filiale in der Zentralschweiz geplant', + 'Drei Verkaufsargumente, davon zwei ohne Beleg aus dem Objektdossier', + ], + }, + { + label: 'Fehlt', + value: [ + 'Objektdossier mit Ausbaustandard und Grundriss', + 'Konditionenblatt mit Nebenkostenaufstellung', + 'Übergabeprotokoll der Vormieterin', + ], + }, + { + label: 'Meldung', + value: + 'Störungsmeldung an den Systembetrieb abgesetzt am 20.05.2026, 06:00 Uhr — Referenz PO-SD-2026-0418, noch keine Rückmeldung', + }, + { label: 'Verbleibende Zeit', value: '2 Stunden 2 Minuten bis zum Termin (Stand 08:58 Uhr)' }, + ], + processingSteps: [ + { + id: 'awi-bruno-02-step-1', + label: 'Termin im Kalender erkannt', + detail: 'Vorbereitung planmässig um 09:00 Uhr gestartet, zwei Stunden vor dem Termin.', + }, + { + id: 'awi-bruno-02-step-2', + label: 'Objektbibliothek nicht erreichbar', + detail: 'Der Lesezugriff wurde dreimal zurückgewiesen — es lag kein vorübergehender Unterbruch vor.', + }, + { + id: 'awi-bruno-02-step-3', + label: 'Auf verfügbare Quellen ausgewichen', + detail: 'Stammdaten und Konditionen aus dem Bewirtschaftungssystem übernommen, öffentliche Quellen ausgewertet.', + }, + { + id: 'awi-bruno-02-step-4', + label: 'Fehlende Angaben markiert statt geschätzt', + detail: 'Ausbaustandard, Grundriss und Nebenkosten bleiben ausdrücklich als Lücke ausgewiesen.', + }, + { + id: 'awi-bruno-02-step-5', + label: 'Versand angehalten und Störung gemeldet', + detail: 'Ein unvollständiges Briefing geht nicht ohne Entscheid der verantwortlichen Person hinaus.', + }, + ], + sourceReferences: [ + { + id: 'awi-bruno-02-src-1', + label: 'Störungsmeldung Systembetrieb', + documentName: 'Stoerungsmeldung_PO-SD-2026-0418.txt', + locator: 'erfasst 20.05.2026, 06:00 Uhr', + }, + { + id: 'awi-bruno-02-src-2', + label: 'Stammdatenauszug Bewirtschaftungssystem', + documentName: 'Stammdaten_OBJ-2178_2026-05-20.pdf', + locator: 'Seite 1', + }, + ], + availableActions: [ + AgentWorkItemAction.DECIDE, + AgentWorkItemAction.EDIT, + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.OPEN_SOURCE, + ], + editableFields: [ + { + id: 'awi-bruno-02-field-gap-note', + label: 'Hinweis zu den Lücken im Briefing', + value: + 'Objektdossier, Grundriss und Nebenkostenaufstellung konnten heute nicht abgerufen werden. Bitte die Angaben zu Ausbaustandard und Nebenkosten vor Ort nicht zusichern.', + multiline: true, + helperText: 'Erscheint am Kopf des Briefings, sobald Sie den Versand freigeben.', + }, + { + id: 'awi-bruno-02-field-recipient', + label: 'Empfängerin des Briefings', + value: 'rahel.studer@property-on.ch', + }, + { + id: 'awi-bruno-02-field-audio', + label: 'Audiofassung mitsenden', + value: 'nein — bei unvollständigem Briefing standardmässig abgeschaltet', + helperText: 'Auf «ja» setzen, wenn die Audiofassung trotz der Lücken gewünscht ist.', + }, + ], + history: [ + { + id: 'awi-bruno-02-hist-1', + at: '2026-05-19T22:14:00.000Z', + actor: 'Bruno', + action: 'Erster fehlgeschlagener Zugriff protokolliert', + note: 'Nächtlicher Abgleich der Objektbibliothek Luzern zurückgewiesen.', + }, + { + id: 'awi-bruno-02-hist-2', + at: '2026-05-20T06:00:00.000Z', + actor: 'Bruno', + action: 'Störungsmeldung abgesetzt', + note: 'Referenz PO-SD-2026-0418 an den Systembetrieb.', + }, + { + id: 'awi-bruno-02-hist-3', + at: '2026-05-20T06:58:00.000Z', + actor: 'Bruno', + action: 'Entscheid angefordert', + note: 'Briefing unvollständig, Versand angehalten.', + }, + ], + }, + + // ── Reto — erledigt ──────────────────────────────────────────────────────── + { + id: 'awi-reto-01', + agentId: AGENT_IDS.RETO, + kind: AgentWorkItemKind.RECAP, + area: AgentDomainArea.VIEWINGS, + title: 'Recap zur Besichtigung Technoparkstrasse 7 mit einem Klick freigegeben', + summary: + 'Aus dem Sprachmemo nach der Besichtigung in Winterthur entstanden Protokoll, Gesprächsnotiz und drei Folgeaufgaben. Die Freigabe erfolgte 32 Minuten nach der Aufnahme mit einem Klick.', + objectId: 'OBJ-2288', + objectLabel: 'Technoparkstrasse 7, 8406 Winterthur — Werk- und Bürofläche, Teilfläche 640 m²', + sourceChannel: AgentChannelType.WHATSAPP, + targetChannel: AgentChannelType.WORKSPACE_CHAT, + createdAt: '2026-05-12T16:48:00.000Z', + completedAt: '2026-05-12T17:20:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.APPROVED, + requiresDecision: false, + inputs: [ + { label: 'Sprachmemo', value: 'WhatsApp-Aufnahme vom 12.05.2026, 16:48 Uhr, Dauer 4:07 Minuten' }, + { label: 'Aufgenommen von', value: 'Marco Steinegger, Vermarktung Winterthur' }, + { + label: 'Termin-Kontext aus dem Kalender', + value: 'Besichtigung 12.05.2026, 15:30–16:30 Uhr, mit Herrn Luca Cassano, Cassano Handels GmbH', + }, + ], + result: [ + { + label: 'Positives Feedback', + value: [ + 'Rampe und Warenanlieferung passen zum Betrieb', + 'Deckenhöhe von 4,20 m im hinteren Teil ausdrücklich gelobt', + 'Lage zur Autobahnauffahrt Winterthur-Ohringen als Vorteil genannt', + ], + }, + { + label: 'Einwände', + value: [ + 'Bürofläche im Obergeschoss wirkt mit 180 m² zu gross', + 'Mietzins liegt rund 10 Prozent über dem bisherigen Standort', + ], + }, + { + label: 'Zusagen', + value: [ + 'Property On reicht bis 15.05.2026 den Grundriss mit Abtrennungsvariante nach', + 'Cassano Handels GmbH meldet bis 22.05.2026 zurück, ob eine Zweitbesichtigung erwünscht ist', + ], + }, + { + label: 'Folgeaufgaben', + value: [ + 'Grundriss mit Abtrennungsvariante versenden — Marco Steinegger, bis 15.05.2026', + 'Mietzins für Teilfläche intern prüfen — Nadine Brunner, bis 19.05.2026', + 'Zweitbesichtigung vormerken — Lea, ab 22.05.2026', + ], + }, + { + label: 'Beobachtung für die Bewirtschaftung', + value: 'Sektionaltor Ost schliesst nur mit deutlichem Nachdruck — Hinweis an die Bewirtschaftung Winterthur', + }, + { label: 'Abgelegt', value: 'Besichtigungsprotokoll im Objektdossier, Gesprächsnotiz im CRM, drei Aufgaben eröffnet' }, + ], + processingSteps: [ + { + id: 'awi-reto-01-step-1', + label: 'Sprachmemo entgegengenommen und zugeordnet', + detail: 'Über Zeitpunkt und Objektbezug der Besichtigung vom 12.05.2026 zugewiesen.', + }, + { + id: 'awi-reto-01-step-2', + label: 'Gesprochenes in Text übertragen und bereinigt', + detail: 'Füllwörter und Wiederholungen entfernt, inhaltliche Aussagen unverändert übernommen.', + }, + { + id: 'awi-reto-01-step-3', + label: 'In Feedback, Einwände, Zusagen und offene Punkte gegliedert', + detail: 'Jeder Punkt behält den Bezug zur Aussage im Memo.', + }, + { + id: 'awi-reto-01-step-4', + label: 'Protokoll, Gesprächsnotiz und Aufgaben als Vorschlag zusammengestellt', + detail: 'Drei Folgeaufgaben mit Frist und zuständiger Person abgeleitet.', + }, + { + id: 'awi-reto-01-step-5', + label: 'Nach der Freigabe in die Zielsysteme übertragen', + detail: 'Übertragung erst nach der Bestätigung von Marco Steinegger um 17:20 Uhr.', + }, + ], + sourceReferences: [ + { + id: 'awi-reto-01-src-1', + label: 'Sprachmemo Besichtigung OBJ-2288', + documentName: 'Sprachmemo_OBJ-2288_2026-05-12_1648.m4a', + locator: 'Gesamtaufnahme 4:07 Minuten', + }, + { + id: 'awi-reto-01-src-2', + label: 'Besichtigungsprotokoll im Objektdossier', + documentName: 'Besichtigungsprotokoll_OBJ-2288_2026-05-12.pdf', + locator: 'Seiten 1–2', + }, + ], + messageThread: [ + { + id: 'awi-reto-01-msg-1', + at: '2026-05-12T16:48:00.000Z', + from: 'Marco Steinegger', + to: 'Reto Diktat (WhatsApp)', + channel: AgentChannelType.WHATSAPP, + body: '[Sprachmemo, 4:07 Minuten] Besichtigung Technoparkstrasse mit Herrn Cassano ist gelaufen. Ich fasse kurz zusammen, was gesagt wurde.', + }, + { + id: 'awi-reto-01-msg-2', + at: '2026-05-12T17:04:00.000Z', + from: 'Reto — Recap Vermarktung', + to: 'Marco Steinegger', + channel: AgentChannelType.WHATSAPP, + body: `Guten Abend Marco + +Das Protokoll zur Besichtigung an der Technoparkstrasse 7 liegt als Vorschlag bereit: 3 Punkte positives Feedback, 2 Einwände, 2 Zusagen und 3 Folgeaufgaben. + +Eine Rückfrage: Du hast von «rund zehn Prozent teurer» gesprochen. Beziehst du das auf den Nettomietzins oder auf die Gesamtkosten inklusive Nebenkosten? + +Freundliche Grüsse +Reto — Recap-Agent`, + }, + { + id: 'awi-reto-01-msg-3', + at: '2026-05-12T17:19:00.000Z', + from: 'Marco Steinegger', + to: 'Reto — Recap Vermarktung', + channel: AgentChannelType.WHATSAPP, + body: 'Auf den Nettomietzins. Sonst passt alles — bitte so ablegen und die drei Aufgaben eröffnen. Freigabe erteilt.', + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-reto-01-hist-1', + at: '2026-05-12T17:04:00.000Z', + actor: 'Reto', + action: 'Vorschlag vorgelegt', + note: 'Protokoll, Gesprächsnotiz und drei Folgeaufgaben zur 1-Klick-Bestätigung.', + }, + { + id: 'awi-reto-01-hist-2', + at: '2026-05-12T17:20:00.000Z', + actor: 'Marco Steinegger', + action: 'Freigabe erteilt', + note: 'Bezugsgrösse Nettomietzins präzisiert, danach mit einem Klick bestätigt.', + }, + ], + }, + + // ── Reto — pendent: Verarbeitungsproblem ─────────────────────────────────── + { + id: 'awi-reto-02', + agentId: AGENT_IDS.RETO, + kind: AgentWorkItemKind.RECAP, + area: AgentDomainArea.VIEWINGS, + title: 'Sprachmemo abgebrochen und teilweise unverständlich — Recap Baarerstrasse 82', + summary: + 'Die Aufnahme bricht nach 3:12 Minuten mitten im Satz ab, drei Passagen sind wegen Baulärm nicht verständlich. Das Protokoll ist damit lückenhaft und wird nicht ohne Entscheid abgelegt.', + objectId: 'OBJ-2094', + objectLabel: 'Baarerstrasse 82, 6300 Zug — Bürofläche 3. Obergeschoss, 720 m²', + sourceChannel: AgentChannelType.WHATSAPP, + targetChannel: AgentChannelType.WORKSPACE_CHAT, + createdAt: '2026-05-19T17:24:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Verarbeitungsproblem: Die Aufnahme endet nach 3:12 Minuten mitten im Satz, drei Passagen von zusammen 47 Sekunden sind wegen Baulärm nicht verständlich. Unsichere Stellen werden markiert und nie erraten.', + decisionQuestion: + 'Soll das Protokoll mit den drei als unklar markierten Passagen abgelegt werden, oder reichen Sie ein neues Sprachmemo zu den fehlenden Teilen nach?', + inputs: [ + { label: 'Sprachmemo', value: 'WhatsApp-Aufnahme vom 19.05.2026, 17:24 Uhr, Dauer 3:12 Minuten (abgebrochen)' }, + { label: 'Aufgenommen von', value: 'Rahel Studer, Vermarktung Zürich' }, + { + label: 'Termin-Kontext aus dem Kalender', + value: 'Besichtigung 19.05.2026, 16:00–17:00 Uhr, mit Lorzenpark Consulting AG (Herr Dominik Aebi)', + }, + { label: 'Störgeräusch', value: 'Baustelle Baarerstrasse — Belagsarbeiten, gemäss Aufnahme durchgehend hörbar' }, + ], + result: [ + { label: 'Stand', value: 'Protokollentwurf zu rund 70 Prozent — Ablage angehalten' }, + { + label: 'Gesichert erfasst', + value: [ + 'Positives Feedback zu Grundriss und Tageslicht im 3. Obergeschoss', + 'Einwand: nur 4 statt der gewünschten 8 Parkplätze verfügbar', + 'Interesse an einem Mietbeginn per 01.01.2027', + ], + }, + { + label: 'Unklare Passage 1', + value: 'Minute 1:12 bis 1:31 — Aussage zum Mietzins, Zahl nicht verständlich', + }, + { + label: 'Unklare Passage 2', + value: 'Minute 2:03 bis 2:18 — Name einer zweiten Ansprechperson, nicht eindeutig', + }, + { + label: 'Unklare Passage 3', + value: 'Minute 2:55 bis 3:08 — Zusage zu einer Frist, Datum nicht verständlich', + }, + { + label: 'Nicht abgeleitet', + value: 'Keine Folgeaufgaben eröffnet — die Fristzusage aus Passage 3 fehlt und wird nicht geschätzt.', + }, + ], + processingSteps: [ + { + id: 'awi-reto-02-step-1', + label: 'Sprachmemo entgegengenommen und zugeordnet', + detail: 'Über Zeitpunkt und Objektbezug der Besichtigung vom 19.05.2026 zugewiesen.', + }, + { + id: 'awi-reto-02-step-2', + label: 'Abbruch der Aufnahme festgestellt', + detail: 'Die Datei endet nach 3:12 Minuten mitten im Satz — kein Abschluss des Gedankens erkennbar.', + }, + { + id: 'awi-reto-02-step-3', + label: 'Drei Passagen als unverständlich markiert', + detail: 'Zusammen 47 Sekunden mit durchgehendem Baulärm — Inhalte werden nicht erraten.', + }, + { + id: 'awi-reto-02-step-4', + label: 'Ablage angehalten und Rückfrage gestellt', + detail: 'Ohne Entscheid gelangt kein lückenhaftes Protokoll ins Dossier und keine Notiz ins CRM.', + }, + ], + sourceReferences: [ + { + id: 'awi-reto-02-src-1', + label: 'Sprachmemo Besichtigung OBJ-2094', + documentName: 'Sprachmemo_OBJ-2094_2026-05-19_1724.m4a', + locator: 'Aufnahme 3:12 Minuten, unklar bei 1:12, 2:03 und 2:55', + }, + { + id: 'awi-reto-02-src-2', + label: 'Kalendereintrag Besichtigung', + documentName: 'Kalender_Vermarktung_2026-05-19.ics', + locator: 'Termin 16:00–17:00 Uhr', + }, + ], + availableActions: [ + AgentWorkItemAction.EDIT, + AgentWorkItemAction.ANSWER_QUERY, + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.REJECT, + ], + editableFields: [ + { + id: 'awi-reto-02-field-rent', + label: 'Passage 1 — genannter Mietzins', + value: '[unklar]', + helperText: 'Aufnahme 1:12 bis 1:31. Bitte den genannten Betrag ergänzen oder das Feld leer lassen.', + }, + { + id: 'awi-reto-02-field-contact', + label: 'Passage 2 — zweite Ansprechperson', + value: '[unklar]', + helperText: 'Aufnahme 2:03 bis 2:18. Name und Funktion beim Interessenten.', + }, + { + id: 'awi-reto-02-field-deadline', + label: 'Passage 3 — zugesagte Frist', + value: '[unklar]', + helperText: 'Aufnahme 2:55 bis 3:08. Aus dieser Angabe entsteht die Folgeaufgabe.', + }, + { + id: 'awi-reto-02-field-summary', + label: 'Zusammenfassung des Protokolls', + value: + 'Die Interessentin zeigte sich vom Grundriss und vom Tageslicht im 3. Obergeschoss überzeugt. Als Einwand wurde die Parkplatzsituation genannt: verfügbar sind 4 statt der gewünschten 8 Plätze. Ein Mietbeginn per 1. Januar 2027 wäre passend.', + multiline: true, + }, + ], + history: [ + { + id: 'awi-reto-02-hist-1', + at: '2026-05-19T17:26:00.000Z', + actor: 'Reto', + action: 'Verarbeitungsproblem gemeldet', + note: 'Aufnahme abgebrochen, drei Passagen unverständlich — Ablage angehalten.', + }, + { + id: 'awi-reto-02-hist-2', + at: '2026-05-20T08:05:00.000Z', + actor: 'Reto', + action: 'Erinnerung an die Rückfrage', + note: 'Seit über 14 Stunden ohne Rückmeldung — Termin ist noch nicht protokolliert.', + }, + ], + }, + + // ── Livia — erledigt ─────────────────────────────────────────────────────── + { + id: 'awi-livia-01', + agentId: AGENT_IDS.LIVIA, + kind: AgentWorkItemKind.LOCATION_MODULE, + area: AgentDomainArea.LOCATION, + title: 'Lagebeschrieb erstellt — Hardturmstrasse 118, Zürich Kreis 5', + summary: + 'Zum neuen Vermarktungsauftrag entstanden ein Kurztext für Inserat und Portal sowie ein Langtext für das Exposé, jeweils mit Quellenverzeichnis und Abrufdatum zu jeder Aussage.', + objectId: 'OBJ-2365', + objectLabel: 'Hardturmstrasse 118, 8005 Zürich — Bürofläche 640 m², Zürich Kreis 5', + sourceChannel: AgentChannelType.WORKSPACE_CHAT, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-04-09T09:15:00.000Z', + completedAt: '2026-04-09T11:40:00.000Z', + priority: AgentWorkItemPriority.LOW, + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + inputs: [ + { label: 'Objektadresse aus dem Vermarktungsauftrag', value: 'Hardturmstrasse 118, 8005 Zürich' }, + { label: 'Quartierzuordnung', value: 'Zürich Kreis 5 (Escher Wyss), geprüft gegen die amtlichen Adressdaten' }, + { label: 'Untersuchungsradius Mikrolage', value: '500 Meter Umkreis, Erdgeschossnutzungen und Erreichbarkeit' }, + { label: 'Auftraggebende Person', value: 'Nadine Brunner, Vermarktung Zürich' }, + ], + result: [ + { + label: 'Kurztext für Inserat und Portal', + value: + 'Die Fläche liegt im Kreis 5 zwischen Escher-Wyss-Platz und Hardbrücke, in einem Quartier mit hohem Anteil an Dienstleistung, Gastronomie und Kreativwirtschaft. Die Haltestelle Escher-Wyss-Platz ist in vier Gehminuten erreichbar, der Bahnhof Hardbrücke in acht Minuten.', + }, + { + label: 'Langtext für das Exposé', + value: + 'Dreiteiliger Text mit Mikrolage (Nachbarschaft, Erdgeschossnutzungen, Publikumsfrequenz, Parkierung), Makrolage (Wirtschaftsstruktur Stadt Zürich, Beschäftigtenentwicklung Kreis 5, Steuerlage) und Umfeldentwicklung — insgesamt 3 940 Zeichen.', + }, + { + label: 'Erreichbarkeit', + value: [ + 'Tram- und Bushaltestelle Escher-Wyss-Platz: 300 Meter, 4 Gehminuten', + 'Bahnhof Zürich Hardbrücke: 650 Meter, 8 Gehminuten', + 'Autobahnanschluss Zürich-Hardturm: 1,4 Kilometer', + 'Veloweg entlang der Hardturmstrasse, durchgehend abgetrennt', + ], + }, + { + label: 'Nahversorgung', + value: [ + 'Einkaufen: zwei Supermärkte innerhalb von 400 Metern', + 'Verpflegung: 14 Gastronomiebetriebe im Umkreis von 300 Metern', + 'Kinderbetreuung: zwei Kindertagesstätten innerhalb von 600 Metern', + ], + }, + { + label: 'Umfeldentwicklung', + value: + 'Arealentwicklung Hardturm-Areal in Etappen bis 2029; Zonenänderung rechtskräftig seit 2024 (Quelle: amtliche Publikation, abgerufen 09.04.2026).', + }, + { label: 'Quellenverzeichnis', value: '11 Quellen mit Abrufdatum, je Aussage zugeordnet' }, + ], + processingSteps: [ + { + id: 'awi-livia-01-step-1', + label: 'Adresse aufgenommen und dem Quartier zugeordnet', + detail: 'Schreibweise gegen die amtlichen Adressdaten geprüft, Zuordnung Zürich Kreis 5.', + }, + { + id: 'awi-livia-01-step-2', + label: 'Mikrolage im Umkreis von 500 Metern abgegangen', + detail: 'Nachbarschaft, Erdgeschossnutzungen, Immissionslage, Publikumsfrequenz und Parkierung erfasst.', + }, + { + id: 'awi-livia-01-step-3', + label: 'Makrolage eingeordnet', + detail: 'Wirtschaftsstruktur, Beschäftigtenentwicklung und Steuerlage der Stadt Zürich ausgewertet.', + }, + { + id: 'awi-livia-01-step-4', + label: 'Amtliche Publikationen zum Umfeld gesichtet', + detail: 'Arealentwicklung und Zonenänderung als für das Objekt relevant eingestuft.', + }, + { + id: 'awi-livia-01-step-5', + label: 'Kurz- und Langtext geschrieben, Quellen angehängt', + detail: 'Je Aussage Quelle und Abrufdatum hinterlegt, Ergebnis am Objekt abgelegt.', + }, + ], + sourceReferences: [ + { + id: 'awi-livia-01-src-1', + label: 'Amtliche Publikation zur Zonenänderung Hardturm', + documentName: 'Amtliche-Publikation_Zonenaenderung-Hardturm_2024-11-08.pdf', + locator: 'Seite 3, abgerufen 09.04.2026', + }, + { + id: 'awi-livia-01-src-2', + label: 'Fahrplanauskunft öffentlicher Verkehr', + documentName: 'Erreichbarkeitsauswertung_OBJ-2365_2026-04-09.pdf', + locator: 'Tabelle «Haltestellen im Umkreis», abgerufen 09.04.2026', + }, + { + id: 'awi-livia-01-src-3', + label: 'Quellenverzeichnis zum Lagebeschrieb', + documentName: 'Lagebeschrieb_OBJ-2365_Quellen_2026-04-09.pdf', + locator: 'Seiten 1–2, 11 Einträge', + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-livia-01-hist-1', + at: '2026-04-09T09:15:00.000Z', + actor: 'Livia', + action: 'Vermarktungsauftrag übernommen', + note: 'Adresse geprüft und dem Quartier Zürich Kreis 5 zugeordnet.', + }, + { + id: 'awi-livia-01-hist-2', + at: '2026-04-09T11:40:00.000Z', + actor: 'Livia', + action: 'Lagebeschrieb abgelegt und zugestellt', + note: 'Kurz- und Langtext mit 11 Quellen an die Vermarktung Zürich.', + }, + { + id: 'awi-livia-01-hist-3', + at: '2026-04-14T08:05:00.000Z', + actor: 'Nadine Brunner', + action: 'Text für das Inserat übernommen', + note: 'Kurztext unverändert im Portalauftritt veröffentlicht.', + }, + ], + }, + + // ── Livia — pendent: Review eines unsicheren Ergebnisses ─────────────────── + { + id: 'awi-livia-02', + agentId: AGENT_IDS.LIVIA, + kind: AgentWorkItemKind.LOCATION_MODULE, + area: AgentDomainArea.LOCATION, + title: 'Unsichere Aussage zum Entwicklungsgebiet Wankdorf — Review erbeten', + summary: + 'Für den Langtext zum Objekt in Bern liegen zwei Quellen mit unterschiedlichen Angaben zur Inbetriebnahme der neuen Haltestelle vor. Die Aussage bleibt aus dem Text ausgeklammert, bis entschieden ist, welche Angabe gilt.', + objectId: 'OBJ-2733', + objectLabel: 'Wankdorffeldstrasse 12, 3014 Bern — Bürofläche 1. bis 3. Obergeschoss, 1 150 m²', + sourceChannel: AgentChannelType.WORKSPACE_CHAT, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-07T07:45:00.000Z', + priority: AgentWorkItemPriority.MEDIUM, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Review eines unsicheren Ergebnisses: Zwei belegte Quellen nennen unterschiedliche Zeitpunkte für die Inbetriebnahme der Haltestelle Wankdorffeld. Eine Aussage ohne eindeutigen Beleg geht nicht in den Exposétext.', + decisionQuestion: + 'Welche Angabe soll in den Langtext übernommen werden — «ab Dezember 2027» gemäss amtlicher Publikation oder «ab Sommer 2028» gemäss Mitteilung der Standortförderung — oder soll der Absatz zur Haltestelle vorerst entfallen?', + inputs: [ + { label: 'Objektadresse aus dem Vermarktungsauftrag', value: 'Wankdorffeldstrasse 12, 3014 Bern' }, + { label: 'Quartierzuordnung', value: 'Bern Wankdorf, Entwicklungsgebiet Wankdorf-Nord' }, + { + label: 'Widersprüchliche Quellen', + value: [ + 'Amtliche Publikation vom 12.03.2026 — Inbetriebnahme Dezember 2027', + 'Mitteilung der Standortförderung vom 28.04.2026 — Inbetriebnahme Sommer 2028', + ], + }, + { label: 'Auftraggebende Person', value: 'Nadine Brunner, Vermarktung Zürich (Mandat Bern)' }, + ], + result: [ + { label: 'Stand', value: 'Kurztext fertig, Langtext zu 90 Prozent — ein Absatz ist ausgeklammert' }, + { + label: 'Fertiger Kurztext', + value: + 'Das Objekt liegt im Entwicklungsgebiet Wankdorf, unmittelbar an der Achse zwischen Bahnhof Wankdorf und Autobahnanschluss Bern-Wankdorf. Das Umfeld ist geprägt von Dienstleistung, öffentlichen Institutionen und Verkaufsflächen.', + }, + { + label: 'Ausgeklammerter Absatz', + value: + 'Die geplante Haltestelle Wankdorffeld liegt rund 200 Meter vom Objekt entfernt. Die Inbetriebnahme ist für [Zeitpunkt offen] vorgesehen.', + }, + { + label: 'Warum die Angabe zählt', + value: + 'Die Wegzeit zum öffentlichen Verkehr sinkt mit der neuen Haltestelle von 11 auf 3 Gehminuten — die Aussage ist für Interessenten mit Publikumsverkehr entscheidend.', + }, + { label: 'Übrige Quellen', value: '9 Quellen mit Abrufdatum, alle eindeutig und im Text belegt' }, + ], + processingSteps: [ + { + id: 'awi-livia-02-step-1', + label: 'Adresse aufgenommen und dem Quartier zugeordnet', + detail: 'Zuordnung Bern Wankdorf, Entwicklungsgebiet Wankdorf-Nord.', + }, + { + id: 'awi-livia-02-step-2', + label: 'Mikro- und Makrolage beschrieben', + detail: 'Nachbarschaft, Erreichbarkeit, Nahversorgung und Wirtschaftsraum Bern erfasst.', + }, + { + id: 'awi-livia-02-step-3', + label: 'Amtliche Publikationen zum Umfeld gesichtet', + detail: 'Beim Verkehrsvorhaben zwei Quellen mit unterschiedlichen Zeitangaben gefunden.', + }, + { + id: 'awi-livia-02-step-4', + label: 'Aussage ausgeklammert statt gemittelt', + detail: 'Der Absatz bleibt offen, bis die massgebende Quelle bestimmt ist.', + }, + { + id: 'awi-livia-02-step-5', + label: 'Review vorgelegt', + detail: 'Beide Belege mit Fundstelle und Abrufdatum zur Entscheidung beigelegt.', + }, + ], + sourceReferences: [ + { + id: 'awi-livia-02-src-1', + label: 'Amtliche Publikation Verkehrsvorhaben Wankdorffeld', + documentName: 'Amtliche-Publikation_Haltestelle-Wankdorffeld_2026-03-12.pdf', + locator: 'Seite 2, Abschnitt «Terminprogramm»', + quote: + '«Die Inbetriebnahme der Haltestelle Wankdorffeld ist auf den Fahrplanwechsel im Dezember 2027 vorgesehen.»', + }, + { + id: 'awi-livia-02-src-2', + label: 'Mitteilung der Standortförderung Bern', + documentName: 'Standortfoerderung-Bern_Mitteilung_2026-04-28.pdf', + locator: 'Seite 1, abgerufen 07.05.2026', + quote: + '«Wegen der Anpassung der Gleisgeometrie verschiebt sich die Eröffnung der Haltestelle Wankdorffeld auf Sommer 2028.»', + }, + ], + availableActions: [ + AgentWorkItemAction.DECIDE, + AgentWorkItemAction.EDIT, + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.OPEN_SOURCE, + ], + editableFields: [ + { + id: 'awi-livia-02-field-date', + label: 'Zu verwendender Zeitpunkt', + value: 'Dezember 2027', + helperText: 'Alternativ «Sommer 2028» oder leer lassen, damit der Absatz entfällt.', + }, + { + id: 'awi-livia-02-field-paragraph', + label: 'Absatz zur Haltestelle', + value: + 'Die geplante Haltestelle Wankdorffeld liegt rund 200 Meter vom Objekt entfernt. Die Inbetriebnahme ist auf Dezember 2027 vorgesehen; die Wegzeit zum öffentlichen Verkehr verkürzt sich damit von 11 auf 3 Gehminuten.', + multiline: true, + }, + { + id: 'awi-livia-02-field-source', + label: 'Anzugebende Quelle', + value: 'Amtliche Publikation Verkehrsvorhaben Wankdorffeld, 12.03.2026, abgerufen 07.05.2026', + helperText: 'Erscheint im Quellenverzeichnis des Exposés.', + }, + ], + history: [ + { + id: 'awi-livia-02-hist-1', + at: '2026-05-07T07:45:00.000Z', + actor: 'Livia', + action: 'Widerspruch erkannt und Review vorgelegt', + note: 'Zwei belegte Quellen mit unterschiedlichen Zeitangaben zur Haltestelle.', + }, + { + id: 'awi-livia-02-hist-2', + at: '2026-05-15T09:20:00.000Z', + actor: 'Livia', + action: 'Quellen erneut geprüft', + note: 'Quartalsprüfung: keine neuere amtliche Publikation vorhanden, Widerspruch besteht weiter.', + }, + ], + }, + + // ── Nora — erledigt ──────────────────────────────────────────────────────── + { + id: 'awi-nora-01', + agentId: AGENT_IDS.NORA, + kind: AgentWorkItemKind.MARKET_SIGNAL, + area: AgentDomainArea.MARKET, + title: 'Marktsignal verifiziert und in die Wochen-Mail aufgenommen — Filialschliessung Basel', + summary: + 'Die angekündigte Schliessung von zwei Verkaufsstandorten der Cassano Handels GmbH in Basel wurde über drei Quellen bestätigt, mit 88 Prozent Konfidenz eingestuft und nach Freigabe in der Wochen-Mail versandt.', + objectId: 'OBJ-2617', + objectLabel: 'Region Basel — Segment Retail, Umfeld Aeschenvorstadt 26', + sourceChannel: AgentChannelType.WORKSPACE_CHAT, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-03-10T06:20:00.000Z', + completedAt: '2026-03-10T09:05:00.000Z', + priority: AgentWorkItemPriority.HIGH, + status: AgentWorkItemStatus.APPROVED, + requiresDecision: false, + inputs: [ + { + label: 'Ausgangsmeldung', + value: 'Amtliche Meldung vom 09.03.2026 zur Auflösung von zwei Zweigniederlassungen der Cassano Handels GmbH', + }, + { label: 'Beobachtete Region und Segment', value: 'Basel — Retail' }, + { + label: 'Herangezogene Quellen', + value: [ + 'Amtliche Meldung, Meldungsnummer SH-2026-03-09-114', + 'Handelsregisterindex — Abgleich von Firmenname, Rechtsform und Sitz', + 'Medienbericht einer Regionalzeitung vom 10.03.2026', + ], + }, + ], + result: [ + { label: 'Signalart', value: 'Filialschliessung' }, + { label: 'Konfidenz', value: '88 Prozent — über der Mindestkonfidenz von 70 Prozent' }, + { label: 'Bestätigende Quellen', value: '3 von 3 stimmen überein' }, + { + label: 'Regionsbezug', + value: 'Basel — betroffen sind zwei Verkaufsflächen von 240 m² und 410 m² an der Freien Strasse und am Claraplatz', + }, + { + label: 'Objektbezug', + value: + 'Kein direkter Objektbezug im Portfolio; das Signal ist für die Angebotssituation im Umfeld von OBJ-2617 relevant.', + }, + { + label: 'Weiterverwendung', + value: + 'Aufgenommen in die Wochen-Mail vom 10.03.2026, freigegeben durch Sandro Kälin um 09:02 Uhr, versandt um 09:05 Uhr an drei Empfängeradressen', + }, + ], + processingSteps: [ + { + id: 'awi-nora-01-step-1', + label: 'Freigegebene Quellen gesichtet', + detail: 'Morgenlauf 06:00 Uhr über die eingestellten Regionen und Segmente.', + }, + { + id: 'awi-nora-01-step-2', + label: 'Meldung als Filialschliessung eingeordnet', + detail: 'Die Textstelle zur Auflösung der Zweigniederlassungen stützt die Einordnung.', + }, + { + id: 'awi-nora-01-step-3', + label: 'Konfidenz vergeben', + detail: '88 Prozent — amtliche Quelle, tagesaktuell, in zwei weiteren Quellen bestätigt.', + }, + { + id: 'awi-nora-01-step-4', + label: 'Region zugeordnet und Entwurf vorgelegt', + detail: 'Aufnahme in den Entwurf der Wochen-Mail, Versand bis zur Freigabe angehalten.', + }, + { + id: 'awi-nora-01-step-5', + label: 'Nach der Freigabe versandt', + detail: 'Wochen-Mail um 09:05 Uhr an Marktbeobachtung, Akquisition Zürich und Bewirtschaftung Winterthur.', + }, + ], + sourceReferences: [ + { + id: 'awi-nora-01-src-1', + label: 'Amtliche Meldung zur Auflösung der Zweigniederlassungen', + documentName: 'Amtliche-Meldung_SH-2026-03-09-114.pdf', + locator: 'Meldung vom 09.03.2026, abgerufen 10.03.2026', + quote: + '«Die Zweigniederlassungen Basel-Freie Strasse und Basel-Claraplatz der Cassano Handels GmbH werden per 30. Juni 2026 aufgehoben.»', + }, + { + id: 'awi-nora-01-src-2', + label: 'Handelsregisterindex — Cassano Handels GmbH', + documentName: 'Handelsregisterindex_Cassano-Handels-GmbH.pdf', + locator: 'abgerufen 10.03.2026', + }, + { + id: 'awi-nora-01-src-3', + label: 'Medienbericht Regionalzeitung', + documentName: 'Medienbericht_Basel-Retail_2026-03-10.pdf', + locator: 'Wirtschaftsteil, abgerufen 10.03.2026', + }, + ], + availableActions: [AgentWorkItemAction.OPEN_SOURCE, AgentWorkItemAction.MARK_DONE], + history: [ + { + id: 'awi-nora-01-hist-1', + at: '2026-03-10T06:20:00.000Z', + actor: 'Nora', + action: 'Signal erfasst und eingeordnet', + note: 'Filialschliessung Basel, Konfidenz 88 Prozent.', + }, + { + id: 'awi-nora-01-hist-2', + at: '2026-03-10T09:02:00.000Z', + actor: 'Sandro Kälin', + action: 'Wochen-Mail freigegeben', + note: 'Signal unverändert übernommen.', + }, + { + id: 'awi-nora-01-hist-3', + at: '2026-03-10T09:05:00.000Z', + actor: 'Nora', + action: 'Wochen-Mail versandt', + note: 'Drei Empfängeradressen gemäss Einstellung.', + }, + ], + }, + + // ── Nora — pendent: Review eines unsicheren Ergebnisses ──────────────────── + { + id: 'awi-nora-02', + agentId: AGENT_IDS.NORA, + kind: AgentWorkItemKind.MARKET_SIGNAL, + area: AgentDomainArea.MARKET, + title: 'Signal in der Review-Queue — mögliches Umzugssignal Lorzenpark Consulting AG, Zug', + summary: + 'Ein Hinweis auf eine Standortsuche in Zug erreicht nur 48 Prozent Konfidenz. Die zweite Quelle bestätigt die Meldung nicht, deshalb bleibt das Signal aus Wochen-Mail und Lead-Liste ausgeschlossen.', + objectLabel: 'Zuordnung offen — Region Zug, Segment Büro', + sourceChannel: AgentChannelType.WORKSPACE_CHAT, + targetChannel: AgentChannelType.EMAIL, + createdAt: '2026-05-17T06:30:00.000Z', + priority: AgentWorkItemPriority.LOW, + status: AgentWorkItemStatus.PENDING, + requiresDecision: true, + escalationReason: + 'Review eines unsicheren Ergebnisses: Die Konfidenz von 48 Prozent liegt unter der Review-Schwelle von 55 Prozent. Der Handelsregisterindex weist unverändert den bisherigen Sitz aus; eine zweite Quelle bestätigt die Standortsuche nicht.', + decisionQuestion: + 'Soll das Signal als verifiziert freigegeben und als Lead an die Akquisition übergeben werden, oder bleibt es bis zu einer zweiten Bestätigung in der Review-Queue?', + inputs: [ + { + label: 'Ausgangsmeldung', + value: + 'Beitrag auf der Firmenwebsite der Lorzenpark Consulting AG vom 15.05.2026: «Wir wachsen — und suchen ab 2027 grössere Räumlichkeiten im Raum Zug.»', + }, + { label: 'Beobachtete Region und Segment', value: 'Zug — Büro' }, + { + label: 'Gegenprüfung', + value: [ + 'Handelsregisterindex: Sitz unverändert Baarerstrasse 55, 6300 Zug, letzte Mutation 14.11.2024', + 'Amtliche Meldungen: keine Eintragung zu einer Sitzverlegung', + 'Medienberichte: keine Fundstelle', + ], + }, + { label: 'Eingestellte Schwellen', value: 'Mindestkonfidenz 70 Prozent, Review-Schwelle 55 Prozent' }, + ], + result: [ + { label: 'Stand', value: 'In der Review-Queue — nicht in Wochen-Mail und nicht in der Lead-Liste' }, + { label: 'Vorgeschlagene Signalart', value: 'Expansion (alternativ Umzug)' }, + { label: 'Konfidenz', value: '48 Prozent — 7 Punkte unter der Review-Schwelle' }, + { + label: 'Was gegen eine Freigabe spricht', + value: [ + 'Nur eine Quelle, und diese stammt vom Unternehmen selbst', + 'Kein Zeitpunkt und keine Flächengrösse genannt', + 'Handelsregisterindex zeigt keine Bewegung', + ], + }, + { + label: 'Was für eine Freigabe spricht', + value: + 'Die Firma hat die Mitarbeitendenzahl auf der eigenen Website innerhalb von zwölf Monaten von 34 auf 51 erhöht; der Zeithorizont 2027 passt zu einer Standortsuche.', + }, + { label: 'Offen seit', value: '3 Tage — ältester Eintrag in der Review-Queue' }, + ], + processingSteps: [ + { + id: 'awi-nora-02-step-1', + label: 'Meldung in den freigegebenen Quellen gefunden', + detail: 'Beitrag auf der öffentlich zugänglichen Firmenwebsite, erfasst mit Fundort und Datum.', + }, + { + id: 'awi-nora-02-step-2', + label: 'Firma und Sitz abgeglichen', + detail: 'Handelsregisterindex bestätigt Firmenname und Rechtsform, weist aber keine Sitzänderung aus.', + }, + { + id: 'awi-nora-02-step-3', + label: 'Zweite Quelle gesucht — keine gefunden', + detail: 'Weder amtliche Meldungen noch Medienberichte bestätigen die Standortsuche.', + }, + { + id: 'awi-nora-02-step-4', + label: 'Konfidenz auf 48 Prozent gesetzt', + detail: 'Einzelquelle vom Unternehmen selbst, ohne Zeitpunkt und ohne Flächenangabe.', + }, + { + id: 'awi-nora-02-step-5', + label: 'In die Review-Queue gelegt', + detail: 'Unterhalb der Review-Schwelle — weder Wochen-Mail noch Lead-Liste, bis jemand geprüft hat.', + }, + ], + sourceReferences: [ + { + id: 'awi-nora-02-src-1', + label: 'Beitrag auf der Firmenwebsite', + documentName: 'Quellenauszug_Lorzenpark-Consulting_2026-05-15.pdf', + locator: 'Seite 1, abgerufen 17.05.2026', + quote: '«Wir wachsen — und suchen ab 2027 grössere Räumlichkeiten im Raum Zug.»', + }, + { + id: 'awi-nora-02-src-2', + label: 'Handelsregisterindex — Lorzenpark Consulting AG', + documentName: 'Handelsregisterindex_Lorzenpark-Consulting-AG.pdf', + locator: 'abgerufen 17.05.2026, letzte Mutation 14.11.2024', + }, + ], + availableActions: [ + AgentWorkItemAction.DECIDE, + AgentWorkItemAction.EDIT, + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.REJECT, + AgentWorkItemAction.OPEN_SOURCE, + ], + editableFields: [ + { + id: 'awi-nora-02-field-signal-type', + label: 'Signalart', + value: 'Expansion', + helperText: 'Zur Auswahl stehen Umzug, Expansion, Verkleinerung, Neugründung und Filialschliessung.', + }, + { + id: 'awi-nora-02-field-confidence', + label: 'Konfidenz in Prozent', + value: '48', + helperText: 'Ab 70 Prozent gilt das Signal als verifiziert und darf in die Wochen-Mail.', + }, + { + id: 'awi-nora-02-field-region', + label: 'Region und Segment', + value: 'Zug — Büro', + }, + { + id: 'awi-nora-02-field-note', + label: 'Notiz für die Lead-Übergabe', + value: + 'Standortsuche ab 2027, Flächenbedarf unbekannt. Vor einer Kontaktaufnahme die Mitarbeitendenzahl und den Zeithorizont verifizieren.', + multiline: true, + }, + ], + history: [ + { + id: 'awi-nora-02-hist-1', + at: '2026-05-17T06:30:00.000Z', + actor: 'Nora', + action: 'Signal erfasst und in die Review-Queue gelegt', + note: 'Konfidenz 48 Prozent, keine zweite Quelle.', + }, + { + id: 'awi-nora-02-hist-2', + at: '2026-05-19T06:05:00.000Z', + actor: 'Nora', + action: 'Erneut gegengeprüft', + note: 'Weiterhin keine bestätigende zweite Quelle — Konfidenz unverändert.', + }, + { + id: 'awi-nora-02-hist-3', + at: '2026-05-20T06:05:00.000Z', + actor: 'Nora', + action: 'Review erinnert', + note: 'Ältester offener Eintrag der Review-Queue, seit 3 Tagen unbearbeitet.', + }, + ], + }, +] diff --git a/src/mock-data/agents/bruno.ts b/src/mock-data/agents/bruno.ts new file mode 100644 index 0000000..56f13a9 --- /dev/null +++ b/src/mock-data/agents/bruno.ts @@ -0,0 +1,377 @@ +/** + * Property On — Personalblatt «Bruno», Besichtigungs-Briefing (Abteilung Vermarktung). + * + * Fachliche Wahrheit aus dem Agenten-Katalog: Rolle, Personalnummer, E-Mail und die + * neun Aufgaben werden unverändert übernommen. Zeitanker der Mockdaten: 20.05.2026. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +export const brunoAgent: TeamAgent = { + id: 'bruno', + name: 'Bruno', + role: 'Besichtigungs-Briefing', + personnelNumber: 'PO-ZD-13', + department: 'Vermarktung', + email: 'bruno@property-on.ch', + avatarTone: AgentAvatarTone.GOLD, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.AUTONOMOUS, + shortDescription: + 'Bereitet Aussentermine automatisch mit Objekt-, Gegenüber- und Argumentationsinformationen vor.', + autonomyNote: '«Autonom»', + + profile: { + purpose: + 'Bruno bereitet jeden Aussentermin der Vermarktung rechtzeitig vor und liefert der verantwortlichen Person ein einseitiges Briefing zu Objekt, Gegenüber und Argumentation.', + input: [ + 'Kalendertermin mit Zeit, Ort, Objektbezug und Teilnehmenden', + 'Objektinformationen aus Dossier, Ablage und Bewirtschaftungssystem', + 'Öffentlich verfügbare Firmeninformationen zum Gegenüber', + ], + coreFlow: [ + 'Zwei Stunden vor dem Aussentermin selbständig anlaufen und den Termin einlesen', + 'Alles zum Objekt zusammentragen: Fläche, Ausbau, Konditionen, Verfügbarkeit, Historie', + 'Offene Punkte klar als fehlende Information kennzeichnen, statt sie zu überspielen', + 'Das Gegenüber über öffentlich zugängliche Quellen einordnen und die Herkunft der Angaben festhalten', + 'Drei Verkaufsargumente und drei erwartbare Einwände samt Antwort formulieren', + 'Alles auf einer Seite bündeln, per E-Mail zustellen und bei Terminverschiebung nachführen', + ], + output: [ + 'Einseitiges Briefing zum Aussentermin', + 'Audiofassung des Briefings für die Anfahrt', + 'Drei Verkaufsargumente je Objekt und Gegenüber', + 'Drei erwartbare Einwände mit vorbereiteter Antwort', + ], + }, + + responsibilities: [ + 'Automatischer Anlauf zwei Stunden vor jedem Aussentermin der Vermarktung', + 'Zusammentragen der Objektinformationen aus Dossier, Ablage und Bewirtschaftungssystem', + 'Sichtbares Markieren fehlender oder veralteter Angaben', + 'Recherche des Gegenübers über öffentlich zugängliche Quellen', + 'Aufbereitung von Verkaufsargumenten und erwartbaren Einwänden', + 'Erstellung und Zustellung des einseitigen Briefings samt optionaler Audiofassung', + 'Nachführung des Briefings bei Terminverschiebung oder geänderter Teilnehmerliste', + ], + + tasks: [ + { + id: 'bruno-task-01', + title: 'Zwei Stunden vor einem Aussentermin automatisch starten', + description: + 'Beobachtet den Kalender der Vermarktung und startet die Vorbereitung selbständig, sobald ein Aussentermin in den eingestellten Vorlauf rückt. Erkennt Ort, Objektbezug und Teilnehmende aus dem Termineintrag.', + enabled: true, + schedule: 'zwei Stunden vor dem Termin', + requiresApproval: false, + dependsOnSystem: AgentSystemType.CALENDAR, + }, + { + id: 'bruno-task-02', + title: 'Objektinformationen zusammentragen', + description: + 'Sammelt Fläche, Ausbaustandard, Mietzins, Nebenkosten, Verfügbarkeit, Parkplätze, Erschliessung und die bisherige Vermarktungshistorie aus Objektdossier, Ablage und Bewirtschaftungssystem.', + enabled: true, + schedule: 'direkt nach dem Start der Vorbereitung', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'bruno-task-03', + title: 'Fehlende Informationen markieren', + description: + 'Kennzeichnet jede Angabe, die nicht auffindbar oder älter als der hinterlegte Zeitraum ist, sichtbar als Lücke. Es werden keine Werte geschätzt und keine Annahmen als Tatsachen ausgewiesen.', + enabled: true, + schedule: 'im Anschluss an das Zusammentragen der Objektinformationen', + requiresApproval: false, + dependsOnSystem: AgentSystemType.IMMOTOP2, + }, + { + id: 'bruno-task-04', + title: 'Gegenüber über öffentliche Quellen recherchieren', + description: + 'Ordnet Firma und Ansprechperson des Gegenübers über öffentlich zugängliche Quellen ein: Handelsregistereintrag, amtliche Meldungen, Firmenwebsite und Medienberichte. Zu jeder Angabe wird die Quelle mitgeführt; nicht belegbare Angaben bleiben aussen vor.', + enabled: true, + schedule: 'parallel zur Objektaufbereitung', + requiresApproval: false, + dependsOnSystem: AgentSystemType.ZEFIX, + }, + { + id: 'bruno-task-05', + title: 'Drei Verkaufsargumente formulieren', + description: + 'Leitet aus Objektstärken und dem Profil des Gegenübers die eingestellte Anzahl Verkaufsargumente ab, je Argument ein Satz mit dem konkreten Nutzen für dieses Unternehmen.', + enabled: true, + schedule: 'nach Abschluss von Objekt- und Gegenüber-Recherche', + requiresApproval: false, + }, + { + id: 'bruno-task-06', + title: 'Drei erwartbare Einwände vorbereiten', + description: + 'Benennt die wahrscheinlichsten Einwände zu Mietzins, Lage, Ausbau, Bezugstermin oder Vertragsdauer und hinterlegt zu jedem eine sachliche Antwort mit Beleg aus dem Objektdossier.', + enabled: true, + schedule: 'unmittelbar nach den Verkaufsargumenten', + requiresApproval: false, + }, + { + id: 'bruno-task-07', + title: 'Einseitiges Briefing erstellen', + description: + 'Bündelt Termindaten, Objektkennzahlen, Gegenüber, Verkaufsargumente, Einwände und markierte Lücken auf einer Seite und stellt es der verantwortlichen Person per E-Mail zu.', + enabled: true, + schedule: 'spätestens 90 Minuten vor dem Termin', + requiresApproval: false, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'bruno-task-08', + title: 'Optionale Audiofassung erstellen', + description: + 'Erstellt auf Wunsch eine gesprochene Fassung des Briefings für die Anfahrt. Die Audiofassung folgt demselben Aufbau wie das Dokument und wird zusammen mit diesem zugestellt.', + enabled: true, + schedule: 'zusammen mit dem Briefing, sofern aktiviert', + requiresApproval: false, + dependsOnChannel: AgentChannelType.AUDIO_OUTPUT, + }, + { + id: 'bruno-task-09', + title: 'Briefing bei Terminverschiebung aktualisieren', + description: + 'Erkennt verschobene, verlegte oder abgesagte Termine sowie geänderte Teilnehmende, führt das Briefing nach und stellt die aktualisierte Fassung mit Hinweis auf die Änderung zu.', + enabled: true, + schedule: 'bei Änderung des Termineintrags', + requiresApproval: false, + dependsOnChannel: AgentChannelType.CALENDAR, + }, + ], + + channels: [ + { + id: 'bruno-ch-calendar', + type: AgentChannelType.CALENDAR, + direction: AgentChannelDirection.INBOUND, + description: + 'Terminkalender der Vermarktung Zürich und Winterthur: liefert Aussentermine mit Zeit, Ort, Objektbezug und Teilnehmenden sowie deren Änderungen.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Terminkalender Vermarktung', + inboxAddress: 'termine@property-on.ch', + defaultRecipients: [], + autoReplyEnabled: false, + }, + }, + { + id: 'bruno-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Versand des einseitigen Briefings an die verantwortliche Person und die weiteren Teilnehmenden des Aussentermins.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Briefing-Versand Vermarktung', + senderAddress: 'bruno@property-on.ch', + defaultRecipients: ['vermarktung@property-on.ch', 'nadine.brunner@property-on.ch'], + autoReplyEnabled: false, + }, + }, + { + id: 'bruno-ch-audio-output', + type: AgentChannelType.AUDIO_OUTPUT, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Gesprochene Fassung des Briefings für die Anfahrt — im Katalog als optionaler Kanal geführt.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + optional: true, + config: { + displayName: 'Audiofassung Briefing', + senderAddress: 'bruno@property-on.ch', + defaultRecipients: ['nadine.brunner@property-on.ch'], + autoReplyEnabled: false, + }, + }, + ], + + systems: [ + { + id: 'bruno-sys-calendar', + type: AgentSystemType.CALENDAR, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Aussentermine samt Ort, Objektbezug und Teilnehmenden und erkennt Verschiebungen und Absagen.', + permissionNote: + 'Nur Lesezugriff — es werden keine Termine erstellt, verschoben oder gelöscht.', + }, + { + id: 'bruno-sys-dms', + type: AgentSystemType.DMS, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Objektdossiers, Grundrisse, Ausbaubeschriebe und frühere Besichtigungsnotizen zur betroffenen Fläche.', + permissionNote: 'Nur Lesezugriff — Dokumente werden weder verändert noch abgelegt.', + }, + { + id: 'bruno-sys-sharepoint', + type: AgentSystemType.SHAREPOINT, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Vermarktungsunterlagen, Konditionenblätter und Bildmaterial aus der Teamablage der Vermarktung.', + permissionNote: + 'Nur Lesezugriff auf die freigegebenen Bibliotheken der Vermarktung; andere Bereiche bleiben unberührt.', + }, + { + id: 'bruno-sys-zefix', + type: AgentSystemType.ZEFIX, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Prüft Firmensitz, Rechtsform, Gründungsjahr und Zeichnungsberechtigte des Gegenübers im Handelsregister.', + permissionNote: 'Nur Lesezugriff auf öffentlich einsehbare Registerangaben.', + }, + { + id: 'bruno-sys-public-web', + type: AgentSystemType.PUBLIC_WEB, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Sichtet Firmenwebsite, Standortangaben und Medienberichte zum Gegenüber und hält zu jeder Angabe die Quelle fest.', + permissionNote: + 'Nur öffentlich zugängliche Quellen; es werden keine Zugangsdaten verwendet und keine Anmeldebereiche geöffnet.', + }, + { + id: 'bruno-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Stammdaten, Konditionen und Verfügbarkeit der kommerziell genutzten Immobilien und meldet veraltete Angaben als Lücke.', + permissionNote: + 'Nur Lesezugriff — Stammdaten der Bewirtschaftung werden nicht verändert.', + }, + ], + + settings: [ + { + id: 'bruno-set-lead-time', + label: 'Triggerzeit vor Termin', + description: + 'Vorlauf, mit dem Bruno die Vorbereitung eines Aussentermins selbständig startet.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.GENERAL, + value: 2, + unit: 'Stunden', + min: 1, + max: 24, + }, + { + id: 'bruno-set-briefing-length', + label: 'Gewünschte Briefinglänge', + description: + 'Bestimmt, wie ausführlich das Briefing ausfällt. Der Katalogstandard ist die einseitige Fassung.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.SPECIFIC, + value: 'one_pager', + options: [ + { value: 'short', label: 'Kurzfassung, halbe Seite' }, + { value: 'one_pager', label: 'Einseitiges Briefing' }, + { value: 'extended', label: 'Ausführlich, zwei Seiten mit Objekthistorie' }, + ], + }, + { + id: 'bruno-set-audio', + label: 'Audiofassung aktiv', + description: + 'Ist die Einstellung aktiv, wird zusätzlich zum Dokument eine gesprochene Fassung für die Anfahrt zugestellt.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.DELIVERY, + value: true, + }, + { + id: 'bruno-set-sources', + label: 'Bevorzugte Quellen', + description: + 'Quellen, die für die Recherche zum Gegenüber und zum Objekt herangezogen werden. Jede Angabe im Briefing wird mit ihrer Quelle ausgewiesen.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['zefix', 'shab', 'company_website', 'property_dossier'], + options: [ + { value: 'zefix', label: 'Zefix' }, + { value: 'shab', label: 'SHAB' }, + { value: 'company_website', label: 'Firmenwebsite' }, + { value: 'property_dossier', label: 'Objektdossier' }, + { value: 'media_reports', label: 'Medienberichte' }, + ], + }, + { + id: 'bruno-set-selling-points', + label: 'Anzahl Verkaufsargumente', + description: 'Anzahl der Argumente, die Bruno je Termin auf das Gegenüber zuschneidet.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 3, + unit: 'Argumente', + min: 1, + max: 6, + }, + { + id: 'bruno-set-objections', + label: 'Anzahl erwartbarer Einwände', + description: + 'Anzahl der Einwände, die Bruno vorwegnimmt und mit einer belegten Antwort hinterlegt.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 3, + unit: 'Einwände', + min: 1, + max: 6, + }, + ], + + metrics: [ + { + id: 'bruno-metric-upcoming', + label: 'Kommende Termine', + value: '14', + hint: 'Aussentermine der Vermarktung in den nächsten 14 Tagen, Stand 20.05.2026', + }, + { + id: 'bruno-metric-briefings', + label: 'Erstellte Briefings', + value: '96', + hint: 'letzte 30 Tage, inklusive 11 Aktualisierungen nach Terminverschiebung', + }, + { + id: 'bruno-metric-coverage', + label: 'Termine mit Briefing', + value: '92 %', + hint: 'Anteil der Aussentermine, die rechtzeitig vorbereitet wurden', + }, + { + id: 'bruno-metric-gaps', + label: 'Erkannte Informationslücken', + value: '23', + hint: 'fehlende oder veraltete Objektangaben, im Briefing sichtbar markiert', + }, + ], + + headlineMetricId: 'bruno-metric-briefings', + lastRun: '2026-05-20T12:00:00.000Z', +} diff --git a/src/mock-data/agents/ferdi.ts b/src/mock-data/agents/ferdi.ts new file mode 100644 index 0000000..6c7b252 --- /dev/null +++ b/src/mock-data/agents/ferdi.ts @@ -0,0 +1,402 @@ +/** + * Property On — Personalblatt «Ferdi», Fristen-Wächter (Abteilung Bewirtschaftung). + * + * Fachliche Wahrheit aus dem Agenten-Katalog: Rolle, Personalnummer, E-Mail und die + * neun Aufgaben werden unverändert übernommen. Zeitanker der Mockdaten: 20.05.2026. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +export const ferdiAgent: TeamAgent = { + id: 'ferdi', + name: 'Ferdi', + role: 'Fristen-Wächter', + personnelNumber: 'PO-ZD-02', + department: 'Bewirtschaftung', + email: 'ferdi@property-on.ch', + avatarTone: AgentAvatarTone.BRAND, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.APPROVAL_REQUIRED, + shortDescription: + 'Überwacht Termine, Optionen, Break-Klauseln und Anpassungsfristen und eskaliert ausbleibende Reaktionen.', + autonomyNote: '«Autonom beim Erinnern, Freigabe bei Eskalationen»', + + profile: { + purpose: + 'Ferdi hält den gesamten Terminbestand der kommerziell genutzten Immobilien im Blick, erinnert rechtzeitig an jede Frist und legt der Bewirtschaftung eine Eskalation vor, sobald eine Reaktion ausbleibt.', + input: [ + 'Mietverträge der bewirtschafteten Liegenschaften in Zürich, Winterthur, Basel, Zug, Bern und St. Gallen', + 'Nachträge, Zusatzvereinbarungen und Optionsausübungen', + 'Terminexporte aus dem Bewirtschaftungssystem', + 'Excel-Terminlisten der Standortteams', + ], + coreFlow: [ + 'Verträge und Nachträge aus Ablage und Bewirtschaftungssystem einlesen', + 'Fristen, Optionen und Break-Klauseln herauslesen und mit Fundstelle festhalten', + 'Jeden neu gefundenen Termin einmalig durch die Bewirtschaftung bestätigen lassen', + 'Den bestätigten Terminbestand jeden Morgen gegen den Kalender abgleichen', + 'Gestaffelt erinnern und nach fünf Arbeitstagen ohne Reaktion ein zweites Mal melden', + 'Bleibt die Reaktion weiter aus, eine Eskalation zur Freigabe vorlegen und alles protokollieren', + ], + output: [ + 'Fristen-Alerts 12, 9, 6 und 3 Monate vor dem Stichtag', + 'Wochen-Pipeline der anstehenden Fristen, montags um 07:00 Uhr', + 'Eskalationsvorschläge zur Freigabe durch die Bewirtschaftung', + 'Lückenloses Meldeprotokoll je Vertrag, Frist und Reaktion', + ], + }, + + responsibilities: [ + 'Vollständige Erfassung aller Verträge und Nachträge im Terminbestand', + 'Herauslesen von Kündigungsfristen, Optionsfristen und Break-Klauseln samt Fundstelle', + 'Einmalige Bestätigung jedes erfassten Termins durch die Bewirtschaftung', + 'Tägliche Prüfung des Terminbestands auf anstehende und überschrittene Fristen', + 'Gestaffelte Erinnerungen 12, 9, 6 und 3 Monate vor Frist', + 'Zweitmeldung bei ausbleibender Reaktion', + 'Vorlage von Eskalationen zur Freigabe durch die zuständige Leitung', + 'Wöchentliche Fristen-Pipeline für die Standortteams', + 'Protokollierung sämtlicher Meldungen, Reaktionen und Freigaben', + ], + + tasks: [ + { + id: 'ferdi-task-01', + title: 'Verträge und Nachträge einlesen', + description: + 'Liest Mietverträge, Nachträge und Zusatzvereinbarungen aus der Dokumentenablage, den Standortordnern und dem Bewirtschaftungssystem ein und ordnet jedes Dokument der richtigen Liegenschaft und Mieteinheit zu.', + enabled: true, + schedule: 'bei Eingang, zusätzlich täglicher Abgleich 05:30', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'ferdi-task-02', + title: 'Fristen, Optionen und Break-Klauseln extrahieren', + description: + 'Liest aus jedem Vertrag die Kündigungsfristen, Optionsfristen, Break-Klauseln sowie Index- und Staffelanpassungen heraus und hält je Termin Stichtag, Fristenlauf und die Fundstelle im Dokument fest. Unklare Formulierungen werden als offen markiert statt geschätzt.', + enabled: true, + schedule: 'direkt nach dem Einlesen eines Dokuments', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'ferdi-task-03', + title: 'Extrahierte Termine zur einmaligen Bestätigung vorlegen', + description: + 'Legt jeden neu gefundenen Termin der zuständigen Person der Bewirtschaftung mit Stichtag, Fristenlauf und Fundstelle einmalig zur Bestätigung vor. Erst ein bestätigter Termin wird in den überwachten Terminbestand aufgenommen.', + enabled: true, + schedule: 'werktags 08:00, Sammelvorlage je Standort', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'ferdi-task-04', + title: 'Terminbestand täglich um 06:00 Uhr prüfen', + description: + 'Gleicht den gesamten bestätigten Terminbestand jeden Morgen gegen das aktuelle Datum und den Kalender ab, ermittelt die fälligen Meldeschwellen und erkennt Termine, die verschoben, erledigt oder überschritten wurden.', + enabled: true, + schedule: 'täglich 06:00', + requiresApproval: false, + dependsOnSystem: AgentSystemType.CALENDAR, + }, + { + id: 'ferdi-task-05', + title: 'Erinnerungen 12, 9, 6 und 3 Monate vor Frist auslösen', + description: + 'Meldet jede anstehende Frist gestaffelt an die zuständige Person: zwölf, neun, sechs und drei Monate vor dem Stichtag, jeweils mit Objekt, Mieter, Fristart und der verbleibenden Zeit bis zur Reaktion.', + enabled: true, + schedule: 'täglich 06:00, sobald eine Meldeschwelle erreicht ist', + requiresApproval: false, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'ferdi-task-06', + title: 'Zweitmeldung nach fünf Arbeitstagen ohne Reaktion', + description: + 'Bleibt eine Erinnerung fünf Arbeitstage ohne Rückmeldung, meldet Ferdi dieselbe Frist ein zweites Mal, zusätzlich im Team-Kanal des Standorts, und kennzeichnet den Vorgang als offen.', + enabled: true, + schedule: 'fünf Arbeitstage nach der ersten Meldung ohne Reaktion', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WORKSPACE_CHAT, + }, + { + id: 'ferdi-task-07', + title: 'Eskalation zur Freigabe vorlegen', + description: + 'Reagiert auch auf die Zweitmeldung niemand, bereitet Ferdi eine Eskalation an die hinterlegten Eskalationsempfänger vor: Objekt, Mieter, Fristart, Stichtag, Meldeverlauf und die Folgen einer versäumten Frist. Die Eskalation geht ausschliesslich nach Freigabe hinaus.', + enabled: true, + schedule: 'bei ausbleibender Reaktion auf die Zweitmeldung', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'ferdi-task-08', + title: 'Fristen-Pipeline montags um 07:00 Uhr erstellen', + description: + 'Stellt zum Wochenstart eine Übersicht aller Fristen der nächsten zwölf Monate zusammen, gegliedert nach Standort, Meldeschwelle und Bearbeitungsstand, und stellt sie den Standortteams zu.', + enabled: true, + schedule: 'montags 07:00', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WORKSPACE_CHAT, + }, + { + id: 'ferdi-task-09', + title: 'Meldungen und Reaktionen protokollieren', + description: + 'Hält je Frist fest, wann gemeldet wurde, wer reagiert hat, welche Freigabe erteilt wurde und wie der Vorgang abgeschlossen worden ist — damit der Verlauf jederzeit nachvollziehbar bleibt.', + enabled: true, + schedule: 'laufend, Tagesabschluss 18:30', + requiresApproval: false, + dependsOnSystem: AgentSystemType.SHAREPOINT, + }, + ], + + channels: [ + { + id: 'ferdi-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.BOTH, + description: + 'Fristen-Postfach der Bewirtschaftung: versendet Bestätigungsanfragen, Erinnerungen und Eskalationen und nimmt die Rückmeldungen der zuständigen Personen entgegen.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Fristen-Postfach Bewirtschaftung', + senderAddress: 'ferdi@property-on.ch', + inboxAddress: 'fristen@property-on.ch', + defaultRecipients: ['bewirtschaftung@property-on.ch'], + autoReplyEnabled: false, + }, + }, + { + id: 'ferdi-ch-whatsapp', + type: AgentChannelType.WHATSAPP, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Kurzmeldung auf das Diensthandy, wenn eine Frist in weniger als 30 Tagen abläuft und die zuständige Person nicht am Arbeitsplatz erreichbar ist.', + status: AgentConnectionStatus.ROADMAP, + enabled: false, + optional: true, + config: { + displayName: 'Kurzmeldung Diensthandy', + senderAddress: '+41 44 500 21 02', + defaultRecipients: ['+41 79 412 66 08'], + autoReplyEnabled: false, + }, + }, + { + id: 'ferdi-ch-calendar', + type: AgentChannelType.CALENDAR, + direction: AgentChannelDirection.BOTH, + description: + 'Fristenkalender der Bewirtschaftung: liest bestehende Termine und trägt bestätigte Fristen samt Vorlaufmarken für 12, 9, 6 und 3 Monate ein.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Fristenkalender Bewirtschaftung', + senderAddress: 'ferdi@property-on.ch', + defaultRecipients: ['bewirtschaftung@property-on.ch', 'andrea.hersche@property-on.ch'], + autoReplyEnabled: false, + }, + }, + { + id: 'ferdi-ch-workspace-chat', + type: AgentChannelType.WORKSPACE_CHAT, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Team-Kanal der Standorte: erhält die Zweitmeldungen und montags die Fristen-Pipeline der nächsten zwölf Monate.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Team-Kanal «Fristen Bewirtschaftung»', + senderAddress: 'ferdi@property-on.ch', + defaultRecipients: ['#fristen-zuerich', '#fristen-winterthur', '#fristen-basel'], + autoReplyEnabled: false, + }, + }, + ], + + systems: [ + { + id: 'ferdi-sys-dms', + type: AgentSystemType.DMS, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Mietverträge, Nachträge und Zusatzvereinbarungen und merkt sich je Termin die Fundstelle im Dokument.', + permissionNote: 'Nur Lesezugriff — Dokumente werden weder verändert noch verschoben.', + }, + { + id: 'ferdi-sys-sharepoint', + type: AgentSystemType.SHAREPOINT, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest die Standortbibliotheken mit Excel-Terminlisten, Übergabeprotokollen und Optionsvereinbarungen der Standortteams.', + permissionNote: + 'Nur Lesezugriff — das Meldeprotokoll wird in der dafür vorgesehenen Fristenliste geführt.', + }, + { + id: 'ferdi-sys-fileshare', + type: AgentSystemType.FILESHARE, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest die historischen Vertragsordner der übernommenen Mandate, die noch nicht in der Dokumentenablage erfasst sind.', + permissionNote: 'Nur Lesezugriff — Ordnerstruktur und Dateien bleiben unberührt.', + }, + { + id: 'ferdi-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Objekt- und Mieterstammdaten, Vertragslaufzeiten und die Terminexporte des Bewirtschaftungssystems zum Abgleich mit dem eigenen Terminbestand.', + permissionNote: + 'Nur Lesezugriff — Stammdaten und Vertragsdaten der Bewirtschaftung werden nicht verändert.', + }, + { + id: 'ferdi-sys-calendar', + type: AgentSystemType.CALENDAR, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Trägt bestätigte Fristen mit ihren Vorlaufmarken ein und hält den Fristenkalender der Standortteams aktuell.', + permissionNote: + 'Trägt ausschliesslich eigene Fristeneinträge ein; fremde Termine werden nie verschoben oder gelöscht.', + }, + { + id: 'ferdi-sys-exchange', + type: AgentSystemType.M365_EXCHANGE, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Versendet Bestätigungsanfragen, Erinnerungen und Zweitmeldungen und liest die Rückmeldungen aus dem Fristen-Postfach.', + permissionNote: + 'Erinnerungen gehen selbständig hinaus; Eskalationen erst nach Freigabe der zuständigen Leitung.', + }, + ], + + settings: [ + { + id: 'ferdi-set-thresholds', + label: 'Meldeschwellen vor Frist', + description: + 'Zeitpunkte, zu denen Ferdi vor einem Stichtag erinnert. Jede aktivierte Schwelle löst eine eigene Meldung an die zuständige Person aus.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['m12', 'm9', 'm6', 'm3'], + options: [ + { value: 'm18', label: '18 Monate vor Frist' }, + { value: 'm12', label: '12 Monate vor Frist' }, + { value: 'm9', label: '9 Monate vor Frist' }, + { value: 'm6', label: '6 Monate vor Frist' }, + { value: 'm3', label: '3 Monate vor Frist' }, + { value: 'm1', label: '1 Monat vor Frist' }, + ], + }, + { + id: 'ferdi-set-second-notice', + label: 'Zweitmeldung nach Arbeitstagen', + description: + 'Anzahl Arbeitstage ohne Rückmeldung, nach denen dieselbe Frist ein zweites Mal gemeldet wird — zusätzlich im Team-Kanal des Standorts.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 5, + unit: 'Arbeitstage', + min: 1, + max: 15, + }, + { + id: 'ferdi-set-escalation-recipients', + label: 'Eskalationsempfänger', + description: + 'Erhalten die Eskalation, wenn auch auf die Zweitmeldung keine Reaktion erfolgt, und erteilen die Freigabe für den Versand.', + kind: AgentSettingKind.TEXT, + group: AgentSettingGroup.APPROVAL, + value: + 'Andrea Hersche, Leitung Bewirtschaftung Zürich (Stellvertretung: Thomas Bühlmann, Winterthur; in Kopie: Mandatsleitung Basel)', + }, + { + id: 'ferdi-set-weekly-report', + label: 'Wochenreport-Zeitpunkt', + description: + 'Uhrzeit, zu der die Fristen-Pipeline der nächsten zwölf Monate zusammengestellt und den Standortteams zugestellt wird. Wochentag ist fix montags.', + kind: AgentSettingKind.TIME, + group: AgentSettingGroup.DELIVERY, + value: '07:00', + }, + { + id: 'ferdi-set-deadline-types', + label: 'Einzubeziehende Fristarten', + description: + 'Arten von Terminen, die Ferdi in den Terminbestand aufnimmt und überwacht. Nicht aktivierte Fristarten werden erfasst, aber nicht gemeldet.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: [ + 'termination', + 'option', + 'break_clause', + 'index', + 'staggered_rent', + 'service_charges', + ], + options: [ + { value: 'termination', label: 'Kündigungsfristen' }, + { value: 'option', label: 'Optionsfristen' }, + { value: 'break_clause', label: 'Break-Klauseln' }, + { value: 'index', label: 'Indexanpassungen' }, + { value: 'staggered_rent', label: 'Staffelmieten' }, + { value: 'service_charges', label: 'Nebenkostenabrechnung' }, + ], + }, + ], + + metrics: [ + { + id: 'ferdi-metric-contracts', + label: 'Erfasste Verträge', + value: '842', + hint: 'Mietverträge und Nachträge über alle Mandate, Stand 20.05.2026', + }, + { + id: 'ferdi-metric-deadlines', + label: 'Überwachte Fristen', + value: '1 316', + hint: 'bestätigte Termine im aktiven Terminbestand', + }, + { + id: 'ferdi-metric-next-critical', + label: 'Nächste kritische Frist', + value: '05.06.2026', + hint: 'Optionsfrist Sihlfeld Logistik GmbH, Bahnhofstrasse 44, Zürich', + }, + { + id: 'ferdi-metric-open-confirmations', + label: 'Offene Bestätigungen', + value: '17', + hint: 'neu gefundene Termine, die auf die einmalige Bestätigung warten', + }, + { + id: 'ferdi-metric-escalations', + label: 'Eskalationen', + value: '3', + hint: 'zur Freigabe vorgelegt, letzte 30 Tage', + }, + ], + + headlineMetricId: 'ferdi-metric-deadlines', + lastRun: '2026-05-20T06:00:00.000Z', +} diff --git a/src/mock-data/agents/lea.ts b/src/mock-data/agents/lea.ts new file mode 100644 index 0000000..a5be37c --- /dev/null +++ b/src/mock-data/agents/lea.ts @@ -0,0 +1,490 @@ +/** + * Property On — Personalblatt «Lea», Anfragen-Managerin (Abteilung Vermarktung). + * + * Fachliche Wahrheit aus dem Agenten-Katalog: Rolle, Personalnummer, E-Mail und die + * elf Aufgaben werden unverändert übernommen. Zeitanker der Mockdaten: 20.05.2026. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +const ABSAGEVORLAGE = `Guten Tag {Anrede} {Nachname} + +Besten Dank für Ihre Anfrage zur Fläche {Objekt} in {Ort}. + +Wir haben Ihre Angaben geprüft. Leider erfüllt Ihr Bedarf die für dieses Objekt +festgelegten Grundvoraussetzungen nicht: {Grund}. + +Sobald eine passende Fläche in unserem Portfolio frei wird, melden wir uns +unaufgefordert bei Ihnen. + +Freundliche Grüsse +Property On — Vermarktung +{Verantwortliche Person}` + +export const leaAgent: TeamAgent = { + id: 'lea', + name: 'Lea', + role: 'Anfragen-Managerin', + personnelNumber: 'PO-ZD-07', + department: 'Vermarktung', + email: 'lea@property-on.ch', + avatarTone: AgentAvatarTone.SUCCESS, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.APPROVAL_REQUIRED, + shortDescription: + 'Normalisiert, qualifiziert und koordiniert Anfragen bis zum bestätigten Besichtigungstermin.', + autonomyNote: + '«Automatische Absage bei Nichterfüllung; Freigabe für Schritte Richtung Termin»', + + profile: { + purpose: + 'Lea nimmt jede eingehende Flächenanfrage auf, prüft sie gegen die Must-Kriterien der ausgeschriebenen Fläche und begleitet sie bis zum bestätigten Besichtigungstermin.', + input: [ + 'Portal-Lead-Mails der Immobilienplattformen', + 'Direktmails an das Anfragen-Postfach der Vermarktung', + 'Website-Formulare von property-on.ch', + 'Telefonnotizen aus dem Empfang Zürich und Winterthur', + 'Must-Kriterien je ausgeschriebener Fläche', + 'Besichtigungskalender der verantwortlichen Person', + ], + coreFlow: [ + 'Anfragen aus allen Kanälen einsammeln und in ein einheitliches Bedarfsprofil überführen', + 'Prüfen, ob dieselbe Person bereits über einen anderen Kanal angefragt hat', + 'Fehlende Pflichtangaben mit einem vorbereiteten Rückfragetext nachfassen', + 'Bedarfsprofil mit den Must-Kriterien der Fläche abgleichen', + 'Nicht passende Interessenten sachlich absagen, passende zur Entscheidung vorlegen', + 'Nach Freigabe freie Zeitfenster suchen, Termin vorschlagen, bestätigen und den Verlauf ablegen', + ], + output: [ + 'Automatische Absagen mit nachvollziehbarer Begründung', + 'Qualifizierte Kurzprofile für die Entscheidung der Vermarktung', + 'Bestätigte Besichtigungstermine samt vorbereitetem Reminder', + 'Lückenloses Anfrage-Log je Fläche und Interessent', + ], + }, + + responsibilities: [ + 'Erfassung sämtlicher Anfragen aus Portalen, Website, E-Mail und Telefon', + 'Überführung von Freitext in ein einheitliches Bedarfsprofil', + 'Deduplizierung über alle Kanäle hinweg', + 'Qualifizierung gegen die Must-Kriterien der ausgeschriebenen Flächen', + 'Automatische Absage bei klarer Nichterfüllung', + 'Kalendersteuerung und Terminkoordination bis zur Bestätigung', + 'Protokollierung von Status und Verlauf je Anfrage', + ], + + tasks: [ + { + id: 'lea-task-01', + title: 'Anfragen aus verschiedenen Kanälen erfassen', + description: + 'Nimmt Portal-Lead-Mails, Direktmails, Website-Formulare und Telefonnotizen entgegen und legt zu jeder Anfrage einen Eintrag auf der betroffenen Fläche an.', + enabled: true, + schedule: 'bei Eingang', + requiresApproval: false, + dependsOnSystem: AgentSystemType.PORTAL_LEAD_INBOX, + }, + { + id: 'lea-task-02', + title: 'Freitext in ein Bedarfsprofil überführen', + description: + 'Liest den Anfragetext und trägt Flächenbedarf, Nutzungsart, Wunschtermin, Budgetrahmen und Vertragsdauer in ein einheitliches Bedarfsprofil ein. Unklare Angaben werden als offen markiert statt geraten.', + enabled: true, + schedule: 'bei Eingang, direkt nach der Erfassung', + requiresApproval: false, + }, + { + id: 'lea-task-03', + title: 'Duplikate kanalübergreifend erkennen', + description: + 'Gleicht Name, Firma, Telefonnummer und E-Mail mit den Anfragen der letzten 90 Tage ab. Mehrfachanfragen derselben Person über Portal, Formular und Telefon werden zu einem Vorgang zusammengeführt.', + enabled: true, + schedule: 'bei Eingang, Abgleich über die letzten 90 Tage', + requiresApproval: false, + dependsOnSystem: AgentSystemType.CRM, + }, + { + id: 'lea-task-04', + title: 'Fehlende Angaben über Rückfrageentwürfe einholen', + description: + 'Erstellt bei unvollständigen Anfragen einen Rückfrageentwurf mit den fehlenden Mindestangaben. Der Entwurf geht erst nach Freigabe der verantwortlichen Person hinaus.', + enabled: true, + schedule: 'bei unvollständiger Anfrage, Sammelvorlage werktags 08:30', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'lea-task-05', + title: 'Anfrage gegen Must-Kriterien prüfen', + description: + 'Vergleicht das Bedarfsprofil mit den hinterlegten Must-Kriterien der Fläche und hält je Kriterium fest, ob es erfüllt, nicht erfüllt oder noch offen ist.', + enabled: true, + schedule: 'unmittelbar nach Vollständigkeit des Bedarfsprofils', + requiresApproval: false, + dependsOnSystem: AgentSystemType.IMMOTOP2, + }, + { + id: 'lea-task-06', + title: 'Nicht passende Interessenten automatisch absagen', + description: + 'Versendet bei klarer Nichterfüllung eines Must-Kriteriums die hinterlegte Absagevorlage mit dem konkreten Grund und schliesst den Vorgang. Grenzfälle werden nie automatisch abgesagt, sondern vorgelegt.', + enabled: true, + schedule: 'stündlich, werktags 07:00–19:00', + requiresApproval: false, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'lea-task-07', + title: 'Passende Interessenten zur Entscheidung vorlegen', + description: + 'Legt der verantwortlichen Person ein Kurzprofil vor: Bedarf, erfüllte und offene Kriterien, Herkunftskanal und Dringlichkeit. Ohne Freigabe geht kein Schritt Richtung Termin hinaus.', + enabled: true, + schedule: 'werktags 07:30 und 13:30', + requiresApproval: true, + dependsOnSystem: AgentSystemType.CRM, + }, + { + id: 'lea-task-08', + title: 'Nach Freigabe Kalender abgleichen', + description: + 'Sucht im Besichtigungskalender der verantwortlichen Person freie Zeitfenster gemäss den hinterlegten Kalenderregeln, unter Berücksichtigung von Termindauer und Pufferzeit.', + enabled: true, + schedule: 'nach erteilter Freigabe', + requiresApproval: true, + dependsOnSystem: AgentSystemType.CALENDAR, + }, + { + id: 'lea-task-09', + title: 'Terminvorschläge senden', + description: + 'Sendet dem Interessenten drei Terminvorschläge mit Treffpunkt und Ansprechperson und reserviert die Zeitfenster vorläufig im Kalender.', + enabled: true, + schedule: 'innerhalb von 30 Minuten nach Freigabe', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'lea-task-10', + title: 'Termin bestätigen und Reminder vorbereiten', + description: + 'Bestätigt den gewählten Termin, trägt ihn verbindlich im Kalender ein, löst die übrigen Reservationen und bereitet den Reminder für Interessent und Ansprechperson vor.', + enabled: true, + schedule: 'bei Zusage des Interessenten', + requiresApproval: true, + dependsOnChannel: AgentChannelType.CALENDAR, + }, + { + id: 'lea-task-11', + title: 'Status und Verlauf protokollieren', + description: + 'Hält je Anfrage Kanal, Eingangszeit, Prüfergebnis, Freigaben, versandte Nachrichten und Terminstatus im Protokoll fest, damit der Verlauf jederzeit nachvollziehbar bleibt.', + enabled: true, + schedule: 'laufend, Tagesabschluss 19:00', + requiresApproval: false, + dependsOnSystem: AgentSystemType.CRM, + }, + ], + + channels: [ + { + id: 'lea-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.BOTH, + description: + 'Anfragen-Postfach der Vermarktung: Direktmails von Interessenten sowie Rückfragen, Absagen und Terminvorschläge im Versand.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Anfragen-Postfach Vermarktung', + senderAddress: 'lea@property-on.ch', + inboxAddress: 'anfragen@property-on.ch', + defaultRecipients: ['vermarktung@property-on.ch'], + autoReplyEnabled: true, + }, + }, + { + id: 'lea-ch-portal-lead-mail', + type: AgentChannelType.PORTAL_LEAD_MAIL, + direction: AgentChannelDirection.INBOUND, + description: + 'Lead-Mails der Immobilienplattformen zu den ausgeschriebenen Flächen in Zürich, Winterthur, Basel, Zug, Bern und St. Gallen.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Portal-Leads Plattformen', + inboxAddress: 'portal-leads@property-on.ch', + defaultRecipients: [], + autoReplyEnabled: false, + }, + }, + { + id: 'lea-ch-web-form', + type: AgentChannelType.WEB_FORM, + direction: AgentChannelDirection.INBOUND, + description: + 'Formular «Fläche anfragen» auf property-on.ch — liefert bereits strukturierte Felder zu Bedarf, Nutzung und Wunschtermin.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Website-Formular «Fläche anfragen»', + inboxAddress: 'formular@property-on.ch', + defaultRecipients: ['vermarktung@property-on.ch'], + autoReplyEnabled: true, + }, + }, + { + id: 'lea-ch-phone-note', + type: AgentChannelType.PHONE_NOTE, + direction: AgentChannelDirection.INBOUND, + description: + 'Telefonnotizen des Empfangs in Zürich und Winterthur, als Freitext erfasst und für die Deduplizierung mitgeführt.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + optional: true, + config: { + displayName: 'Telefonnotizen Empfang', + inboxAddress: 'telefonnotiz@property-on.ch', + defaultRecipients: [], + autoReplyEnabled: false, + }, + }, + { + id: 'lea-ch-calendar', + type: AgentChannelType.CALENDAR, + direction: AgentChannelDirection.BOTH, + description: + 'Besichtigungskalender der Vermarktung: liest freie Zeitfenster, setzt Reservationen und trägt bestätigte Termine ein.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Besichtigungskalender Vermarktung', + senderAddress: 'lea@property-on.ch', + defaultRecipients: ['nadine.brunner@property-on.ch', 'vermarktung@property-on.ch'], + autoReplyEnabled: false, + }, + }, + ], + + systems: [ + { + id: 'lea-sys-portal-inbox', + type: AgentSystemType.PORTAL_LEAD_INBOX, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Holt die Lead-Mails der Immobilienplattformen ab und ordnet sie der ausgeschriebenen Fläche zu.', + permissionNote: 'Nur Lesezugriff — es werden keine Einträge im Portal verändert.', + }, + { + id: 'lea-sys-crm', + type: AgentSystemType.CRM, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Führt Interessent, Bedarfsprofil, Prüfergebnis und Terminstatus, erkennt Duplikate und schreibt das Anfrage-Log.', + permissionNote: + 'Schreibt Anfragen, Notizen und Statuswechsel selbständig; Vertragsdaten bleiben unberührt.', + }, + { + id: 'lea-sys-calendar', + type: AgentSystemType.CALENDAR, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Sucht freie Zeitfenster, reserviert Terminvorschläge und trägt bestätigte Besichtigungen samt Reminder ein.', + permissionNote: + 'Schreibt erst nach Freigabe der verantwortlichen Person; bestehende Termine werden nie verschoben oder gelöscht.', + }, + { + id: 'lea-sys-exchange', + type: AgentSystemType.M365_EXCHANGE, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest das Anfragen-Postfach und versendet Absagen, Rückfragen, Terminvorschläge und Bestätigungen.', + permissionNote: + 'Automatischer Versand ausschliesslich für Absagen; alle übrigen Nachrichten gehen erst nach Freigabe hinaus.', + }, + { + id: 'lea-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Flächenstammdaten, Verfügbarkeit und die hinterlegten Must-Kriterien der kommerziell genutzten Immobilien.', + permissionNote: + 'Nur Lesezugriff — Stammdaten der Bewirtschaftung werden nicht verändert.', + }, + ], + + settings: [ + { + id: 'lea-set-must-criteria', + label: 'Must-Kriterien je Fläche', + description: + 'Kriterien, die eine Anfrage zwingend erfüllen muss. Wird eines davon klar verfehlt, folgt die automatische Absage.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['area_fit', 'usage_allowed', 'start_date', 'budget_range', 'credit_check'], + options: [ + { value: 'area_fit', label: 'Flächenbedarf im ausgeschriebenen Rahmen' }, + { value: 'usage_allowed', label: 'Nutzungsart für das Objekt zulässig' }, + { value: 'start_date', label: 'Wunschtermin Mietbeginn passt zur Verfügbarkeit' }, + { value: 'budget_range', label: 'Budgetrahmen erreicht die Mietpreisvorstellung' }, + { value: 'credit_check', label: 'Bonitätsnachweis kann beigebracht werden' }, + { value: 'lease_term', label: 'Vertragsdauer mindestens fünf Jahre' }, + { value: 'parking', label: 'Parkplatzbedarf am Standort abgedeckt' }, + ], + }, + { + id: 'lea-set-auto-reject', + label: 'Automatische Absage aktiv', + description: + 'Ist die Einstellung aktiv, sagt Lea bei klarer Nichterfüllung eines Must-Kriteriums ohne Rückfrage ab. Grenzfälle werden weiterhin vorgelegt.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.APPROVAL, + value: true, + }, + { + id: 'lea-set-reject-template', + label: 'Absagevorlage', + description: + 'Text der automatischen Absage. Platzhalter in geschweiften Klammern werden je Anfrage eingesetzt.', + kind: AgentSettingKind.TEXT, + group: AgentSettingGroup.DELIVERY, + value: ABSAGEVORLAGE, + }, + { + id: 'lea-set-min-fields', + label: 'Mindestangaben', + description: + 'Angaben, die für eine Qualifizierung vorliegen müssen. Fehlt eine davon, erstellt Lea einen Rückfrageentwurf.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['contact', 'company', 'area_sqm', 'usage', 'start_date'], + options: [ + { value: 'contact', label: 'Name und Kontaktangaben' }, + { value: 'company', label: 'Firma und Branche' }, + { value: 'area_sqm', label: 'Gewünschte Fläche in m²' }, + { value: 'usage', label: 'Geplante Nutzungsart' }, + { value: 'start_date', label: 'Wunschtermin Mietbeginn' }, + { value: 'budget', label: 'Budgetrahmen' }, + { value: 'lease_term', label: 'Gewünschte Vertragsdauer' }, + { value: 'parking', label: 'Parkplatzbedarf' }, + ], + }, + { + id: 'lea-set-owner', + label: 'Verantwortliche Person', + description: + 'Erhält die qualifizierten Kurzprofile und erteilt die Freigabe für alle Schritte Richtung Termin.', + kind: AgentSettingKind.TEXT, + group: AgentSettingGroup.APPROVAL, + value: 'Nadine Brunner, Vermarktung Zürich (Stellvertretung: Marco Steinegger, Winterthur)', + }, + { + id: 'lea-set-calendar-rules', + label: 'Kalenderregeln', + description: 'Bestimmt, welche Zeitfenster Lea für Besichtigungen vorschlagen darf.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.SPECIFIC, + value: 'business_hours', + options: [ + { value: 'business_hours', label: 'Geschäftszeiten Mo–Fr, 08:00–17:00' }, + { value: 'extended', label: 'Erweitert inkl. Samstagvormittag' }, + { value: 'owner_availability', label: 'Nach Verfügbarkeit der verantwortlichen Person' }, + { value: 'fixed_slots', label: 'Feste Besichtigungsfenster Dienstag und Donnerstag' }, + ], + }, + { + id: 'lea-set-duration', + label: 'Termindauer', + description: 'Dauer eines Besichtigungstermins, die Lea im Kalender reserviert.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 45, + unit: 'Minuten', + min: 15, + max: 120, + }, + { + id: 'lea-set-buffer', + label: 'Pufferzeit', + description: 'Freizuhaltende Zeit vor und nach einem Termin, etwa für Anfahrt und Übergabe.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 15, + unit: 'Minuten', + min: 0, + max: 60, + }, + { + id: 'lea-set-reminder', + label: 'Reminder-Zeitpunkt', + description: 'Wann Interessent und Ansprechperson an den bestätigten Termin erinnert werden.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.DELIVERY, + value: 'h24', + options: [ + { value: 'h24', label: '24 Stunden vor dem Termin' }, + { value: 'h4', label: 'vier Stunden vor dem Termin' }, + { value: 'h2', label: 'zwei Stunden vor dem Termin' }, + { value: 'h1', label: 'eine Stunde vor dem Termin' }, + ], + }, + ], + + metrics: [ + { + id: 'lea-metric-new', + label: 'Neue Anfragen', + value: '128', + hint: 'letzte 30 Tage, alle Kanäle, nach Deduplizierung', + }, + { + id: 'lea-metric-qualified', + label: 'Qualifizierte Anfragen', + value: '74', + hint: 'Mindestangaben vollständig und Must-Kriterien erfüllt', + }, + { + id: 'lea-metric-auto-rejected', + label: 'Automatische Absagen', + value: '39', + hint: 'klare Nichterfüllung eines Must-Kriteriums', + }, + { + id: 'lea-metric-open-decisions', + label: 'Offene Entscheidungen', + value: '6', + hint: 'wartet auf Freigabe der verantwortlichen Person', + }, + { + id: 'lea-metric-confirmed', + label: 'Bestätigte Termine', + value: '31', + hint: 'Besichtigungen mit vorbereitetem Reminder', + }, + { + id: 'lea-metric-first-response', + label: 'Durchschnittliche Erstreaktionszeit', + value: '11 Minuten', + hint: 'vom Eingang bis zur ersten Rückmeldung, Stand 20.05.2026', + }, + ], + + headlineMetricId: 'lea-metric-qualified', + lastRun: '2026-05-20T06:15:00.000Z', +} diff --git a/src/mock-data/agents/livia.ts b/src/mock-data/agents/livia.ts new file mode 100644 index 0000000..892802d --- /dev/null +++ b/src/mock-data/agents/livia.ts @@ -0,0 +1,370 @@ +/** + * Property On — Personalblatt «Livia», Lage-Analystin (Vermarktung). + * + * Fachliche Wahrheit aus dem Agenten-Katalog: Rolle, Personalnummer, E-Mail und + * die elf Katalogaufgaben werden hier unverändert abgebildet. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +export const liviaAgent: TeamAgent = { + id: 'livia', + name: 'Livia', + role: 'Lage-Analystin', + personnelNumber: 'PO-ZD-21', + department: 'Vermarktung', + email: 'livia@property-on.ch', + avatarTone: AgentAvatarTone.WARNING, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.AUTONOMOUS, + shortDescription: + 'Erstellt aktuelle Mikro- und Makrolagebeschriebe in zwei Längen und mit Quellen.', + autonomyNote: + 'Autonom. Livia recherchiert, schreibt und frischt Lagebeschriebe ohne vorgängige Freigabe auf. Jede Aussage wird mit Quelle und Abrufdatum protokolliert, damit die Vermarktung jeden Satz nachprüfen kann.', + + profile: { + purpose: + 'Livia beschreibt die Lage einer kommerziell genutzten Immobilie so, dass Vermarktung und Interessenten in wenigen Minuten verstehen, wo das Objekt steht, wie es erreichbar ist und was sich im Umfeld verändert.', + input: [ + 'Objektadresse aus dem Vermarktungsauftrag', + 'Öffentliche Umfeld- und Verkehrsdaten zu Quartier, Anbindung und Nahversorgung', + 'Amtliche Publikationen zu Bauprojekten, Zonenänderungen und Umfeldentwicklungen', + 'Bestehende Objektunterlagen aus der Dokumentenablage', + ], + coreFlow: [ + 'Adresse aufnehmen und dem richtigen Quartier zuordnen, etwa Zürich Kreis 5 oder Winterthur Neuhegi.', + 'Umfeld im festgelegten Radius abgehen: Nachbarschaft, Erdgeschossnutzungen, Erreichbarkeit, Einkaufen und Verpflegung.', + 'Gemeinde und Region einordnen: Wirtschaftsstruktur, Steuerlage, Entwicklung der Nachfrage.', + 'Amtliche Publikationen und Bauprojekte im Umfeld sichten und bewerten, ob sie für das Objekt relevant sind.', + 'Aus den gesammelten Bausteinen einen Kurztext fürs Inserat und einen Langtext fürs Exposé schreiben.', + 'Quellen anhängen, Ergebnis je Objekt ablegen und quartalsweise gegen den aktuellen Stand prüfen.', + ], + output: [ + 'Kurztext für Inserat und Portalauftritt', + 'Langtext für das Exposé mit Mikro-, Makro- und Umfeldteil', + 'Quellenverzeichnis mit Abrufdatum je Aussage', + 'Änderungshinweise, sobald sich das Umfeld spürbar verändert', + ], + }, + + responsibilities: [ + 'Mikro- und Makrolage kommerziell genutzter Immobilien nachvollziehbar beschreiben', + 'Erreichbarkeit, Nahversorgung und geplante Umfeldentwicklungen sauber trennen und darstellen', + 'Kurz- und Langtexte in der Firmen-Tonalität liefern, ohne Werbefloskeln ohne Beleg', + 'Jede Aussage mit einer nachprüfbaren Quelle und einem Abrufdatum hinterlegen', + 'Lagebeschriebe quartalsweise auffrischen und relevante Abweichungen an die Vermarktung melden', + ], + + tasks: [ + { + id: 'livia-task-01', + title: 'Objektadresse aufnehmen', + description: + 'Übernimmt Strasse, Hausnummer, Postleitzahl und Ort aus dem Vermarktungsauftrag, prüft die Schreibweise gegen die amtlichen Adressdaten und ordnet das Objekt einem Quartier zu, etwa Zürich Kreis 5, Winterthur Neuhegi oder Basel Gundeldingen.', + enabled: true, + schedule: 'bei Eingang eines Vermarktungsauftrags', + requiresApproval: false, + dependsOnSystem: AgentSystemType.IMMOTOP2, + }, + { + id: 'livia-task-02', + title: 'Mikrolage analysieren', + description: + 'Beschreibt das unmittelbare Umfeld im eingestellten Radius: Nachbarschaft, Erdgeschossnutzungen, Lärm- und Immissionslage, Publikumsfrequenz sowie Parkierung. Trennt beobachtbare Fakten klar von Einschätzungen.', + enabled: true, + schedule: 'nach der Adressaufnahme', + requiresApproval: false, + dependsOnSystem: AgentSystemType.PUBLIC_WEB, + }, + { + id: 'livia-task-03', + title: 'Makrolage einordnen', + description: + 'Ordnet Gemeinde und Wirtschaftsraum ein: Bevölkerungs- und Beschäftigtenentwicklung, Branchenmix, Steuerlage und Bedeutung des Standorts innerhalb der Region, etwa Zug im Wirtschaftsraum Zentralschweiz oder St. Gallen als Zentrum der Ostschweiz.', + enabled: true, + schedule: 'nach der Mikrolage-Analyse', + requiresApproval: false, + dependsOnSystem: AgentSystemType.PUBLIC_WEB, + }, + { + id: 'livia-task-04', + title: 'Erreichbarkeit und Nahversorgung darstellen', + description: + 'Stellt Anbindung an öffentlichen Verkehr, Autobahnanschluss, Velo- und Fusswege sowie Einkaufen, Verpflegung, Kinderbetreuung und Gesundheitsversorgung dar — jeweils mit Distanz oder Wegzeit statt mit unbelegten Werbeaussagen.', + enabled: true, + schedule: 'nach der Makrolage-Einordnung', + requiresApproval: false, + dependsOnSystem: AgentSystemType.PUBLIC_WEB, + }, + { + id: 'livia-task-05', + title: 'Geplante Projekte und Umfeldentwicklungen aufnehmen', + description: + 'Sichtet amtliche Publikationen und Bauprojekte im Umfeld — Arealentwicklungen, Zonenänderungen, Verkehrsvorhaben, neue Haltestellen — und hält fest, ab wann sie für das Objekt spürbar werden, etwa das Entwicklungsgebiet Bern Wankdorf.', + enabled: true, + schedule: 'montags 07:00', + requiresApproval: false, + dependsOnSystem: AgentSystemType.SHAB, + }, + { + id: 'livia-task-06', + title: 'Kurzen Inserattext erstellen', + description: + 'Verdichtet die Lagebausteine auf einen Kurztext für Inserat und Portalauftritt, innerhalb des eingestellten Zeichenlimits und in der gewählten Firmen-Tonalität.', + enabled: true, + schedule: 'sobald alle Lagebausteine vorliegen', + requiresApproval: false, + }, + { + id: 'livia-task-07', + title: 'Ausführlichen Exposé-Text erstellen', + description: + 'Schreibt den Langtext für das Exposé mit Mikrolage, Makrolage, Erreichbarkeit, Nahversorgung und Umfeldentwicklung in getrennten Abschnitten, damit sich Interessenten gezielt einlesen können.', + enabled: true, + schedule: 'direkt im Anschluss an den Kurztext', + requiresApproval: false, + }, + { + id: 'livia-task-08', + title: 'Quellen hinterlegen', + description: + 'Hängt jeder Aussage die verwendete Quelle mit Bezeichnung und Abrufdatum an. Lässt sich eine Aussage nicht belegen, wird sie gestrichen statt abgeschwächt.', + enabled: true, + schedule: 'bei jeder Texterstellung', + requiresApproval: false, + }, + { + id: 'livia-task-09', + title: 'Ergebnisse je Objekt lokal zwischenspeichern', + description: + 'Legt Kurztext, Langtext und Quellenverzeichnis je Objekt in der Dokumentenablage ab, sodass der nächste Arbeitsauftrag auf dem bestehenden Stand aufsetzt und nichts doppelt recherchiert wird.', + enabled: true, + schedule: 'nach jedem abgeschlossenen Arbeitsauftrag', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'livia-task-10', + title: 'Quartalsweisen Aktualisierungslauf simulieren', + description: + 'Führt den «Quartals-Refresh» durch: prüft alle abgelegten Lagebeschriebe gegen den aktuellen Stand der Quellen, markiert veraltete Abschnitte und erneuert sie. Der Lauf wird protokolliert und ist je Objekt nachvollziehbar.', + enabled: true, + schedule: 'quartalsweise am ersten Werktag 05:30', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'livia-task-11', + title: 'Relevante Änderungen melden', + description: + 'Meldet der Vermarktung nur, was den Lagebeschrieb wirklich verändert — neue Haltestelle, bewilligte Arealentwicklung, wegfallende Nahversorgung — mit Hinweis auf den betroffenen Textabschnitt und die Quelle.', + enabled: true, + schedule: 'bei erkannter Abweichung im Aktualisierungslauf', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WORKSPACE_CHAT, + }, + ], + + channels: [ + { + id: 'livia-ch-workspace-chat', + type: AgentChannelType.WORKSPACE_CHAT, + direction: AgentChannelDirection.BOTH, + description: + 'Livia nimmt Objektadressen und Rückfragen im Arbeitsbereich entgegen und liefert Kurztext, Langtext und Änderungshinweise direkt im Kanal der Vermarktung.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Livia — Lage & Umfeld', + inboxAddress: 'arbeitsbereich/vermarktung/lage', + defaultRecipients: ['vermarktung@property-on.ch'], + autoReplyEnabled: true, + }, + }, + { + id: 'livia-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Versand der fertigen Lagebeschriebe und der Quartalsmeldung an Vermarktung und Bewirtschaftung. Eingehende Mails werden nicht bearbeitet.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Livia (Property On)', + senderAddress: 'livia@property-on.ch', + defaultRecipients: ['vermarktung@property-on.ch', 'bewirtschaftung@property-on.ch'], + autoReplyEnabled: false, + }, + }, + ], + + systems: [ + { + id: 'livia-sys-public-web', + type: AgentSystemType.PUBLIC_WEB, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Öffentliche Umfeld- und Verkehrsdaten: Haltestellen und Fahrpläne, Wegzeiten, Einkaufen und Verpflegung, Gemeindeangaben zu Bevölkerung und Beschäftigung.', + permissionNote: 'Nur lesend. Livia ruft ausschliesslich frei zugängliche Seiten ab.', + }, + { + id: 'livia-sys-shab', + type: AgentSystemType.SHAB, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Amtliche Publikationen zu Baugesuchen, Zonenänderungen und Arealentwicklungen im Umfeld des Objekts.', + permissionNote: 'Nur lesend. Meldungen werden als Quelle mit Publikationsdatum übernommen.', + }, + { + id: 'livia-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Objektstammdaten und Adressen aus der Bewirtschaftung als Ausgangspunkt jedes Lagebeschriebs.', + permissionNote: 'Nur lesend. Livia verändert keine Stammdaten in der Bewirtschaftungslösung.', + }, + { + id: 'livia-sys-dms', + type: AgentSystemType.DMS, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Ablage von Kurztext, Langtext und Quellenverzeichnis je Objekt sowie Zwischenstand für den Quartalslauf.', + permissionNote: + 'Schreibt ausschliesslich in den Ordner «Lagebeschriebe» des jeweiligen Objekts; bestehende Fassungen bleiben als Version erhalten.', + }, + { + id: 'livia-sys-workspace', + type: AgentSystemType.WORKSPACE, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Aufträge, Rückfragen und Änderungshinweise im Arbeitsbereich der Vermarktung, inklusive Protokoll der Aktualisierungsläufe.', + permissionNote: + 'Schreibt nur in den eigenen Bereich «Lage & Umfeld»; Objektdossiers anderer Teams bleiben unberührt.', + }, + ], + + settings: [ + { + id: 'livia-set-short-limit', + label: 'Kurztext-Zeichenlimit', + description: 'Obergrenze für den Lagetext im Inserat und auf den Portalen.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 600, + unit: 'Zeichen', + min: 200, + max: 1200, + }, + { + id: 'livia-set-long-limit', + label: 'Langtext-Zeichenlimit', + description: 'Obergrenze für den ausführlichen Lageteil im Exposé.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 2400, + unit: 'Zeichen', + min: 800, + max: 5000, + }, + { + id: 'livia-set-tonality', + label: 'Firmen-Tonalität', + description: 'Sprachstil, in dem Kurz- und Langtext verfasst werden.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.GENERAL, + value: 'sachlich', + options: [ + { value: 'sachlich', label: 'sachlich' }, + { value: 'hochwertig', label: 'hochwertig' }, + { value: 'zurueckhaltend', label: 'zurückhaltend' }, + ], + }, + { + id: 'livia-set-refresh-interval', + label: 'Aktualisierungsintervall', + description: 'Rhythmus, in dem bestehende Lagebeschriebe gegen die Quellen geprüft werden.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.SPECIFIC, + value: 'quartalsweise', + options: [ + { value: 'monatlich', label: 'monatlich' }, + { value: 'quartalsweise', label: 'quartalsweise' }, + { value: 'halbjaehrlich', label: 'halbjährlich' }, + { value: 'jaehrlich', label: 'jährlich' }, + ], + }, + { + id: 'livia-set-analysis-radius', + label: 'Radius der Umfeldanalyse', + description: 'Umkreis um die Objektadresse, der für die Mikrolage ausgewertet wird.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 800, + unit: 'Meter', + min: 200, + max: 3000, + }, + { + id: 'livia-set-source-types', + label: 'Relevante Quellentypen', + description: 'Welche Quellen Livia für Lagebeschriebe und Aktualisierungsläufe heranzieht.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['amtliche_publikationen', 'verkehrsdaten', 'bauprojekte', 'nahversorgung'], + options: [ + { value: 'amtliche_publikationen', label: 'amtliche Publikationen' }, + { value: 'verkehrsdaten', label: 'Verkehrsdaten' }, + { value: 'bauprojekte', label: 'Bauprojekte' }, + { value: 'nahversorgung', label: 'Nahversorgung' }, + { value: 'medienberichte', label: 'Medienberichte' }, + ], + }, + ], + + metrics: [ + { + id: 'livia-metric-objects', + label: 'Analysierte Objekte', + value: '184', + hint: 'Objekte mit vollständigem Lagebeschrieb, unter anderem in Zürich, Winterthur, Zug und St. Gallen.', + }, + { + id: 'livia-metric-current-blocks', + label: 'Aktuelle Lagebausteine', + value: '162', + hint: 'Bausteine, die im letzten Aktualisierungslauf gegen die Quellen bestätigt wurden.', + }, + { + id: 'livia-metric-due-refresh', + label: 'Fällige Aktualisierungen', + value: '9', + hint: 'Objekte, deren Lagebeschrieb im nächsten Quartals-Refresh erneuert wird.', + }, + { + id: 'livia-metric-detected-changes', + label: 'Erkannte Umfeldänderungen', + value: '14', + hint: 'Seit dem letzten Lauf gemeldete Änderungen, etwa die Arealentwicklung Winterthur Neuhegi.', + }, + ], + + headlineMetricId: 'livia-metric-objects', + lastRun: '2026-05-20T06:00:00.000Z', +} diff --git a/src/mock-data/agents/nora.ts b/src/mock-data/agents/nora.ts new file mode 100644 index 0000000..01a1498 --- /dev/null +++ b/src/mock-data/agents/nora.ts @@ -0,0 +1,397 @@ +/** + * Property On — Personalblatt «Nora», Markt-Scout. + * + * Nora beobachtet öffentliche Quellen und simulierte Feeds zu den definierten + * Regionen und Segmenten, stuft jedes Signal ein und legt unsichere Fälle in die + * Review-Queue. Sie arbeitet ausschliesslich auf Vorschlagsbasis: Wochen-Mail, + * Leads und Markteinschätzung gehen erst nach Freigabe durch die Marktbeobachtung + * hinaus. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +export const noraAgent: TeamAgent = { + id: 'nora', + name: 'Nora', + role: 'Markt-Scout', + personnelNumber: 'PO-ZD-19', + department: 'Marktbeobachtung', + email: 'nora@property-on.ch', + avatarTone: AgentAvatarTone.SLATE, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.PROPOSAL_ONLY, + shortDescription: + 'Beobachtet Marktsignale, ordnet sie ein und erstellt Wochen-Mails, Leads und Marktbausteine.', + autonomyNote: '«Vorschlag»', + + profile: { + purpose: + 'Nora hält die Marktbeobachtung ohne tägliche Handarbeit auf dem Laufenden, indem sie Bewegungen bei kommerziell genutzten Immobilien früh erkennt, einordnet und als prüffertigen Vorschlag vorlegt.', + input: [ + 'Regionen: Zürich, Winterthur, Zug, Basel, Bern und St. Gallen', + 'Segmente: Büro, Retail, Gastronomie, Logistik, Gewerbe und Produktion', + 'Öffentliche Quellen wie SHAB-Meldungen, Zefix-Einträge und Firmenwebseiten', + 'Simulierte Feeds zu Ausschreibungen, Standortmeldungen und Medienberichten', + 'Objekt- und Portfoliostammdaten aus dem Arbeitsbereich', + ], + coreFlow: [ + 'Nora sichtet die freigegebenen Quellen und Feeds zu den eingestellten Regionen und Segmenten.', + 'Jede Meldung wird als Signalart eingeordnet — zum Beispiel Umzug, Expansion oder Filialschliessung.', + 'Zu jedem Signal vergibt sie eine Konfidenz und hinterlegt die Quelle mit Datum.', + 'Liegt die Konfidenz unter der Review-Schwelle, wandert das Signal in die Review-Queue statt in den Versand.', + 'Verifizierte Signale werden einer Region und, wenn möglich, einem konkreten Objekt zugeordnet.', + 'Wochen-Mail, Lead-Liste und Markteinschätzung entstehen als Vorschlag und gehen erst nach Freigabe raus.', + ], + output: [ + 'Wochen-Mail mit den wichtigsten Marktsignalen, dienstags um 08:00 Uhr', + 'Einzelne Marktsignale mit Signalart, Konfidenz, Quelle und Regionsbezug', + 'Lead-Liste aus Umzugs- und Expansionssignalen zur Übergabe an das CRM', + 'Monatliche Markteinschätzung je Region und Segment', + 'Review-Queue mit den unsicheren Signalen zur Sichtung durch das Team', + ], + }, + + responsibilities: [ + 'Laufende Beobachtung der freigegebenen Quellen und Feeds in den eingestellten Regionen', + 'Nachvollziehbare Einordnung jedes Signals nach Signalart und Konfidenz', + 'Saubere Trennung zwischen verifizierten Signalen und Fällen für die Review-Queue', + 'Zuordnung der Signale zu Regionen und, wo belegbar, zu einzelnen Objekten', + 'Vorbereitung von Wochen-Mail, Lead-Liste und monatlicher Markteinschätzung als Vorschlag', + ], + + tasks: [ + { + id: 'nora-task-01', + title: 'Quellen und Feeds simuliert überwachen', + description: + 'Sichtet die freigegebenen öffentlichen Quellen und die simulierten Feeds zu den eingestellten Regionen und Segmenten. Neue Meldungen werden mit Quelle, Datum und Fundort erfasst; es wird nichts veröffentlicht und nichts weitergegeben.', + enabled: true, + schedule: 'täglich 06:00, zusätzlich stündlich zwischen 08:00 und 18:00', + requiresApproval: false, + dependsOnSystem: AgentSystemType.PUBLIC_WEB, + }, + { + id: 'nora-task-02', + title: 'Marktsignale klassifizieren', + description: + 'Ordnet jede erfasste Meldung einer Signalart zu — Umzug, Expansion, Verkleinerung, Neugründung oder Filialschliessung — und hält fest, welche Textstelle der Quelle die Einordnung stützt.', + enabled: true, + schedule: 'bei Eingang einer neuen Meldung', + requiresApproval: false, + dependsOnSystem: AgentSystemType.WORKSPACE, + }, + { + id: 'nora-task-03', + title: 'Konfidenz vergeben', + description: + 'Vergibt jedem Signal einen Konfidenzwert in Prozent. Grundlage sind Verlässlichkeit der Quelle, Aktualität der Meldung und ob sich dieselbe Information in einer zweiten Quelle bestätigt.', + enabled: true, + schedule: 'bei jeder Klassifizierung', + requiresApproval: false, + }, + { + id: 'nora-task-04', + title: 'unsichere Signale in eine Review-Queue legen', + description: + 'Liegt die Konfidenz unter der eingestellten Review-Schwelle oder widersprechen sich die Quellen, legt Nora das Signal in die Review-Queue der Marktbeobachtung. Solche Signale erscheinen weder in der Wochen-Mail noch in der Lead-Liste, bevor jemand sie geprüft hat.', + enabled: true, + schedule: 'sobald die Konfidenz unter der Review-Schwelle liegt', + requiresApproval: false, + dependsOnSystem: AgentSystemType.WORKSPACE, + }, + { + id: 'nora-task-05', + title: 'Signale Regionen und Objekten zuordnen', + description: + 'Verknüpft jedes verifizierte Signal mit der betroffenen Region und, sofern Adresse oder Firmensitz eindeutig belegt sind, mit einem konkreten Objekt im Portfolio. Bleibt die Zuordnung unklar, wird sie offen gelassen und als offen ausgewiesen.', + enabled: true, + schedule: 'bei jedem verifizierten Signal', + requiresApproval: false, + dependsOnSystem: AgentSystemType.WORKSPACE, + }, + { + id: 'nora-task-06', + title: 'Wochen-Mail dienstags um 08:00 Uhr erstellen', + description: + 'Stellt die wichtigsten Signale der vergangenen Woche nach Region und Segment zusammen, jeweils mit Konfidenz und Quellenangabe. Der Entwurf geht als Vorschlag an die Marktbeobachtung und wird erst nach Freigabe an die eingetragenen Empfänger versendet.', + enabled: true, + schedule: 'dienstags 08:00', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + { + id: 'nora-task-07', + title: 'Umzugs- und Expansionssignale als Leads ausweisen', + description: + 'Kennzeichnet Signale zu Umzug und Expansion als Lead, ergänzt Firma, Region, gesuchtes Segment und Zeithorizont und bereitet die Übergabe an das simulierte CRM vor. Übergeben wird erst nach ausdrücklicher Freigabe.', + enabled: true, + schedule: 'bei Erkennung eines Umzugs- oder Expansionssignals', + requiresApproval: true, + dependsOnSystem: AgentSystemType.CRM, + }, + { + id: 'nora-task-08', + title: 'monatliche Markteinschätzung erstellen', + description: + 'Fasst die Signale des Monats je Region und Segment zu einer Einschätzung zusammen: Bewegung im Markt, auffällige Häufungen, offene Reviews und Vergleich zum Vormonat. Der Bericht wird als Vorschlag vorgelegt und nach Freigabe verteilt.', + enabled: true, + schedule: 'monatlich am 1. um 07:00', + requiresApproval: true, + dependsOnChannel: AgentChannelType.EMAIL, + }, + ], + + channels: [ + { + id: 'nora-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Versand der freigegebenen Wochen-Mail und der monatlichen Markteinschätzung an Marktbeobachtung, Akquisition und Bewirtschaftung. Nora nimmt über diesen Kanal keine Post entgegen.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Nora — Markt-Scout', + senderAddress: 'nora@property-on.ch', + defaultRecipients: [ + 'marktbeobachtung@property-on.ch', + 'akquisition.zuerich@property-on.ch', + 'bewirtschaftung.winterthur@property-on.ch', + ], + autoReplyEnabled: false, + }, + }, + { + id: 'nora-ch-teams', + type: AgentChannelType.TEAMS, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Kurzmeldung in den Teamkanal, sobald ein Signal mit hoher Konfidenz auftaucht — etwa eine angekündigte Filialschliessung in Basel. Reine Ausgabe, ohne Rückkanal.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Nora — Markt-Scout', + defaultRecipients: ['marktbeobachtung', 'akquisition-mitte'], + autoReplyEnabled: false, + }, + }, + { + id: 'nora-ch-workspace-chat', + type: AgentChannelType.WORKSPACE_CHAT, + direction: AgentChannelDirection.BOTH, + description: + 'Arbeitskanal für die Review-Queue: Das Team fragt Signale ab, bestätigt oder verwirft sie, und Nora meldet zurück, welche Vorschläge noch auf Freigabe warten.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Nora — Markt-Scout', + defaultRecipients: ['marktbeobachtung', 'portfolio-steuerung'], + autoReplyEnabled: true, + }, + }, + ], + + systems: [ + { + id: 'nora-sys-public-web', + type: AgentSystemType.PUBLIC_WEB, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Öffentlich zugängliche Firmenwebseiten, Standortmeldungen und Medienberichte aus Zürich, Winterthur, Zug, Basel, Bern und St. Gallen als Ausgangspunkt für Marktsignale.', + permissionNote: + 'Nur Leserecht auf frei zugängliche Seiten. Nora meldet sich nirgends an, füllt keine Formulare aus und veröffentlicht nichts.', + }, + { + id: 'nora-sys-shab', + type: AgentSystemType.SHAB, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Amtliche Meldungen zu Gründungen, Sitzverlegungen, Umfirmierungen und Liquidationen — die verlässlichste Grundlage für Umzugs- und Neugründungssignale.', + permissionNote: + 'Nur Leserecht. Nora liest veröffentlichte Meldungen und übernimmt Meldungsnummer und Datum als Quellenangabe.', + }, + { + id: 'nora-sys-zefix', + type: AgentSystemType.ZEFIX, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Abgleich von Firmenname, Rechtsform und eingetragenem Sitz, damit ein Signal der richtigen Firma und der richtigen Region zugeordnet wird.', + permissionNote: + 'Nur Leserecht auf den öffentlichen Handelsregisterindex. Es werden keine Einträge erfasst oder verändert.', + }, + { + id: 'nora-sys-crm', + type: AgentSystemType.CRM, + access: AgentAccessLevel.WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Ablage der freigegebenen Leads aus Umzugs- und Expansionssignalen, damit die Akquisition direkt weiterarbeiten kann.', + permissionNote: + 'Übergibt Leads an ein simuliertes CRM. Geschrieben wird erst nach Freigabe durch die Marktbeobachtung; bestehende Datensätze werden weder überschrieben noch gelöscht.', + }, + { + id: 'nora-sys-workspace', + type: AgentSystemType.WORKSPACE, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Führt die Signalliste, die Review-Queue und die Entwürfe für Wochen-Mail und Markteinschätzung; liest Objekt- und Portfoliostammdaten für die Zuordnung.', + permissionNote: + 'Lese- und Schreibrecht im eigenen Arbeitsbereich. Nora legt dort Signale, Reviews und Entwürfe ab — der Versand nach aussen bleibt an die Freigabe gebunden.', + }, + ], + + settings: [ + { + id: 'nora-set-regions', + label: 'Regionen', + description: 'Nur Meldungen aus diesen Regionen werden als Marktsignal erfasst.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.GENERAL, + value: ['ZUERICH', 'WINTERTHUR', 'ZUG', 'BASEL', 'BERN', 'ST_GALLEN'], + options: [ + { value: 'ZUERICH', label: 'Zürich' }, + { value: 'WINTERTHUR', label: 'Winterthur' }, + { value: 'ZUG', label: 'Zug' }, + { value: 'BASEL', label: 'Basel' }, + { value: 'BERN', label: 'Bern' }, + { value: 'ST_GALLEN', label: 'St. Gallen' }, + { value: 'LUZERN', label: 'Luzern' }, + ], + }, + { + id: 'nora-set-segments', + label: 'Segmente', + description: + 'Nutzungsarten der kommerziell genutzten Immobilien, die Nora beobachtet.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.GENERAL, + value: ['BUERO', 'RETAIL', 'GASTRONOMIE', 'LOGISTIK'], + options: [ + { value: 'BUERO', label: 'Büro' }, + { value: 'RETAIL', label: 'Retail' }, + { value: 'GASTRONOMIE', label: 'Gastronomie' }, + { value: 'LOGISTIK', label: 'Logistik' }, + { value: 'GEWERBE', label: 'Gewerbe' }, + { value: 'PRODUKTION', label: 'Produktion' }, + ], + }, + { + id: 'nora-set-signal-types', + label: 'Signalarten', + description: 'Diese Arten von Marktbewegungen werden erfasst und eingeordnet.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['UMZUG', 'EXPANSION', 'NEUGRUENDUNG', 'FILIALSCHLIESSUNG'], + options: [ + { value: 'UMZUG', label: 'Umzug' }, + { value: 'EXPANSION', label: 'Expansion' }, + { value: 'VERKLEINERUNG', label: 'Verkleinerung' }, + { value: 'NEUGRUENDUNG', label: 'Neugründung' }, + { value: 'FILIALSCHLIESSUNG', label: 'Filialschliessung' }, + ], + }, + { + id: 'nora-set-min-confidence', + label: 'Mindestkonfidenz', + description: + 'Ab diesem Wert gilt ein Signal als verifiziert und darf in Wochen-Mail und Lead-Liste aufgenommen werden.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 70, + unit: '%', + min: 40, + max: 95, + }, + { + id: 'nora-set-review-threshold', + label: 'Review-Schwelle', + description: + 'Signale unterhalb dieses Werts gehen in die Review-Queue und warten auf Sichtung durch die Marktbeobachtung.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.APPROVAL, + value: 55, + unit: '%', + min: 20, + max: 90, + }, + { + id: 'nora-set-lead-handover', + label: 'Lead-Weitergabe aktiv', + description: + 'Legt fest, ob freigegebene Umzugs- und Expansionssignale als Lead an das simulierte CRM übergeben werden.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.APPROVAL, + value: true, + }, + { + id: 'nora-set-weekly-recipients', + label: 'Empfänger der Wochen-Mail', + description: + 'Adressen, die die freigegebene Wochen-Mail erhalten — mehrere Adressen mit Komma trennen.', + kind: AgentSettingKind.TEXT, + group: AgentSettingGroup.DELIVERY, + value: + 'marktbeobachtung@property-on.ch, akquisition.zuerich@property-on.ch, bewirtschaftung.winterthur@property-on.ch', + }, + { + id: 'nora-set-send-time', + label: 'Versandzeit', + description: 'Uhrzeit des wöchentlichen Versands, jeweils dienstags.', + kind: AgentSettingKind.TIME, + group: AgentSettingGroup.DELIVERY, + value: '08:00', + }, + ], + + metrics: [ + { + id: 'nora-metric-weekly-signals', + label: 'Signale der Woche', + value: '128', + hint: 'Erfasste Meldungen aus Quellen und Feeds vom 14.05.2026 bis 20.05.2026', + }, + { + id: 'nora-metric-verified-signals', + label: 'Verifizierte Signale', + value: '86', + hint: 'Konfidenz von mindestens 70 % und mit einer zweiten Quelle bestätigt', + }, + { + id: 'nora-metric-open-reviews', + label: 'Offene Reviews', + value: '19', + hint: 'Signale in der Review-Queue, ältester Eintrag seit 3 Tagen offen', + }, + { + id: 'nora-metric-generated-leads', + label: 'Erzeugte Leads', + value: '23', + hint: 'Freigegebene Umzugs- und Expansionssignale, letzte Übergabe am 19.05.2026', + }, + { + id: 'nora-metric-monitored-regions', + label: 'Überwachte Regionen', + value: '6', + hint: 'Zürich, Winterthur, Zug, Basel, Bern und St. Gallen', + }, + ], + + headlineMetricId: 'nora-metric-weekly-signals', + lastRun: '2026-05-20T06:00:00.000Z', +} diff --git a/src/mock-data/agents/reto.ts b/src/mock-data/agents/reto.ts new file mode 100644 index 0000000..8628f45 --- /dev/null +++ b/src/mock-data/agents/reto.ts @@ -0,0 +1,398 @@ +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +/** + * Reto — Recap-Agent der Vermarktung. + * + * Nimmt Sprachmemos und Kurztexte nach Besichtigungen und Telefonaten entgegen und + * legt daraus ein sauberes Protokoll, einen CRM-Entwurf und Folgeaufgaben vor. + * Geschrieben wird nie ohne die 1-Klick-Freigabe der Bewirtschafterin oder des + * Bewirtschafters. + */ +export const retoAgent: TeamAgent = { + id: 'reto', + name: 'Reto', + role: 'Recap-Agent', + personnelNumber: 'PO-ZD-14', + department: 'Vermarktung', + email: 'reto@property-on.ch', + avatarTone: AgentAvatarTone.SIGNAL, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.APPROVAL_REQUIRED, + shortDescription: 'Macht aus Sprachmemos strukturierte Protokolle, CRM-Einträge und Folgeaufgaben.', + autonomyNote: '«Mit 1-Klick-Freigabe»', + + profile: { + purpose: + 'Reto verwandelt das gesprochene Wort nach einer Besichtigung oder einem Telefonat in ein sauberes Protokoll, einen CRM-Entwurf und konkrete Folgeaufgaben — freigegeben mit einem einzigen Klick.', + input: [ + 'Sprachmemo per WhatsApp direkt vom Objekt, etwa nach einer Besichtigung in Zürich-Altstetten', + 'Telefonaufnahme aus einem Interessentengespräch', + 'Kurztext oder Stichwortnotiz per E-Mail', + 'Termin-Kontext aus dem Kalender: Objekt, Interessent, Uhrzeit, Teilnehmende', + ], + coreFlow: [ + 'Memo entgegennehmen und das Gesprochene in Text übertragen', + 'Füllwörter, Wiederholungen und Nebengeräusche entfernen', + 'Aussagen sortieren nach Feedback, Einwänden, Zusagen und offenen Punkten', + 'Termin-Kontext ergänzen: Objekt, Interessent, Datum, Teilnehmende', + 'Protokoll, CRM-Entwurf und Aufgabenliste als Vorschlag zusammenstellen', + 'Zur 1-Klick-Bestätigung vorlegen und erst danach in die Zielsysteme übertragen', + ], + output: [ + 'Besichtigungsprotokoll als abgelegtes Dokument im Dossier', + 'CRM-Entwurf mit Gesprächsnotiz, Interessentenstatus und nächstem Schritt', + 'Folgeaufgaben mit Fälligkeit und zuständiger Person', + 'Objektbeobachtungen für die Bewirtschaftung, etwa Mängel oder Rückfragen zum Ausbau', + ], + }, + + responsibilities: [ + 'Jedes Gespräch der Vermarktung ist am selben Tag protokolliert und im Dossier auffindbar.', + 'Feedback, Einwände und Zusagen der Interessenten sind strukturiert festgehalten, nicht nur als Fliesstext.', + 'Kein Eintrag gelangt ohne ausdrückliche Freigabe in CRM, Dossier oder Aufgabenliste.', + 'Termine ohne Recap werden aktiv nachgefasst, damit keine Zusage verloren geht.', + ], + + tasks: [ + { + id: 'reto-task-01', + title: 'Sprachmemo oder Kurztext entgegennehmen', + description: + 'Nimmt Sprachmemos aus WhatsApp, Telefonaufnahmen und Kurztexte per E-Mail entgegen und ordnet sie über Zeitpunkt und Objektbezug dem richtigen Termin zu — etwa der Besichtigung an der Technoparkstrasse in Winterthur.', + enabled: true, + schedule: 'bei Eingang', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WHATSAPP, + }, + { + id: 'reto-task-02', + title: 'Sprache transkribieren', + description: + 'Überträgt das Gesprochene in Text, inklusive Schweizer Ortsnamen, Objektbezeichnungen und Fachbegriffen aus der Bewirtschaftung. Unsichere Stellen werden markiert statt geraten.', + enabled: true, + schedule: 'unmittelbar nach Eingang des Memos', + requiresApproval: false, + dependsOnChannel: AgentChannelType.PHONE_VOICE, + }, + { + id: 'reto-task-03', + title: 'Inhalt bereinigen', + description: + 'Entfernt Füllwörter, Versprecher und Wiederholungen, ordnet die Sätze chronologisch und behält dabei jede inhaltliche Aussage unverändert bei.', + enabled: true, + schedule: 'direkt nach der Transkription', + requiresApproval: false, + }, + { + id: 'reto-task-04', + title: 'Feedback, Einwände und Zusagen strukturieren', + description: + 'Trennt das Gespräch in vier Blöcke: positives Feedback zum Objekt, Einwände wie Fläche oder Mietzins, verbindliche Zusagen beider Seiten und offene Fragen. Jeder Punkt behält den Bezug zur Aussage im Memo.', + enabled: true, + schedule: 'nach der Bereinigung', + requiresApproval: false, + }, + { + id: 'reto-task-05', + title: 'Nächste Schritte identifizieren', + description: + 'Leitet aus Zusagen und offenen Fragen die konkreten nächsten Schritte ab — Unterlagen nachreichen, Zweitbesichtigung ansetzen, Mietzins intern klären — je mit Frist und zuständiger Person.', + enabled: true, + schedule: 'nach der Strukturierung', + requiresApproval: false, + }, + { + id: 'reto-task-06', + title: 'CRM- und Protokolleinträge vorschlagen', + description: + 'Stellt den Entwurf für die Gesprächsnotiz im CRM und das Besichtigungsprotokoll für das Objektdossier zusammen. Beides bleibt Vorschlag, bis die Freigabe erteilt ist.', + enabled: true, + schedule: 'nach der Strukturierung', + requiresApproval: true, + dependsOnSystem: AgentSystemType.CRM, + }, + { + id: 'reto-task-07', + title: 'Aufgaben aus dem Gespräch ableiten', + description: + 'Formuliert aus den nächsten Schritten einzelne Aufgaben mit Titel, Fälligkeit und Zuständigkeit — etwa «Grundrisse 3. OG an Interessent senden, bis 22.05.2026». Die Aufgaben werden erst nach Bestätigung angelegt.', + enabled: true, + schedule: 'nach der Strukturierung', + requiresApproval: true, + }, + { + id: 'reto-task-08', + title: 'Ergebnis zur 1-Klick-Bestätigung vorlegen', + description: + 'Legt Protokoll, CRM-Entwurf und Aufgabenliste in einer Übersicht vor. Alles lässt sich vor dem Bestätigen anpassen; ein Klick gibt das gesamte Paket frei.', + enabled: true, + schedule: 'sobald der Entwurf vollständig ist', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WORKSPACE_CHAT, + }, + { + id: 'reto-task-09', + title: 'Nach Bestätigung simuliert ins Zielsystem schreiben', + description: + 'Überträgt das freigegebene Paket simuliert in das eingestellte Zielsystem: Gesprächsnotiz ins CRM, Protokoll ins Dossier, Aufgaben in die Aufgabenliste. Steht kein CRM- oder ERP-Zugang zur Verfügung, greift der eingestellte CRM-/ERP-Fallback und das Paket geht als strukturierte E-Mail an die zuständige Person. Ohne Freigabe passiert nichts.', + enabled: true, + schedule: 'unmittelbar nach der Freigabe', + requiresApproval: true, + dependsOnSystem: AgentSystemType.CRM, + }, + { + id: 'reto-task-10', + title: 'Drei Stunden nach Termin an fehlendes Diktat erinnern', + description: + 'Prüft nach jedem Vermarktungstermin im Kalender, ob ein Memo eingegangen ist. Fehlt es, folgt eine kurze Erinnerung mit Objekt und Interessent auf demselben Weg, auf dem sonst diktiert wird.', + enabled: true, + schedule: 'drei Stunden nach Terminende', + requiresApproval: false, + dependsOnSystem: AgentSystemType.CALENDAR, + }, + ], + + channels: [ + { + id: 'reto-ch-whatsapp', + type: AgentChannelType.WHATSAPP, + direction: AgentChannelDirection.INBOUND, + description: + 'Hauptweg für Sprachmemos direkt nach der Besichtigung — diktiert im Auto oder im Treppenhaus, ganz ohne Formular.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Reto Diktat (WhatsApp)', + inboxAddress: '+41 44 500 21 14', + defaultRecipients: ['vermarktung@property-on.ch'], + autoReplyEnabled: true, + }, + }, + { + id: 'reto-ch-phone', + type: AgentChannelType.PHONE_VOICE, + direction: AgentChannelDirection.INBOUND, + description: + 'Diktatnummer für längere Gesprächsnotizen und Telefonaufnahmen aus Interessentengesprächen.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Reto Diktatlinie', + inboxAddress: '+41 44 500 21 15', + defaultRecipients: ['vermarktung@property-on.ch'], + autoReplyEnabled: false, + }, + }, + { + id: 'reto-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.BOTH, + description: + 'Nimmt Kurztexte und Stichwortnotizen entgegen und versendet das fertige Protokoll sowie den strukturierten Fallback.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Reto — Recap Vermarktung', + senderAddress: 'reto@property-on.ch', + inboxAddress: 'recap@property-on.ch', + defaultRecipients: ['vermarktung@property-on.ch', 'bewirtschaftung@property-on.ch'], + autoReplyEnabled: true, + }, + }, + { + id: 'reto-ch-chat', + type: AgentChannelType.WORKSPACE_CHAT, + direction: AgentChannelDirection.OUTBOUND, + description: + 'Legt die Freigabekarte mit Protokoll, CRM-Entwurf und Aufgaben im Arbeitsbereich vor und erinnert dort an fehlende Diktate.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Property On — Vermarktung', + defaultRecipients: ['#vermarktung-zuerich', '#vermarktung-winterthur'], + autoReplyEnabled: false, + }, + }, + ], + + systems: [ + { + id: 'reto-sys-crm', + type: AgentSystemType.CRM, + access: AgentAccessLevel.WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Legt Gesprächsnotiz, Interessentenstatus und nächsten Schritt beim passenden Kontakt und Objekt ab.', + permissionNote: + 'Schreibt ausschliesslich nach der 1-Klick-Freigabe — und in dieser Ausbaustufe nur simuliert. Ohne Bestätigung entsteht kein Eintrag.', + }, + { + id: 'reto-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest Objekt- und Flächendaten, damit Protokoll und Notiz die richtige Liegenschaft, Einheit und Fläche nennen.', + permissionNote: 'Nur lesender Zugang. Reto verändert keine Objekt- oder Vertragsdaten.', + }, + { + id: 'reto-sys-dms', + type: AgentSystemType.DMS, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Liest bestehende Protokolle desselben Objekts und legt das freigegebene Besichtigungsprotokoll im Objektdossier ab.', + permissionNote: + 'Ablage erfolgt erst nach Freigabe und nur im Dossier des betroffenen Objekts. Bestehende Dokumente werden nie überschrieben.', + }, + { + id: 'reto-sys-calendar', + type: AgentSystemType.CALENDAR, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Holt den Termin-Kontext: Objekt, Interessent, Uhrzeit und Teilnehmende — und erkennt Termine ohne Recap.', + permissionNote: 'Nur lesender Zugang. Es werden keine Termine erstellt, verschoben oder abgesagt.', + }, + { + id: 'reto-sys-exchange', + type: AgentSystemType.M365_EXCHANGE, + access: AgentAccessLevel.READ_WRITE, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Empfängt Kurztexte aus dem Postfach und versendet Protokoll sowie strukturierten Fallback an die zuständige Person.', + permissionNote: + 'Versand nur an interne Adressen der eigenen Organisation und erst nach Freigabe. Interessenten erhalten nie automatisch eine Nachricht.', + }, + ], + + settings: [ + { + id: 'reto-set-structure', + label: 'Standardstruktur des Recaps', + description: 'Welche Blöcke jedes Protokoll enthält. Leere Blöcke werden im Ergebnis ausgeblendet.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['SUMMARY', 'FEEDBACK', 'OBJECTIONS', 'COMMITMENTS', 'NEXT_STEPS', 'OBJECT_NOTES'], + options: [ + { value: 'SUMMARY', label: 'Kurzfazit in zwei Sätzen' }, + { value: 'FEEDBACK', label: 'Feedback zum Objekt' }, + { value: 'OBJECTIONS', label: 'Einwände und Bedenken' }, + { value: 'COMMITMENTS', label: 'Zusagen beider Seiten' }, + { value: 'NEXT_STEPS', label: 'Nächste Schritte' }, + { value: 'OBJECT_NOTES', label: 'Objektbeobachtungen für die Bewirtschaftung' }, + { value: 'PARTICIPANTS', label: 'Teilnehmende und Rollen' }, + { value: 'BUDGET', label: 'Mietzins- und Kostenthemen' }, + ], + }, + { + id: 'reto-set-crm-target', + label: 'CRM-Zielsystem', + description: 'Wohin die freigegebene Gesprächsnotiz übertragen wird.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.DELIVERY, + value: 'IMMOTOP2_CRM', + options: [ + { value: 'IMMOTOP2_CRM', label: 'ImmoTop2 — Interessentenbetreuung' }, + { value: 'ABACUS_ABAIMMO', label: 'Abacus AbaImmo' }, + { value: 'RIMO_R5', label: 'Rimo R5' }, + { value: 'GARAIO_REM', label: 'Garaio REM' }, + { value: 'INTERNAL_CRM', label: 'Property On — internes Kontaktjournal' }, + ], + }, + { + id: 'reto-set-fallback', + label: 'Fallback-Ziel', + description: + 'Was geschieht, wenn das CRM- oder ERP-System keinen Zugang für den Eintrag bereitstellt oder nicht erreichbar ist.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.DELIVERY, + value: 'STRUCTURED_MAIL', + options: [ + { value: 'STRUCTURED_MAIL', label: 'Strukturierter Mail-Fallback an die zuständige Person' }, + { value: 'DMS_DOSSIER', label: 'Ablage als Protokoll im Objektdossier' }, + { value: 'WORKSPACE_CARD', label: 'Karte im Arbeitsbereich der Vermarktung' }, + { value: 'HOLD', label: 'Zurückhalten und erneut zur Bestätigung vorlegen' }, + ], + }, + { + id: 'reto-set-approval-locked', + label: 'Freigabe zwingend aktiv', + description: 'Jedes Ergebnis wird vor der Übertragung zur Bestätigung vorgelegt.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.APPROVAL, + value: true, + locked: true, + lockedReason: + 'Fachlich nicht abschaltbar: Ohne erteilte Freigabe schreibt Reto nie in ein Zielsystem — weder ins CRM noch ins Dossier noch in die Aufgabenliste.', + }, + { + id: 'reto-set-reminder-hours', + label: 'Erinnerung nach Termin', + description: 'Nach wie vielen Stunden ohne Diktat eine Erinnerung ausgelöst wird.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.SPECIFIC, + value: 3, + unit: 'Stunden', + min: 1, + max: 24, + }, + { + id: 'reto-set-auto-tasks', + label: 'Aufgaben automatisch vorschlagen', + description: + 'Leitet aus jedem Gespräch Folgeaufgaben mit Frist und Zuständigkeit ab. Angelegt werden sie erst nach der Freigabe.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.GENERAL, + value: true, + }, + ], + + metrics: [ + { + id: 'reto-metric-open-approvals', + label: 'Offene Bestätigungen', + value: '4', + hint: 'Fertige Recaps, die auf die 1-Klick-Freigabe warten — älteste seit gestern 16:20.', + }, + { + id: 'reto-metric-processed', + label: 'Verarbeitete Diktate', + value: '128', + hint: 'Seit dem 01.05.2026 entgegengenommene Sprachmemos und Kurztexte.', + }, + { + id: 'reto-metric-duration', + label: 'Durchschnittliche Verarbeitungszeit', + value: '2 Min. 40 Sek.', + hint: 'Vom Eingang des Memos bis zum vorgelegten Recap.', + }, + { + id: 'reto-metric-tasks-created', + label: 'Erstellte Aufgaben', + value: '96', + hint: 'Nach Freigabe angelegte Folgeaufgaben, davon 11 aktuell überfällig.', + }, + { + id: 'reto-metric-missing-recaps', + label: 'Termine ohne Recap', + value: '3', + hint: 'Vermarktungstermine der letzten sieben Tage ohne eingegangenes Diktat.', + }, + ], + + headlineMetricId: 'reto-metric-open-approvals', + lastRun: '2026-05-20T16:42:00.000Z', +} diff --git a/src/mock-data/agents/sina.ts b/src/mock-data/agents/sina.ts new file mode 100644 index 0000000..1fcf462 --- /dev/null +++ b/src/mock-data/agents/sina.ts @@ -0,0 +1,331 @@ +/** + * Property On — Personalblatt «Sina», Vertragsauskunft (Abteilung Bewirtschaftung). + * + * Fachliche Wahrheit aus dem Agenten-Katalog: Rolle, Personalnummer, E-Mail und die + * sieben Aufgaben werden unverändert übernommen. Sina arbeitet ausschliesslich lesend + * und belegt jede Antwort mit einer Fundstelle. Zeitanker der Mockdaten: 20.05.2026. + */ + +import type { TeamAgent } from '../../domain/teamAgent' +import { + AgentStatus, + AgentAutonomyLevel, + AgentChannelType, + AgentChannelDirection, + AgentConnectionStatus, + AgentSystemType, + AgentAccessLevel, + AgentSettingKind, + AgentSettingGroup, + AgentAvatarTone, +} from '../../domain/teamAgent' + +export const sinaAgent: TeamAgent = { + id: 'sina', + name: 'Sina', + role: 'Vertragsauskunft', + personnelNumber: 'PO-ZD-03', + department: 'Bewirtschaftung', + email: 'sina@property-on.ch', + avatarTone: AgentAvatarTone.INFO, + status: AgentStatus.ACTIVE, + autonomyLevel: AgentAutonomyLevel.AUTONOMOUS, + shortDescription: + 'Beantwortet Vertrags- und Objektfragen ausschliesslich aus hinterlegten Dokumenten und immer mit Fundstelle.', + autonomyNote: '«Autonom, ausschliesslich lesend»', + + profile: { + purpose: + 'Sina gibt dem Bewirtschaftungsteam in Sekunden eine belegte Auskunft zu Vertrags- und Objektfragen, statt dass jemand Ordner und Ablagen durchsuchen muss.', + input: [ + 'Mietverträge der kommerziell genutzten Immobilien in Zürich, Winterthur und Basel', + 'Nachträge, Zusatzvereinbarungen und Indexklauseln', + 'Abnahme-, Übergabe- und Sitzungsprotokolle', + 'Grundriss- und Flächenpläne inklusive Flächenausweise', + 'Korrespondenz mit Mieterschaft, Eigentümerschaft und Behörden', + ], + coreFlow: [ + 'Neue und geänderte Dokumente aus den freigegebenen Ablagen werden laufend erfasst und durchsuchbar gemacht.', + 'Eine Frage trifft über den Teamkanal oder per E-Mail ein.', + 'Sina sucht die massgebenden Stellen in den erlaubten Dokumentquellen.', + 'Die Antwort geht mit wörtlichem Zitat, Dokumentname und Seitenzahl zurück.', + 'Findet sie keine belegte Stelle, antwortet sie «nicht dokumentiert» und meldet die Lücke.', + 'Jede Auskunft wird mit Frage, Quelle und Zeitpunkt protokolliert.', + ], + output: [ + 'Antwort mit Direktzitat aus dem Originaldokument', + 'Fundstelle mit Dokumentname und Seitenzahl', + 'Hinweis «nicht dokumentiert», wenn keine Quelle vorliegt', + 'Meldungen zu erkannten Dokumentationslücken', + 'Monatlicher Lückenreport je Liegenschaft', + ], + }, + + responsibilities: [ + 'Belegte Auskunft zu Verträgen, Nachträgen und Objektunterlagen', + 'Nachweis der Fundstelle bei jeder einzelnen Antwort', + 'Aktuelles, durchsuchbares Verzeichnis der Objektdokumente', + 'Transparenz über fehlende oder unvollständige Unterlagen', + 'Lückenlose Protokollierung sämtlicher Auskünfte', + ], + + tasks: [ + { + id: 'sina-task-01', + title: 'Objektdokumente laufend indexieren', + description: + 'Erfasst neue und geänderte Verträge, Nachträge, Protokolle und Pläne aus den freigegebenen Ablagen und hält sie durchsuchbar. Genutzt werden ausschliesslich Leserechte — es wird nichts abgelegt, verschoben oder verändert.', + enabled: true, + schedule: 'bei Dokumenteingang, zusätzlich täglich 05:30', + requiresApproval: false, + dependsOnSystem: AgentSystemType.DMS, + }, + { + id: 'sina-task-02', + title: 'Vertrags- und Objektfragen beantworten', + description: + 'Beantwortet Fragen zu Laufzeiten, Kündigungsfristen, Indexierung, Nebenkosten, Flächen und Zuständigkeiten allein auf Basis der hinterlegten Dokumente. Eigene Annahmen oder Schätzungen sind ausgeschlossen.', + enabled: true, + schedule: 'bei Eingang der Frage', + requiresApproval: false, + dependsOnChannel: AgentChannelType.WORKSPACE_CHAT, + }, + { + id: 'sina-task-03', + title: 'Antworten immer mit Zitat und Fundstelle ausgeben', + description: + 'Jede Antwort enthält das wörtliche Zitat der massgebenden Stelle sowie Dokumentname, Stand und Seitenzahl. Damit ist jede Auskunft im Original nachprüfbar — der Quellenzwang gilt ohne Ausnahme.', + enabled: true, + schedule: 'bei jeder Antwort', + requiresApproval: false, + }, + { + id: 'sina-task-04', + title: 'bei fehlender Quelle «nicht dokumentiert» anzeigen', + description: + 'Lässt sich eine Frage nicht aus einem hinterlegten Dokument belegen, gibt Sina «nicht dokumentiert» aus und nennt, welche Unterlage fehlt. Eine Antwort ohne Fundstelle wird nie ausgegeben.', + enabled: true, + schedule: 'sobald keine belegte Fundstelle vorliegt', + requiresApproval: false, + }, + { + id: 'sina-task-05', + title: 'Dokumentationslücken aktiv melden', + description: + 'Meldet dem zuständigen Bewirtschaftungsteam, zu welchen Liegenschaften Unterlagen fehlen, veraltet sind oder ohne Unterschrift abgelegt wurden — etwa fehlende Nachträge am Standort Winterthur.', + enabled: true, + schedule: 'montags 07:00', + requiresApproval: false, + }, + { + id: 'sina-task-06', + title: 'sämtliche Auskünfte protokollieren', + description: + 'Hält Frage, Antwort, verwendete Fundstelle, Kanal und Zeitpunkt in einem nachvollziehbaren Protokoll fest. Das Protokoll ist die Grundlage für Rückfragen aus Bewirtschaftung und Revision.', + enabled: true, + schedule: 'bei jeder Auskunft', + requiresApproval: false, + }, + { + id: 'sina-task-07', + title: 'monatlichen Lückenreport erstellen', + description: + 'Fasst alle offenen Dokumentationslücken je Liegenschaft und Portfolio zusammen und stellt den Report der Bewirtschaftung in Zürich, Winterthur und Basel zu.', + enabled: true, + schedule: 'monatlich am 1. um 06:00', + requiresApproval: false, + dependsOnChannel: AgentChannelType.EMAIL, + }, + ], + + channels: [ + { + id: 'sina-ch-workspace-chat', + type: AgentChannelType.WORKSPACE_CHAT, + direction: AgentChannelDirection.BOTH, + description: + 'Hauptkanal für Sofortfragen aus der Bewirtschaftung. Die Antwort erscheint direkt im Teamkanal, immer mit Direktzitat und Fundstelle.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Sina — Vertragsauskunft', + defaultRecipients: ['bewirtschaftung-zuerich', 'bewirtschaftung-winterthur'], + autoReplyEnabled: true, + }, + }, + { + id: 'sina-ch-email', + type: AgentChannelType.EMAIL, + direction: AgentChannelDirection.BOTH, + description: + 'Fragen per E-Mail sowie Versand von Lückenmeldungen und des monatlichen Lückenreports an die Standorte Zürich, Winterthur und Basel.', + status: AgentConnectionStatus.CONNECTED, + enabled: true, + config: { + displayName: 'Sina — Vertragsauskunft', + senderAddress: 'sina@property-on.ch', + inboxAddress: 'sina@property-on.ch', + defaultRecipients: [ + 'bewirtschaftung.zuerich@property-on.ch', + 'bewirtschaftung.winterthur@property-on.ch', + 'bewirtschaftung.basel@property-on.ch', + ], + autoReplyEnabled: true, + }, + }, + ], + + systems: [ + { + id: 'sina-sys-dms', + type: AgentSystemType.DMS, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Führende Ablage für Mietverträge, Nachträge und Protokolle der Seefeld Immobilien AG, Zürich — Grundlage für Zitat und Seitenzahl.', + permissionNote: + 'Nur Leserecht. Sina liest Dokumente und Versionsstände; sie legt nichts ab, ändert nichts und löscht nichts.', + }, + { + id: 'sina-sys-sharepoint', + type: AgentSystemType.SHAREPOINT, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Objektbibliotheken der Winterthurer Liegenschaften AG mit Übergabeprotokollen, Flächenausweisen und Sitzungsunterlagen.', + permissionNote: + 'Nur Leserecht. Sina öffnet die Bibliotheken ausschliesslich lesend und schreibt dort unter keinen Umständen zurück.', + }, + { + id: 'sina-sys-onedrive', + type: AgentSystemType.ONEDRIVE, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Ausdrücklich freigegebene Arbeitsordner einzelner Bewirtschafterinnen und Bewirtschafter mit noch nicht abgelegter Korrespondenz.', + permissionNote: + 'Nur Leserecht auf freigegebene Ordner. Sina erstellt, ändert und verschiebt keine Dateien.', + }, + { + id: 'sina-sys-fileshare', + type: AgentSystemType.FILESHARE, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Historische Laufwerksablage der Rheinpark Bewirtschaftung AG, Basel — Altverträge und Pläne aus der Zeit vor der Übernahme ins Dokumentensystem.', + permissionNote: + 'Nur Leserecht. Sina greift ausschliesslich lesend auf die Laufwerke zu; ein Schreibzugriff ist fachlich wie technisch ausgeschlossen.', + }, + { + id: 'sina-sys-immotop2', + type: AgentSystemType.IMMOTOP2, + access: AgentAccessLevel.READ, + status: AgentConnectionStatus.CONNECTED, + usage: + 'Stammdaten zu Liegenschaften, Mietobjekten und Vertragsbezügen, um die richtige Akte und den gültigen Vertragsstand zu finden.', + permissionNote: + 'Nur Leserecht. Sina liest Stammdaten und verknüpfte Belege; sie erfasst und mutiert keine Datensätze im Bewirtschaftungssystem.', + }, + ], + + settings: [ + { + id: 'sina-set-sources', + label: 'Erlaubte Dokumentquellen', + description: + 'Nur Dokumente aus diesen Ablagen werden für Antworten und Fundstellen verwendet.', + kind: AgentSettingKind.MULTI_SELECT, + group: AgentSettingGroup.SPECIFIC, + value: ['DMS', 'SHAREPOINT', 'FILESHARE', 'IMMOTOP2'], + options: [ + { value: 'DMS', label: 'Dokumentensystem' }, + { value: 'SHAREPOINT', label: 'SharePoint-Objektbibliotheken' }, + { value: 'ONEDRIVE', label: 'Freigegebene OneDrive-Ordner' }, + { value: 'FILESHARE', label: 'Laufwerksablage' }, + { value: 'IMMOTOP2', label: 'ImmoTop2-Belege' }, + ], + }, + { + id: 'sina-set-source-requirement', + label: 'Quellenzwang', + description: + 'Jede Antwort muss auf eine belegte Fundstelle in einem hinterlegten Dokument zurückgehen.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.SPECIFIC, + value: true, + locked: true, + lockedReason: + 'Nicht abschaltbar: Eine Vertragsauskunft ohne Fundstelle ist fachlich unzulässig, weil sie weder nachprüfbar noch gegenüber Eigentümerschaft und Revision belegbar wäre.', + }, + { + id: 'sina-set-max-length', + label: 'Maximale Antwortlänge', + description: + 'Obergrenze für den Antworttext ohne Zitat und Fundstelle — hält Auskünfte knapp und lesbar.', + kind: AgentSettingKind.NUMBER, + group: AgentSettingGroup.DELIVERY, + value: 1200, + unit: 'Zeichen', + min: 300, + max: 4000, + }, + { + id: 'sina-set-direct-quote', + label: 'Ausgabe mit Direktzitat', + description: + 'Die massgebende Stelle wird zusätzlich zu Dokumentname und Seitenzahl wörtlich zitiert.', + kind: AgentSettingKind.BOOLEAN, + group: AgentSettingGroup.DELIVERY, + value: true, + }, + { + id: 'sina-set-missing-doc', + label: 'Verhalten bei fehlender Dokumentation', + description: 'Legt fest, wie Sina reagiert, wenn keine belegte Fundstelle vorliegt.', + kind: AgentSettingKind.SELECT, + group: AgentSettingGroup.SPECIFIC, + value: 'NOT_DOCUMENTED', + options: [ + { value: 'NOT_DOCUMENTED', label: '«nicht dokumentiert» anzeigen' }, + { value: 'ASK_BACK', label: 'Rückfrage stellen' }, + { value: 'REPORT_GAP', label: 'Lücke melden' }, + ], + }, + ], + + metrics: [ + { + id: 'sina-metric-documents', + label: 'Indexierte Dokumente', + value: '18 420', + hint: 'Durchsuchbare Verträge, Nachträge, Protokolle und Pläne per 20.05.2026', + }, + { + id: 'sina-metric-questions', + label: 'Beantwortete Fragen', + value: '312', + hint: 'Auskünfte über Teamkanal und E-Mail in den letzten 30 Tagen', + }, + { + id: 'sina-metric-response-time', + label: 'Durchschnittliche Antwortzeit', + value: '14 Sekunden', + hint: 'Von der Frage bis zur Antwort mit Fundstelle', + }, + { + id: 'sina-metric-gaps', + label: 'Erkannte Dokumentationslücken', + value: '27', + hint: 'Offene Lücken aus dem Report vom 01.05.2026, davon 9 in Winterthur', + }, + { + id: 'sina-metric-sourced', + label: 'Antworten mit Fundstelle', + value: '100 %', + hint: 'Quellenzwang ist gesperrt — Antworten ohne Fundstelle sind nicht möglich', + }, + ], + + headlineMetricId: 'sina-metric-questions', + lastRun: '2026-05-20T06:00:00.000Z', +} diff --git a/src/mock-data/teamAgents.ts b/src/mock-data/teamAgents.ts new file mode 100644 index 0000000..7feb646 --- /dev/null +++ b/src/mock-data/teamAgents.ts @@ -0,0 +1,40 @@ +/** + * Property On — Kernteam der sieben digitalen Mitarbeiter. + * + * Reihenfolge alphabetisch nach Vorname (§7.2 der Spezifikation): Bruno, Ferdi, + * Lea, Livia, Nora, Reto, Sina. Die Agentenliste in der Personalverwaltung + * rendert genau diese Reihenfolge, ohne selbst zu sortieren. + */ + +import type { TeamAgent } from '../domain/teamAgent' +import { brunoAgent } from './agents/bruno' +import { ferdiAgent } from './agents/ferdi' +import { leaAgent } from './agents/lea' +import { liviaAgent } from './agents/livia' +import { noraAgent } from './agents/nora' +import { retoAgent } from './agents/reto' +import { sinaAgent } from './agents/sina' + +export const mockTeamAgents: TeamAgent[] = [ + brunoAgent, + ferdiAgent, + leaAgent, + liviaAgent, + noraAgent, + retoAgent, + sinaAgent, +] + +/** Stabile IDs — Mockdaten anderer Bestände referenzieren ausschliesslich diese. */ +export const AGENT_IDS = { + BRUNO: 'bruno', + FERDI: 'ferdi', + LEA: 'lea', + LIVIA: 'livia', + NORA: 'nora', + RETO: 'reto', + SINA: 'sina', +} as const + +/** Zeitanker sämtlicher Property-On-Mockdaten. */ +export const MOCK_TODAY = '2026-05-20T09:00:00.000Z' diff --git a/src/provider/IAgentConnectionProvider.ts b/src/provider/IAgentConnectionProvider.ts new file mode 100644 index 0000000..7a56dde --- /dev/null +++ b/src/provider/IAgentConnectionProvider.ts @@ -0,0 +1,16 @@ +import type { + AgentConnection, + AgentConnectionTestResult, + CreateAgentConnectionInput, +} from '../domain/agentConnection' + +export interface IAgentConnectionProvider { + getAll(): Promise + getById(id: string): Promise + /** Simulierter Verbindungsaufbau aus dem mehrstufigen Assistenten (§13.5). */ + connect(input: CreateAgentConnectionInput): Promise + disconnect(id: string): Promise + /** Deterministisches Testergebnis — kein Zufall (§19.2). */ + test(id: string): Promise + reset(): Promise +} diff --git a/src/provider/IAgentProtocolProvider.ts b/src/provider/IAgentProtocolProvider.ts new file mode 100644 index 0000000..4f47388 --- /dev/null +++ b/src/provider/IAgentProtocolProvider.ts @@ -0,0 +1,27 @@ +import type { + AgentProtocolEntry, + AgentProtocolEventType, + AgentProtocolStatus, + CreateAgentProtocolEntryInput, +} from '../domain/agentProtocol' +import type { AgentChannelType } from '../domain/teamAgent' +import type { AgentPeriod } from '../domain/agentFilters' + +/** Filterdimensionen des Protokoll-Tabs (§12.3). */ +export interface AgentProtocolFilters { + agentId?: string + period?: AgentPeriod + eventType?: AgentProtocolEventType + status?: AgentProtocolStatus + objectId?: string + channel?: AgentChannelType + /** Nur Einträge mit Freigabeentscheidung. */ + onlyApprovals?: boolean + search?: string +} + +export interface IAgentProtocolProvider { + getAll(filters?: AgentProtocolFilters): Promise + create(entry: CreateAgentProtocolEntryInput): Promise + reset(): Promise +} diff --git a/src/provider/IAgentWorkItemProvider.ts b/src/provider/IAgentWorkItemProvider.ts new file mode 100644 index 0000000..fd9f0af --- /dev/null +++ b/src/provider/IAgentWorkItemProvider.ts @@ -0,0 +1,32 @@ +import type { + AgentWorkItem, + AgentWorkItemKind, + AgentWorkItemPriority, + AgentWorkItemStatus, + AgentDomainArea, + UpdateAgentWorkItemInput, +} from '../domain/agentWorkItem' +import type { AgentChannelType } from '../domain/teamAgent' +import type { AgentPeriod, AgentWorkItemSort } from '../domain/agentFilters' + +/** Filterdimensionen des Bearbeitungsverlaufs (§5.3). */ +export interface AgentWorkItemFilters { + /** true → «Pendente Anfragen», false → «Erledigte Aufträge», undefined → alle. */ + requiresDecision?: boolean + agentId?: string + area?: AgentDomainArea + kind?: AgentWorkItemKind + priority?: AgentWorkItemPriority + channel?: AgentChannelType + status?: AgentWorkItemStatus + period?: AgentPeriod + search?: string + sort?: AgentWorkItemSort +} + +export interface IAgentWorkItemProvider { + getAll(filters?: AgentWorkItemFilters): Promise + getById(id: string): Promise + update(id: string, data: UpdateAgentWorkItemInput): Promise + reset(): Promise +} diff --git a/src/provider/ITeamAgentProvider.ts b/src/provider/ITeamAgentProvider.ts new file mode 100644 index 0000000..6b58acb --- /dev/null +++ b/src/provider/ITeamAgentProvider.ts @@ -0,0 +1,9 @@ +import type { TeamAgent, UpdateTeamAgentInput } from '../domain/teamAgent' + +export interface ITeamAgentProvider { + getAll(): Promise + getById(id: string): Promise + update(id: string, data: UpdateTeamAgentInput): Promise + /** Stellt den Auslieferungszustand her («Demo zurücksetzen», §16.2). */ + reset(): Promise +} diff --git a/src/provider/MockupAgentConnectionProvider.ts b/src/provider/MockupAgentConnectionProvider.ts new file mode 100644 index 0000000..8498253 --- /dev/null +++ b/src/provider/MockupAgentConnectionProvider.ts @@ -0,0 +1,119 @@ +import type { IAgentConnectionProvider } from './IAgentConnectionProvider' +import type { + AgentConnection, + AgentConnectionTestResult, + CreateAgentConnectionInput, +} from '../domain/agentConnection' +import { AgentConnectionCategory } from '../domain/agentConnection' +import { AgentConnectionStatus } from '../domain/teamAgent' +import { mockAgentConnections } from '../mock-data/agentConnections' +import { + TEAM_STORAGE_KEYS, + loadVersioned, + persistVersioned, + simulateLatency, +} from './teamPersistence' +import { teamNowIso } from '../lib/teamClock' +import { AGENT_SIM_DELAY_MS, AGENT_SIM_DELAY_SLOW_MS } from '../lib/constants' + +function seed(): AgentConnection[] { + return loadVersioned(TEAM_STORAGE_KEYS.CONNECTIONS) ?? structuredClone(mockAgentConnections) +} + +let store: AgentConnection[] = seed() + +function save(): void { + persistVersioned(TEAM_STORAGE_KEYS.CONNECTIONS, store) +} + +/** + * Feste Testszenarien statt Zufall (§19.2): eine unverbundene und eine geplante + * Verbindung schlagen reproduzierbar fehl, damit der Fehlerzustand in der Demo + * gezeigt werden kann, ohne dass die Oberfläche unvorhersehbar wird. + */ +function evaluateTest(connection: AgentConnection): AgentConnectionTestResult { + const checkedAt = teamNowIso() + if (connection.status === AgentConnectionStatus.ROADMAP) { + return { + ok: false, + message: 'Diese Anbindung ist geplant und steht in der Demo noch nicht zur Verfügung.', + checkedAt, + } + } + if (connection.status === AgentConnectionStatus.DISCONNECTED) { + return { + ok: false, + message: 'Es ist keine Verbindung hinterlegt. Zuerst über «Verbinden» einrichten.', + checkedAt, + } + } + return { + ok: true, + message: 'Verbindung erreichbar. Simulierter Test, es wurde kein echtes System kontaktiert.', + checkedAt, + } +} + +export const MockupAgentConnectionProvider: IAgentConnectionProvider = { + async getAll() { + return structuredClone(store) + }, + + async getById(id) { + const found = store.find(c => c.id === id) + return found ? structuredClone(found) : null + }, + + async connect(input: CreateAgentConnectionInput) { + await simulateLatency(AGENT_SIM_DELAY_SLOW_MS) + const idx = store.findIndex(c => c.id === input.connectionId) + if (idx === -1) throw new Error(`Verbindung «${input.connectionId}» nicht gefunden.`) + + const current = store[idx] + store[idx] = { + ...current, + status: AgentConnectionStatus.CONNECTED, + accountCount: Math.max(1, current.accountCount), + connectionLabel: input.connectionLabel, + usedByAgentIds: input.agentIds, + lastSyncAt: teamNowIso(), + ...(current.category === AgentConnectionCategory.ERP && input.vendor + ? { vendor: input.vendor } + : {}), + permissions: current.permissions.map(p => ({ + ...p, + granted: input.permissionIds.includes(p.id), + })), + } + save() + return structuredClone(store[idx]) + }, + + async disconnect(id) { + await simulateLatency(AGENT_SIM_DELAY_MS) + const idx = store.findIndex(c => c.id === id) + if (idx === -1) throw new Error(`Verbindung «${id}» nicht gefunden.`) + + store[idx] = { + ...store[idx], + status: AgentConnectionStatus.DISCONNECTED, + accountCount: 0, + lastSyncAt: undefined, + connectionLabel: undefined, + permissions: store[idx].permissions.map(p => ({ ...p, granted: false })), + } + save() + return structuredClone(store[idx]) + }, + + async test(id) { + await simulateLatency(AGENT_SIM_DELAY_SLOW_MS) + const found = store.find(c => c.id === id) + if (!found) throw new Error(`Verbindung «${id}» nicht gefunden.`) + return evaluateTest(found) + }, + + async reset() { + store = structuredClone(mockAgentConnections) + }, +} diff --git a/src/provider/MockupAgentProtocolProvider.ts b/src/provider/MockupAgentProtocolProvider.ts new file mode 100644 index 0000000..4064948 --- /dev/null +++ b/src/provider/MockupAgentProtocolProvider.ts @@ -0,0 +1,56 @@ +import type { IAgentProtocolProvider, AgentProtocolFilters } from './IAgentProtocolProvider' +import type { AgentProtocolEntry, CreateAgentProtocolEntryInput } from '../domain/agentProtocol' +import { AgentPeriod, isWithinPeriod } from '../domain/agentFilters' +import { mockAgentProtocol } from '../mock-data/agentProtocol' +import { TEAM_STORAGE_KEYS, loadVersioned, persistVersioned } from './teamPersistence' +import { teamNow } from '../lib/teamClock' + +function seed(): AgentProtocolEntry[] { + return loadVersioned(TEAM_STORAGE_KEYS.PROTOCOL) ?? structuredClone(mockAgentProtocol) +} + +let store: AgentProtocolEntry[] = seed() + +function save(): void { + persistVersioned(TEAM_STORAGE_KEYS.PROTOCOL, store) +} + +export const MockupAgentProtocolProvider: IAgentProtocolProvider = { + async getAll(filters?: AgentProtocolFilters) { + let results = store + + if (filters?.agentId) results = results.filter(e => e.agentId === filters.agentId) + if (filters?.eventType) results = results.filter(e => e.eventType === filters.eventType) + if (filters?.status) results = results.filter(e => e.status === filters.status) + if (filters?.objectId) results = results.filter(e => e.objectId === filters.objectId) + if (filters?.channel) results = results.filter(e => e.channel === filters.channel) + if (filters?.onlyApprovals) results = results.filter(e => e.approvalDecision !== undefined) + if (filters?.period && filters.period !== AgentPeriod.ALL) { + const now = teamNow() + results = results.filter(e => isWithinPeriod(e.timestamp, filters.period!, now)) + } + if (filters?.search) { + const q = filters.search.trim().toLowerCase() + results = results.filter( + e => + e.title.toLowerCase().includes(q) || + e.description.toLowerCase().includes(q) || + (e.objectId?.toLowerCase().includes(q) ?? false), + ) + } + + // Neueste zuerst — das Protokoll wird immer von oben gelesen. + return structuredClone([...results].sort((a, b) => b.timestamp.localeCompare(a.timestamp))) + }, + + async create(entry: CreateAgentProtocolEntryInput) { + const next: AgentProtocolEntry = { id: crypto.randomUUID(), ...entry } + store = [next, ...store] + save() + return structuredClone(next) + }, + + async reset() { + store = structuredClone(mockAgentProtocol) + }, +} diff --git a/src/provider/MockupAgentWorkItemProvider.ts b/src/provider/MockupAgentWorkItemProvider.ts new file mode 100644 index 0000000..62e173f --- /dev/null +++ b/src/provider/MockupAgentWorkItemProvider.ts @@ -0,0 +1,131 @@ +import type { IAgentWorkItemProvider, AgentWorkItemFilters } from './IAgentWorkItemProvider' +import type { AgentWorkItem, UpdateAgentWorkItemInput } from '../domain/agentWorkItem' +import { AgentWorkItemPriority } from '../domain/agentWorkItem' +import { AgentPeriod, AgentWorkItemSort, isWithinPeriod } from '../domain/agentFilters' +import { mockAgentWorkItems } from '../mock-data/agentWorkItems' +import { + TEAM_STORAGE_KEYS, + loadVersioned, + persistVersioned, + simulateLatency, +} from './teamPersistence' +import { teamNow } from '../lib/teamClock' +import { AGENT_SIM_DELAY_MS } from '../lib/constants' + +const PRIORITY_RANK: Record = { + [AgentWorkItemPriority.CRITICAL]: 0, + [AgentWorkItemPriority.HIGH]: 1, + [AgentWorkItemPriority.MEDIUM]: 2, + [AgentWorkItemPriority.LOW]: 3, +} + +function seed(): AgentWorkItem[] { + return loadVersioned(TEAM_STORAGE_KEYS.WORK_ITEMS) ?? structuredClone(mockAgentWorkItems) +} + +let store: AgentWorkItem[] = seed() + +function save(): void { + persistVersioned(TEAM_STORAGE_KEYS.WORK_ITEMS, store) +} + +/** Volltextsuche über die Felder, die ein Bewirtschafter tatsächlich im Kopf hat. */ +function matchesSearch(item: AgentWorkItem, query: string): boolean { + const q = query.trim().toLowerCase() + if (!q) return true + return ( + item.title.toLowerCase().includes(q) || + item.summary.toLowerCase().includes(q) || + (item.objectId?.toLowerCase().includes(q) ?? false) || + (item.objectLabel?.toLowerCase().includes(q) ?? false) || + (item.escalationReason?.toLowerCase().includes(q) ?? false) + ) +} + +function sortItems(items: AgentWorkItem[], sort: AgentWorkItemSort): AgentWorkItem[] { + const sorted = [...items] + switch (sort) { + case AgentWorkItemSort.OLDEST: + return sorted.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + case AgentWorkItemSort.PRIORITY: + return sorted.sort((a, b) => { + const delta = (PRIORITY_RANK[a.priority] ?? 9) - (PRIORITY_RANK[b.priority] ?? 9) + return delta !== 0 ? delta : b.createdAt.localeCompare(a.createdAt) + }) + case AgentWorkItemSort.AGENT: + return sorted.sort((a, b) => { + const delta = a.agentId.localeCompare(b.agentId) + return delta !== 0 ? delta : b.createdAt.localeCompare(a.createdAt) + }) + case AgentWorkItemSort.OBJECT_ID: + return sorted.sort((a, b) => { + const delta = (a.objectId ?? '￿').localeCompare(b.objectId ?? '￿') + return delta !== 0 ? delta : b.createdAt.localeCompare(a.createdAt) + }) + case AgentWorkItemSort.NEWEST: + default: + return sorted.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + } +} + +export const MockupAgentWorkItemProvider: IAgentWorkItemProvider = { + async getAll(filters?: AgentWorkItemFilters) { + let results = store + + if (filters?.requiresDecision !== undefined) { + results = results.filter(i => i.requiresDecision === filters.requiresDecision) + } + if (filters?.agentId) results = results.filter(i => i.agentId === filters.agentId) + if (filters?.area) results = results.filter(i => i.area === filters.area) + if (filters?.kind) results = results.filter(i => i.kind === filters.kind) + if (filters?.priority) results = results.filter(i => i.priority === filters.priority) + if (filters?.status) results = results.filter(i => i.status === filters.status) + if (filters?.channel) { + results = results.filter( + i => i.sourceChannel === filters.channel || i.targetChannel === filters.channel, + ) + } + if (filters?.period && filters.period !== AgentPeriod.ALL) { + const now = teamNow() + results = results.filter(i => isWithinPeriod(i.completedAt ?? i.createdAt, filters.period!, now)) + } + if (filters?.search) { + results = results.filter(i => matchesSearch(i, filters.search!)) + } + + return structuredClone(sortItems(results, filters?.sort ?? AgentWorkItemSort.NEWEST)) + }, + + async getById(id) { + const found = store.find(i => i.id === id) + return found ? structuredClone(found) : null + }, + + async update(id, data: UpdateAgentWorkItemInput) { + await simulateLatency(AGENT_SIM_DELAY_MS) + const idx = store.findIndex(i => i.id === id) + if (idx === -1) throw new Error(`Vorgang «${id}» nicht gefunden.`) + + const current = store[idx] + const history = data.historyEntry + ? [...current.history, { id: crypto.randomUUID(), ...data.historyEntry }] + : current.history + + store[idx] = { + ...current, + ...(data.status !== undefined ? { status: data.status } : {}), + ...(data.result ? { result: data.result } : {}), + ...(data.editableFields ? { editableFields: data.editableFields } : {}), + ...(data.rejectionReason !== undefined ? { rejectionReason: data.rejectionReason } : {}), + ...(data.completedAt !== undefined ? { completedAt: data.completedAt } : {}), + ...(data.requiresDecision !== undefined ? { requiresDecision: data.requiresDecision } : {}), + history, + } + save() + return structuredClone(store[idx]) + }, + + async reset() { + store = structuredClone(mockAgentWorkItems) + }, +} diff --git a/src/provider/MockupTeamAgentProvider.ts b/src/provider/MockupTeamAgentProvider.ts new file mode 100644 index 0000000..9304861 --- /dev/null +++ b/src/provider/MockupTeamAgentProvider.ts @@ -0,0 +1,56 @@ +import type { ITeamAgentProvider } from './ITeamAgentProvider' +import type { TeamAgent, UpdateTeamAgentInput } from '../domain/teamAgent' +import { mockTeamAgents } from '../mock-data/teamAgents' +import { + TEAM_STORAGE_KEYS, + loadVersioned, + persistVersioned, + simulateLatency, +} from './teamPersistence' +import { AGENT_SIM_DELAY_MS } from '../lib/constants' + +/** + * Der Bestand wird beim Laden aus dem Local Storage überlagert: geänderte + * Einstellungen, Aufgabenschalter, Kanalschalter und pausierte Mitarbeiter + * überleben so einen Reload. Fehlt oder passt der gespeicherte Bestand nicht + * zur Schemaversion, greift der Seed aus `src/mock-data/`. + */ +function seed(): TeamAgent[] { + return loadVersioned(TEAM_STORAGE_KEYS.AGENTS) ?? structuredClone(mockTeamAgents) +} + +let store: TeamAgent[] = seed() + +function save(): void { + persistVersioned(TEAM_STORAGE_KEYS.AGENTS, store) +} + +export const MockupTeamAgentProvider: ITeamAgentProvider = { + async getAll() { + return structuredClone(store) + }, + + async getById(id) { + const found = store.find(a => a.id === id) + return found ? structuredClone(found) : null + }, + + async update(id, data: UpdateTeamAgentInput) { + await simulateLatency(AGENT_SIM_DELAY_MS) + const idx = store.findIndex(a => a.id === id) + if (idx === -1) throw new Error(`Digitaler Mitarbeiter «${id}» nicht gefunden.`) + store[idx] = { + ...store[idx], + ...(data.status !== undefined ? { status: data.status } : {}), + ...(data.tasks ? { tasks: data.tasks } : {}), + ...(data.channels ? { channels: data.channels } : {}), + ...(data.settings ? { settings: data.settings } : {}), + } + save() + return structuredClone(store[idx]) + }, + + async reset() { + store = structuredClone(mockTeamAgents) + }, +} diff --git a/src/provider/teamPersistence.ts b/src/provider/teamPersistence.ts new file mode 100644 index 0000000..081b228 --- /dev/null +++ b/src/provider/teamPersistence.ts @@ -0,0 +1,89 @@ +/** + * Property On — lokale Persistenz (§16.2). + * + * Ausschliesslich von den Mockup-Providern benutzt: die Providerschicht ist der + * einzige Ort, der Datenhaltung berührt (CLAUDE.md §4.1). Services, Hooks und + * Komponenten sehen davon nichts. + * + * Persistiert werden geänderte Einstellungen, Aufgabenstatus, erledigte + * Vorgänge, Verbindungsstatus und pausierte Mitarbeiter. «Demo zurücksetzen» + * löscht genau diese Schlüssel und stellt die ursprünglichen Mockdaten her. + */ + +const PREFIX = 'property-on:' + +export const TEAM_STORAGE_KEYS = { + AGENTS: `${PREFIX}agents`, + WORK_ITEMS: `${PREFIX}work-items`, + PROTOCOL: `${PREFIX}protocol`, + CONNECTIONS: `${PREFIX}connections`, +} as const + +export type TeamStorageKey = typeof TEAM_STORAGE_KEYS[keyof typeof TEAM_STORAGE_KEYS] + +/** + * Liest einen persistierten Bestand. + * Gibt null zurück, wenn nichts gespeichert ist oder der Inhalt unlesbar wurde — + * dann greift der Seed aus `src/mock-data/`. Ein defekter Local-Storage-Eintrag + * darf die Anwendung nie blockieren. + */ +export function loadPersisted(key: TeamStorageKey): T | null { + try { + const raw = window.localStorage.getItem(key) + if (!raw) return null + return JSON.parse(raw) as T + } catch { + return null + } +} + +/** Schreibt einen Bestand. Fehler (z. B. volles Kontingent) werden bewusst geschluckt. */ +export function persist(key: TeamStorageKey, value: T): void { + try { + window.localStorage.setItem(key, JSON.stringify(value)) + } catch { + // Persistenz ist ein Komfort, kein Muss — die Demo läuft auch ohne weiter. + } +} + +/** + * Schemaversion der persistierten Bestände. Erhöhen, sobald sich eine Struktur + * unter `src/domain/` ändert — sonst überlagert ein alter Local-Storage-Eintrag + * die neuen Mockdaten und die Demo zeigt stillschweigend veraltete Inhalte. + */ +export const TEAM_SCHEMA_VERSION = 1 + +interface PersistedEnvelope { + version: number + data: T +} + +/** Wie `loadPersisted`, verwirft aber Bestände einer fremden Schemaversion. */ +export function loadVersioned(key: TeamStorageKey): T | null { + const envelope = loadPersisted>(key) + if (!envelope || envelope.version !== TEAM_SCHEMA_VERSION) return null + return envelope.data +} + +export function persistVersioned(key: TeamStorageKey, data: T): void { + persist>(key, { version: TEAM_SCHEMA_VERSION, data }) +} + +/** Löscht sämtliche Property-On-Schlüssel («Demo zurücksetzen»). */ +export function clearTeamPersistence(): void { + try { + for (const key of Object.values(TEAM_STORAGE_KEYS)) { + window.localStorage.removeItem(key) + } + } catch { + // siehe oben + } +} + +/** + * Simulierte Ladezeit (§19.1). Bewusst deterministisch statt zufällig — ein + * flackernder Zufallswert macht die Demo unvorhersehbar und Tests instabil. + */ +export function simulateLatency(ms: number): Promise { + return new Promise(resolve => { setTimeout(resolve, ms) }) +} diff --git a/src/services/agentConnectionService.ts b/src/services/agentConnectionService.ts new file mode 100644 index 0000000..6a0a8dc --- /dev/null +++ b/src/services/agentConnectionService.ts @@ -0,0 +1,82 @@ +import { MockupAgentConnectionProvider } from '../provider/MockupAgentConnectionProvider' +import type { + AgentConnection, + AgentConnectionTestResult, + CreateAgentConnectionInput, +} from '../domain/agentConnection' +import { AgentProtocolEventType, AgentProtocolStatus } from '../domain/agentProtocol' +import { AGENT_CONNECTION_CATEGORY_LABELS } from '../lib/constants' +import type { ListResponse, ItemResponse } from './types' +import { throwServiceError } from './errors' +import { agentProtocolService } from './agentProtocolService' + +const provider = MockupAgentConnectionProvider + +export const agentConnectionService = { + async getAll(): Promise> { + try { + const data = await provider.getAll() + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + } catch (err) { + throwServiceError(err) + } + }, + + async getById(id: string): Promise> { + try { + const data = await provider.getById(id) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async connect(input: CreateAgentConnectionInput): Promise> { + try { + const data = await provider.connect(input) + await agentProtocolService.logUserAction({ + connectionId: data.id, + eventType: AgentProtocolEventType.CONNECTION_CREATED, + title: `Verbindung angelegt: ${data.name}`, + description: `«${input.connectionLabel}» wurde simuliert verbunden. ${input.agentIds.length} digitale Mitarbeiter nutzen diese Verbindung.`, + status: AgentProtocolStatus.SUCCESS, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async disconnect(id: string): Promise> { + try { + const data = await provider.disconnect(id) + await agentProtocolService.logUserAction({ + connectionId: data.id, + eventType: AgentProtocolEventType.CONNECTION_REMOVED, + title: `Verbindung getrennt: ${data.name}`, + description: `${AGENT_CONNECTION_CATEGORY_LABELS[data.category] ?? data.name} steht den digitalen Mitarbeitern nicht mehr zur Verfügung.`, + status: AgentProtocolStatus.WARNING, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async test(id: string): Promise> { + try { + const data = await provider.test(id) + const connection = await provider.getById(id) + await agentProtocolService.logUserAction({ + connectionId: id, + eventType: AgentProtocolEventType.CHANNEL_TESTED, + title: `Verbindung getestet: ${connection?.name ?? id}`, + description: data.message, + status: data.ok ? AgentProtocolStatus.SUCCESS : AgentProtocolStatus.ERROR, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, +} diff --git a/src/services/agentProtocolService.ts b/src/services/agentProtocolService.ts new file mode 100644 index 0000000..dc78dac --- /dev/null +++ b/src/services/agentProtocolService.ts @@ -0,0 +1,68 @@ +import { MockupAgentProtocolProvider } from '../provider/MockupAgentProtocolProvider' +import type { AgentProtocolFilters } from '../provider/IAgentProtocolProvider' +import type { + AgentProtocolEntry, + AgentProtocolEventType, + AgentProtocolStatus, + AgentApprovalDecision, + CreateAgentProtocolEntryInput, +} from '../domain/agentProtocol' +import { AgentTriggerSource } from '../domain/agentProtocol' +import type { AgentChannelType } from '../domain/teamAgent' +import type { AgentDetailField } from '../domain/agentWorkItem' +import type { ListResponse, ItemResponse } from './types' +import { throwServiceError } from './errors' +import { useSessionStore } from '../stores/sessionStore' +import { teamNowIso } from '../lib/teamClock' + +const provider = MockupAgentProtocolProvider + +/** Eingabe für eine vom Menschen ausgelöste Aktion — Zeit und Person füllt der Service. */ +export interface LogUserActionInput { + agentId?: string + connectionId?: string + eventType: AgentProtocolEventType + title: string + description: string + status: AgentProtocolStatus + objectId?: string + channel?: AgentChannelType + approvalDecision?: AgentApprovalDecision + workItemId?: string + output?: AgentDetailField[] +} + +export const agentProtocolService = { + async getAll(filters?: AgentProtocolFilters): Promise> { + try { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + } catch (err) { + throwServiceError(err) + } + }, + + async log(entry: CreateAgentProtocolEntryInput): Promise> { + try { + const data = await provider.create(entry) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + /** + * Protokolliert eine Handlung des Nutzers. Jede freigabepflichtige Aktion in + * Property On läuft hierüber — ohne Eintrag gäbe es keine Nachvollziehbarkeit, + * und genau die ist der Zweck des Protokolls (§12.1). + */ + async logUserAction(input: LogUserActionInput): Promise> { + const userName = useSessionStore.getState().currentUser?.name ?? 'Unbekannt' + return agentProtocolService.log({ + ...input, + timestamp: teamNowIso(), + triggeredBy: AgentTriggerSource.USER, + triggeredByName: userName, + }) + }, +} diff --git a/src/services/agentWorkItemService.ts b/src/services/agentWorkItemService.ts new file mode 100644 index 0000000..c21846f --- /dev/null +++ b/src/services/agentWorkItemService.ts @@ -0,0 +1,225 @@ +import { MockupAgentWorkItemProvider } from '../provider/MockupAgentWorkItemProvider' +import type { AgentWorkItemFilters } from '../provider/IAgentWorkItemProvider' +import type { + AgentWorkItem, + AgentEditableField, + AgentWorkloadKpis, +} from '../domain/agentWorkItem' +import { AgentWorkItemStatus, AgentWorkItemAction } from '../domain/agentWorkItem' +import { + AgentProtocolEventType, + AgentProtocolStatus, + AgentApprovalDecision, +} from '../domain/agentProtocol' +import type { ListResponse, ItemResponse } from './types' +import { throwServiceError } from './errors' +import { agentProtocolService } from './agentProtocolService' +import { useSessionStore } from '../stores/sessionStore' +import { teamNowIso } from '../lib/teamClock' + +const provider = MockupAgentWorkItemProvider + +/** Aktionen, die eine Entscheidung zur Bestätigung vorlegen — siehe `AgentWorkloadKpis`. */ +const APPROVAL_ACTIONS: string[] = [ + AgentWorkItemAction.APPROVE, + AgentWorkItemAction.ONE_CLICK_CONFIRM, + AgentWorkItemAction.DECIDE, +] + +function actorName(): string { + return useSessionStore.getState().currentUser?.name ?? 'Unbekannt' +} + +function isApprovalRequest(item: AgentWorkItem): boolean { + return item.availableActions.some(a => APPROVAL_ACTIONS.includes(a)) +} + +export const agentWorkItemService = { + async getAll(filters?: AgentWorkItemFilters): Promise> { + try { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + } catch (err) { + throwServiceError(err) + } + }, + + async getById(id: string): Promise> { + try { + const data = await provider.getById(id) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + /** + * Kennzahlen der Auswertung. Bewusst ein einziger Durchlauf über die gefilterte + * Liste statt dreier `.filter()`-Aufrufe (CLAUDE.md §10.4). + * + * Zurückgewiesene Vorgänge zählen nicht als erledigte Aufgabe — sie wurden + * entschieden, aber nicht ausgeführt. Eine Kennzahl, die sie mitzählt, würde + * die Leistung des Teams zu gut darstellen. + */ + async getKpis(filters?: AgentWorkItemFilters): Promise> { + try { + // requiresDecision bewusst ausgeklammert: die Kennzahlen betrachten beide Reiter. + const scoped: AgentWorkItemFilters = { ...filters, requiresDecision: undefined } + const items = await provider.getAll(scoped) + + let completedTasks = 0 + let pendingMessages = 0 + let pendingApprovals = 0 + + for (const item of items) { + if (item.requiresDecision) { + if (isApprovalRequest(item)) pendingApprovals++ + else pendingMessages++ + } else if (item.status !== AgentWorkItemStatus.REJECTED) { + completedTasks++ + } + } + + return { data: { completedTasks, pendingMessages, pendingApprovals } } + } catch (err) { + throwServiceError(err) + } + }, + + /** Freigabe bzw. 1-Klick-Bestätigung: Vorgang wechselt zu «Erledigte Aufträge». */ + async approve(id: string): Promise> { + try { + const now = teamNowIso() + const data = await provider.update(id, { + status: AgentWorkItemStatus.APPROVED, + requiresDecision: false, + completedAt: now, + historyEntry: { at: now, actor: actorName(), action: 'Freigegeben' }, + }) + await agentProtocolService.logUserAction({ + agentId: data.agentId, + eventType: AgentProtocolEventType.APPROVAL, + title: `Freigegeben: ${data.title}`, + description: data.summary, + status: AgentProtocolStatus.SUCCESS, + objectId: data.objectId, + channel: data.targetChannel ?? data.sourceChannel, + approvalDecision: AgentApprovalDecision.APPROVED, + workItemId: data.id, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async reject(id: string, reason?: string): Promise> { + try { + const now = teamNowIso() + const data = await provider.update(id, { + status: AgentWorkItemStatus.REJECTED, + requiresDecision: false, + completedAt: now, + rejectionReason: reason, + historyEntry: { at: now, actor: actorName(), action: 'Zurückgewiesen', note: reason }, + }) + await agentProtocolService.logUserAction({ + agentId: data.agentId, + eventType: AgentProtocolEventType.REJECTION, + title: `Zurückgewiesen: ${data.title}`, + description: reason?.trim() + ? `Begründung: ${reason.trim()}` + : 'Ohne Angabe einer Begründung zurückgewiesen.', + status: AgentProtocolStatus.WARNING, + objectId: data.objectId, + approvalDecision: AgentApprovalDecision.REJECTED, + workItemId: data.id, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + /** + * «Anpassen»: Änderungen werden lokal gespeichert, der Vorgang bleibt aber bis + * zur finalen Entscheidung pendent (§5.6). + */ + async saveEdit(id: string, fields: AgentEditableField[]): Promise> { + try { + const now = teamNowIso() + const data = await provider.update(id, { + status: AgentWorkItemStatus.EDITING, + requiresDecision: true, + editableFields: fields, + historyEntry: { at: now, actor: actorName(), action: 'Angepasst' }, + }) + await agentProtocolService.logUserAction({ + agentId: data.agentId, + eventType: AgentProtocolEventType.EDIT, + title: `Angepasst: ${data.title}`, + description: 'Der Vorgang wurde bearbeitet und wartet weiterhin auf die finale Entscheidung.', + status: AgentProtocolStatus.INFO, + objectId: data.objectId, + approvalDecision: AgentApprovalDecision.EDITED, + workItemId: data.id, + output: fields.map(f => ({ label: f.label, value: f.value })), + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + /** Rückfrage beantworten — die Antwort wird Teil des Ergebnisses. */ + async answerQuery(id: string, answer: string): Promise> { + try { + const current = await provider.getById(id) + if (!current) throw new Error(`Vorgang «${id}» nicht gefunden.`) + const now = teamNowIso() + const data = await provider.update(id, { + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + completedAt: now, + result: [...current.result, { label: 'Antwort der Bewirtschaftung', value: answer }], + historyEntry: { at: now, actor: actorName(), action: 'Rückfrage beantwortet', note: answer }, + }) + await agentProtocolService.logUserAction({ + agentId: data.agentId, + eventType: AgentProtocolEventType.APPROVAL, + title: `Rückfrage beantwortet: ${data.title}`, + description: answer, + status: AgentProtocolStatus.SUCCESS, + objectId: data.objectId, + workItemId: data.id, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async markDone(id: string): Promise> { + try { + const now = teamNowIso() + const data = await provider.update(id, { + status: AgentWorkItemStatus.COMPLETED, + requiresDecision: false, + completedAt: now, + historyEntry: { at: now, actor: actorName(), action: 'Als erledigt markiert' }, + }) + await agentProtocolService.logUserAction({ + agentId: data.agentId, + eventType: AgentProtocolEventType.APPROVAL, + title: `Als erledigt markiert: ${data.title}`, + description: data.summary, + status: AgentProtocolStatus.SUCCESS, + objectId: data.objectId, + workItemId: data.id, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, +} diff --git a/src/services/teamAgentService.ts b/src/services/teamAgentService.ts new file mode 100644 index 0000000..1a69211 --- /dev/null +++ b/src/services/teamAgentService.ts @@ -0,0 +1,98 @@ +import { MockupTeamAgentProvider } from '../provider/MockupTeamAgentProvider' +import type { TeamAgent, AgentTask, AgentChannel, AgentSetting } from '../domain/teamAgent' +import { AgentStatus } from '../domain/teamAgent' +import { AgentProtocolEventType, AgentProtocolStatus } from '../domain/agentProtocol' +import type { ListResponse, ItemResponse } from './types' +import { throwServiceError } from './errors' +import { agentProtocolService } from './agentProtocolService' + +const provider = MockupTeamAgentProvider + +export const teamAgentService = { + async getAll(): Promise> { + try { + const data = await provider.getAll() + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + } catch (err) { + throwServiceError(err) + } + }, + + async getById(id: string): Promise> { + try { + const data = await provider.getById(id) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + /** + * Pausiert oder reaktiviert einen digitalen Mitarbeiter (§7.5). + * Der Aktiv-Haken auf der Teamübersicht spiegelt genau diesen Zustand. + */ + async setActive(id: string, active: boolean): Promise> { + try { + const data = await provider.update(id, { + status: active ? AgentStatus.ACTIVE : AgentStatus.PAUSED, + }) + await agentProtocolService.logUserAction({ + agentId: id, + eventType: active ? AgentProtocolEventType.AGENT_RESUMED : AgentProtocolEventType.AGENT_PAUSED, + title: active ? `${data.name} reaktiviert` : `${data.name} pausiert`, + description: active + ? `${data.name} nimmt die Arbeit wieder auf. Alle aktivierten Aufgaben laufen erneut nach Zeitplan.` + : `${data.name} wurde pausiert. Bis zur Reaktivierung werden keine Aufgaben ausgeführt.`, + status: active ? AgentProtocolStatus.SUCCESS : AgentProtocolStatus.WARNING, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async saveTasks(id: string, tasks: AgentTask[]): Promise> { + try { + const data = await provider.update(id, { tasks }) + const disabled = tasks.filter(t => !t.enabled).length + await agentProtocolService.logUserAction({ + agentId: id, + eventType: AgentProtocolEventType.TASKS_SAVED, + title: 'Aufgaben gespeichert', + description: + disabled === 0 + ? `Alle ${tasks.length} Aufgaben von ${data.name} sind aktiv.` + : `${tasks.length - disabled} von ${tasks.length} Aufgaben aktiv, ${disabled} deaktiviert.`, + status: disabled === 0 ? AgentProtocolStatus.SUCCESS : AgentProtocolStatus.WARNING, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async saveSettings(id: string, settings: AgentSetting[]): Promise> { + try { + const data = await provider.update(id, { settings }) + await agentProtocolService.logUserAction({ + agentId: id, + eventType: AgentProtocolEventType.SETTINGS_SAVED, + title: 'Einstellungen gespeichert', + description: `Die Einstellungen von ${data.name} wurden angepasst.`, + status: AgentProtocolStatus.SUCCESS, + }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, + + async saveChannels(id: string, channels: AgentChannel[]): Promise> { + try { + const data = await provider.update(id, { channels }) + return { data } + } catch (err) { + throwServiceError(err) + } + }, +} diff --git a/src/services/teamDemoService.ts b/src/services/teamDemoService.ts new file mode 100644 index 0000000..621a4f0 --- /dev/null +++ b/src/services/teamDemoService.ts @@ -0,0 +1,32 @@ +import { MockupTeamAgentProvider } from '../provider/MockupTeamAgentProvider' +import { MockupAgentWorkItemProvider } from '../provider/MockupAgentWorkItemProvider' +import { MockupAgentProtocolProvider } from '../provider/MockupAgentProtocolProvider' +import { MockupAgentConnectionProvider } from '../provider/MockupAgentConnectionProvider' +import { clearTeamPersistence } from '../provider/teamPersistence' +import type { ItemResponse } from './types' +import { throwServiceError } from './errors' + +/** + * «Demo zurücksetzen» (§16.2). + * + * Löscht alle lokalen Änderungen und stellt die ursprünglichen Mockdaten her. + * Bewusst OHNE Protokolleintrag: nach dem Zurücksetzen soll exakt der + * Auslieferungszustand stehen — ein Eintrag über den Reset selbst wäre bereits + * eine Abweichung davon. + */ +export const teamDemoService = { + async reset(): Promise> { + try { + clearTeamPersistence() + await Promise.all([ + MockupTeamAgentProvider.reset(), + MockupAgentWorkItemProvider.reset(), + MockupAgentProtocolProvider.reset(), + MockupAgentConnectionProvider.reset(), + ]) + return { data: undefined } + } catch (err) { + throwServiceError(err) + } + }, +} diff --git a/src/stores/teamStore.ts b/src/stores/teamStore.ts new file mode 100644 index 0000000..16dd15d --- /dev/null +++ b/src/stores/teamStore.ts @@ -0,0 +1,101 @@ +import { create } from 'zustand' +import type { + AgentWorkItemKind, + AgentWorkItemPriority, + AgentWorkItemStatus, + AgentDomainArea, +} from '../domain/agentWorkItem' +import type { AgentChannelType } from '../domain/teamAgent' +import { AgentPeriod, AgentWorkItemSort } from '../domain/agentFilters' + +/** Der jeweils andere Reiter des Bearbeitungsverlaufs (§5.2). */ +export const HistoryTab = { + DONE: 'DONE', + PENDING: 'PENDING', +} as const +export type HistoryTab = typeof HistoryTab[keyof typeof HistoryTab] + +export type AgentFilterValue = T | 'ALL' + +interface TeamStore { + // ── Auswertung auf der Teamübersicht (§4.2) ───────────────────────────────── + overviewPeriod: AgentPeriod + overviewArea: AgentFilterValue + overviewAgentId: AgentFilterValue + setOverviewPeriod: (p: AgentPeriod) => void + setOverviewArea: (a: AgentFilterValue) => void + setOverviewAgentId: (id: AgentFilterValue) => void + resetOverviewFilters: () => void + + // ── Bearbeitungsverlauf (§5.3) ────────────────────────────────────────────── + historyTab: HistoryTab + setHistoryTab: (t: HistoryTab) => void + historyPeriod: AgentPeriod + historyAgentId: AgentFilterValue + historyArea: AgentFilterValue + historyKind: AgentFilterValue + historyPriority: AgentFilterValue + historyChannel: AgentFilterValue + historyStatus: AgentFilterValue + historySearch: string + historySort: AgentWorkItemSort + setHistoryPeriod: (p: AgentPeriod) => void + setHistoryAgentId: (id: AgentFilterValue) => void + setHistoryArea: (a: AgentFilterValue) => void + setHistoryKind: (k: AgentFilterValue) => void + setHistoryPriority: (p: AgentFilterValue) => void + setHistoryChannel: (c: AgentFilterValue) => void + setHistoryStatus: (s: AgentFilterValue) => void + setHistorySearch: (q: string) => void + setHistorySort: (s: AgentWorkItemSort) => void + resetHistoryFilters: () => void + + // ── Detail-Drawer ─────────────────────────────────────────────────────────── + selectedWorkItemId: string | null + setSelectedWorkItemId: (id: string | null) => void +} + +const HISTORY_FILTER_DEFAULTS = { + historyPeriod: AgentPeriod.ALL, + historyAgentId: 'ALL' as AgentFilterValue, + historyArea: 'ALL' as AgentFilterValue, + historyKind: 'ALL' as AgentFilterValue, + historyPriority: 'ALL' as AgentFilterValue, + historyChannel: 'ALL' as AgentFilterValue, + historyStatus: 'ALL' as AgentFilterValue, + historySearch: '', + historySort: AgentWorkItemSort.NEWEST, +} + +const OVERVIEW_FILTER_DEFAULTS = { + // Der Monat ist der Zeitraum, in dem eine Bewirtschaftung ihre Leistung liest — + // «heute» wäre für die Startseite zu eng und wirkte oft leer. + overviewPeriod: AgentPeriod.MONTH, + overviewArea: 'ALL' as AgentFilterValue, + overviewAgentId: 'ALL' as AgentFilterValue, +} + +export const useTeamStore = create((set) => ({ + ...OVERVIEW_FILTER_DEFAULTS, + setOverviewPeriod: (p) => set({ overviewPeriod: p }), + setOverviewArea: (a) => set({ overviewArea: a }), + setOverviewAgentId: (id) => set({ overviewAgentId: id }), + resetOverviewFilters: () => set({ ...OVERVIEW_FILTER_DEFAULTS }), + + historyTab: HistoryTab.PENDING, + setHistoryTab: (t) => set({ historyTab: t }), + ...HISTORY_FILTER_DEFAULTS, + setHistoryPeriod: (p) => set({ historyPeriod: p }), + setHistoryAgentId: (id) => set({ historyAgentId: id }), + setHistoryArea: (a) => set({ historyArea: a }), + setHistoryKind: (k) => set({ historyKind: k }), + setHistoryPriority: (p) => set({ historyPriority: p }), + setHistoryChannel: (c) => set({ historyChannel: c }), + setHistoryStatus: (s) => set({ historyStatus: s }), + setHistorySearch: (q) => set({ historySearch: q }), + setHistorySort: (s) => set({ historySort: s }), + resetHistoryFilters: () => set({ ...HISTORY_FILTER_DEFAULTS }), + + selectedWorkItemId: null, + setSelectedWorkItemId: (id) => set({ selectedWorkItemId: id }), +}))