From 4429a7890af68f8473b6cec01eed30906982d923 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 17 May 2026 13:50:33 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20F026=20final=20assembly=20=E2=80=94=20A?= =?UTF-8?q?ctivityTimeline,=20expanded=20demo=20event=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace ActivityTimeline stub with full vertical timeline page: grouped by day, filterable by workflow category (Suche/Matching/ Intelligence/Review/Governance), KPI strip, AI vs human distinction - Expand governanceService mock data from 3 → 24 events covering the full demo storyline (source crawl → signal detection → need creation → AI parse → match generation → review → shortlist → decision brief) - Add 11 new ActivityEventTypes: AI_PARSE_COMPLETED, MATCH_GENERATED, FUTURE_SIGNAL_DETECTED/CONVERTED, SHORTLIST_CREATED/FINALIZED, DECISION_BRIEF_CREATED, SOURCE_CRAWLED, DATA_QUALITY_FLAGGED, REVIEW_COMPLETED, AI_OUTPUT_REVIEWED - Update Governance.tsx switch statements to handle all new types Co-Authored-By: Claude Sonnet 4.6 --- src/pages/ops/ActivityTimeline.tsx | 368 ++++++++++++++++++++++++++++- src/pages/ops/Governance.tsx | 64 ++++- src/services/governanceService.ts | 49 +++- 3 files changed, 459 insertions(+), 22 deletions(-) diff --git a/src/pages/ops/ActivityTimeline.tsx b/src/pages/ops/ActivityTimeline.tsx index bdcedd8..1cebbaa 100644 --- a/src/pages/ops/ActivityTimeline.tsx +++ b/src/pages/ops/ActivityTimeline.tsx @@ -1,16 +1,362 @@ -import { Box, Typography } from '@mui/material' -import { PageHeader } from '../../components/layout' +import { useState } from 'react' +import { Box, Chip, CircularProgress, Stack, Tooltip, Typography } from '@mui/material' +import { + Activity, + Bot, + Bookmark, + BookmarkCheck, + Building2, + AlertTriangle, + CheckCircle, + ClipboardList, + Edit, + FileText, + GitMerge, + Radar, + Search, + ServerCog, + TrendingUp, + User, + XCircle, +} from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { governanceService, type ActivityCategory, type ActivityEvent, type ActivityEventType } from '../../services/governanceService' +import { EmptyState } from '../../components/ui' + +// ── Labels & colours ────────────────────────────────────────────────────────── + +const EVENT_LABELS: Record = { + PROPERTY_CREATED: 'Objekt erstellt', + PROPERTY_UPDATED: 'Objekt aktualisiert', + MATCH_APPROVED: 'Match genehmigt', + MATCH_REJECTED: 'Match abgelehnt', + SIGNAL_VERIFIED: 'Signal verifiziert', + NEED_CREATED: 'Bedarf erstellt', + REVIEW_REQUESTED: 'Überprüfung angefordert', + AI_PARSE_COMPLETED: 'KI-Analyse abgeschlossen', + AI_OUTPUT_REVIEWED: 'KI-Output geprüft', + MATCH_GENERATED: 'Matches generiert', + FUTURE_SIGNAL_DETECTED: 'Zukunftssignal erkannt', + FUTURE_SIGNAL_CONVERTED: 'Signal konvertiert', + SHORTLIST_CREATED: 'Shortlist erstellt', + SHORTLIST_FINALIZED: 'Shortlist finalisiert', + DECISION_BRIEF_CREATED: 'Entscheidungsbriefing erstellt', + SOURCE_CRAWLED: 'Quelle gecrawlt', + DATA_QUALITY_FLAGGED: 'Datenqualität markiert', + REVIEW_COMPLETED: 'Prüfung abgeschlossen', +} + +const EVENT_COLORS: Record = { + PROPERTY_CREATED: '#1e3a5f', + PROPERTY_UPDATED: '#1e3a5f', + MATCH_APPROVED: '#1a7a4a', + MATCH_REJECTED: '#c0392b', + SIGNAL_VERIFIED: '#7c3aed', + NEED_CREATED: '#0891b2', + REVIEW_REQUESTED: '#d97706', + AI_PARSE_COMPLETED: '#0891b2', + AI_OUTPUT_REVIEWED: '#1a7a4a', + MATCH_GENERATED: '#0891b2', + FUTURE_SIGNAL_DETECTED: '#7c3aed', + FUTURE_SIGNAL_CONVERTED: '#7c3aed', + SHORTLIST_CREATED: '#0f766e', + SHORTLIST_FINALIZED: '#0f766e', + DECISION_BRIEF_CREATED: '#0f766e', + SOURCE_CRAWLED: '#6366f1', + DATA_QUALITY_FLAGGED: '#ea580c', + REVIEW_COMPLETED: '#1a7a4a', +} + +const CATEGORY_META: Record = { + SUCHE: { label: 'Suche', color: '#0891b2' }, + MATCHING: { label: 'Matching', color: '#1a7a4a' }, + INTELLIGENCE: { label: 'Intelligence', color: '#7c3aed' }, + REVIEW: { label: 'Review', color: '#d97706' }, + GOVERNANCE: { label: 'Governance', color: '#1e3a5f' }, +} + +const ALL_CATEGORIES: ActivityCategory[] = ['SUCHE', 'MATCHING', 'INTELLIGENCE', 'REVIEW', 'GOVERNANCE'] + +function getEventIcon(type: ActivityEventType) { + const s = 13 + switch (type) { + case 'PROPERTY_CREATED': return + case 'PROPERTY_UPDATED': return + case 'MATCH_APPROVED': return + case 'MATCH_REJECTED': return + case 'SIGNAL_VERIFIED': return + case 'NEED_CREATED': return + case 'REVIEW_REQUESTED': return + case 'AI_PARSE_COMPLETED': + case 'AI_OUTPUT_REVIEWED': + case 'MATCH_GENERATED': + case 'DECISION_BRIEF_CREATED': return + case 'FUTURE_SIGNAL_DETECTED': + case 'FUTURE_SIGNAL_CONVERTED': return + case 'SHORTLIST_CREATED': return + case 'SHORTLIST_FINALIZED': return + case 'SOURCE_CRAWLED': return + case 'DATA_QUALITY_FLAGGED': return + case 'REVIEW_COMPLETED': return + default: return + } +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('de-CH', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }) +} + +function formatTime(iso: string): string { + return new Date(iso).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) +} + +function dateKey(iso: string): string { + return iso.slice(0, 10) +} + +function isToday(iso: string): boolean { + return dateKey(iso) === new Date().toISOString().slice(0, 10) +} + +function groupByDate(events: ActivityEvent[]): { key: string; label: string; events: ActivityEvent[] }[] { + const map = new Map() + for (const e of events) { + const k = dateKey(e.createdAt) + if (!map.has(k)) map.set(k, []) + map.get(k)!.push(e) + } + return [...map.entries()] + .sort(([a], [b]) => b.localeCompare(a)) + .map(([key, evts]) => ({ + key, + label: isToday(evts[0].createdAt) ? 'Heute' : formatDate(evts[0].createdAt), + events: evts, + })) +} + +// ── EventRow ────────────────────────────────────────────────────────────────── + +function EventRow({ event, isLast }: { event: ActivityEvent; isLast: boolean }) { + const color = EVENT_COLORS[event.type] ?? '#64748b' + const catMeta = CATEGORY_META[event.category] -export default function ActivityTimeline() { return ( - - - - Timeline wird implementiert... - + + {/* Vertical connector */} + {!isLast && ( + + )} + + {/* Icon dot */} + + {getEventIcon(event.type)} + + + {/* Content */} + + + + + + {EVENT_LABELS[event.type]} + + + {event.isAiAction && ( + + + + )} + + {event.notes && ( + + {event.notes} + + )} + + {event.isAiAction + ? + : + } + + {event.isAiAction ? 'KI-System' : event.performedBy} + + + + + {formatTime(event.createdAt)} + + + + + ) +} + +// ── Page ────────────────────────────────────────────────────────────────────── + +export default function ActivityTimeline() { + const [filterCategory, setFilterCategory] = useState('ALL') + + const { data: resp, isLoading, error } = useQuery({ + queryKey: ['activityTimeline', 'org-wincasa'], + queryFn: () => governanceService.getActivityLog('org-wincasa'), + staleTime: 30_000, + }) + + const allEvents = resp?.data ?? [] + + const filtered = filterCategory === 'ALL' + ? allEvents + : allEvents.filter(e => e.category === filterCategory) + + const grouped = groupByDate(filtered) + + const aiCount = allEvents.filter(e => e.isAiAction).length + const humanCount = allEvents.length - aiCount + const uniqueActors = new Set(allEvents.map(e => e.performedBy)).size + const todayCount = allEvents.filter(e => isToday(e.createdAt)).length + + if (isLoading) { + return ( + + + + ) + } + + if (error) { + return ( + + Aktivitätsverlauf konnte nicht geladen werden. + + ) + } + + return ( + + {/* Header */} + + + + + Aktivitäts-Timeline + + {/* KPI chips */} + + + {todayCount > 0 && ( + + )} + + + + + + + End-to-End Aktivitätsverlauf — KI-Aktionen und Menschliche Entscheidungen im Überblick + + + + {/* Filter bar */} + + + setFilterCategory('ALL')} + sx={{ + bgcolor: filterCategory === 'ALL' ? '#1e3a5f' : 'transparent', + color: filterCategory === 'ALL' ? 'white' : '#64748b', + border: `1px solid ${filterCategory === 'ALL' ? '#1e3a5f' : '#e2e8f0'}`, + fontWeight: filterCategory === 'ALL' ? 700 : 400, + fontSize: '0.75rem', + }} + /> + {ALL_CATEGORIES.map(cat => { + const meta = CATEGORY_META[cat] + const active = filterCategory === cat + return ( + setFilterCategory(cat)} + sx={{ + bgcolor: active ? meta.color : 'transparent', + color: active ? 'white' : meta.color, + border: `1px solid ${active ? meta.color : meta.color + '40'}`, + fontWeight: active ? 700 : 400, + fontSize: '0.75rem', + }} + /> + ) + })} + {filterCategory !== 'ALL' && ( + + {filtered.length} von {allEvents.length} + + )} + + + + {/* Timeline body */} + + {grouped.length === 0 ? ( + + ) : ( + + {grouped.map(({ key, label, events: dayEvents }) => ( + + {/* Day header */} + + + {label} + + + + {dayEvents.length} Ereignisse + + + + {/* Events */} + + {dayEvents.map((evt, idx) => ( + + ))} + + + ))} + + )} + ) } diff --git a/src/pages/ops/Governance.tsx b/src/pages/ops/Governance.tsx index b8d7086..948a6c2 100644 --- a/src/pages/ops/Governance.tsx +++ b/src/pages/ops/Governance.tsx @@ -16,6 +16,13 @@ import { TrendingUp, Search, ClipboardList, + Bot, + Radar, + ServerCog, + Bookmark, + BookmarkCheck, + AlertTriangle, + GitMerge, } from 'lucide-react' import { useQuery } from '@tanstack/react-query' import { governanceService, type ActivityEventType, type ActivityEvent } from '../../services/governanceService' @@ -30,25 +37,46 @@ function getEventLabel(type: ActivityEventType): string { case 'SIGNAL_VERIFIED': return 'Signal verifiziert' case 'NEED_CREATED': return 'Bedarf erstellt' case 'REVIEW_REQUESTED': return 'Überprüfung angefordert' + case 'AI_PARSE_COMPLETED': return 'KI-Analyse abgeschlossen' + case 'AI_OUTPUT_REVIEWED': return 'KI-Output geprüft' + case 'MATCH_GENERATED': return 'Matches generiert' + case 'FUTURE_SIGNAL_DETECTED': return 'Zukunftssignal erkannt' + case 'FUTURE_SIGNAL_CONVERTED': return 'Signal konvertiert' + case 'SHORTLIST_CREATED': return 'Shortlist erstellt' + case 'SHORTLIST_FINALIZED': return 'Shortlist finalisiert' + case 'DECISION_BRIEF_CREATED': return 'Entscheidungsbriefing erstellt' + case 'SOURCE_CRAWLED': return 'Quelle gecrawlt' + case 'DATA_QUALITY_FLAGGED': return 'Datenqualität markiert' + case 'REVIEW_COMPLETED': return 'Prüfung abgeschlossen' } } function getEventDescription(event: ActivityEvent): string { - const actor = event.performedBy + const actor = event.isAiAction ? 'KI-System' : event.performedBy const action = getEventLabel(event.type) - const entity = `${event.entityType} ${event.entityId}` - return `${actor} hat ${entity} — ${action}` + return `${actor} — ${action}` } function getEventColor(type: ActivityEventType): string { switch (type) { - case 'PROPERTY_CREATED': return '#1e3a5f' + case 'PROPERTY_CREATED': case 'PROPERTY_UPDATED': return '#1e3a5f' - case 'MATCH_APPROVED': return '#1a7a4a' + case 'MATCH_APPROVED': + case 'REVIEW_COMPLETED': return '#1a7a4a' case 'MATCH_REJECTED': return '#c0392b' - case 'SIGNAL_VERIFIED': return '#7c3aed' - case 'NEED_CREATED': return '#0891b2' + case 'SIGNAL_VERIFIED': + case 'FUTURE_SIGNAL_DETECTED': + case 'FUTURE_SIGNAL_CONVERTED': return '#7c3aed' + case 'NEED_CREATED': + case 'AI_PARSE_COMPLETED': + case 'MATCH_GENERATED': return '#0891b2' case 'REVIEW_REQUESTED': return '#d97706' + case 'AI_OUTPUT_REVIEWED': return '#1a7a4a' + case 'SHORTLIST_CREATED': + case 'SHORTLIST_FINALIZED': + case 'DECISION_BRIEF_CREATED': return '#0f766e' + case 'SOURCE_CRAWLED': return '#6366f1' + case 'DATA_QUALITY_FLAGGED': return '#ea580c' } } @@ -62,6 +90,17 @@ function getEventIcon(type: ActivityEventType) { case 'SIGNAL_VERIFIED': return case 'NEED_CREATED': return case 'REVIEW_REQUESTED': return + case 'AI_PARSE_COMPLETED': + case 'AI_OUTPUT_REVIEWED': + case 'MATCH_GENERATED': + case 'DECISION_BRIEF_CREATED': return + case 'FUTURE_SIGNAL_DETECTED': + case 'FUTURE_SIGNAL_CONVERTED': return + case 'SHORTLIST_CREATED': return + case 'SHORTLIST_FINALIZED': return + case 'SOURCE_CRAWLED': return + case 'DATA_QUALITY_FLAGGED': return + case 'REVIEW_COMPLETED': return } } @@ -73,6 +112,17 @@ const ALL_EVENT_TYPES: ActivityEventType[] = [ 'SIGNAL_VERIFIED', 'NEED_CREATED', 'REVIEW_REQUESTED', + 'AI_PARSE_COMPLETED', + 'AI_OUTPUT_REVIEWED', + 'MATCH_GENERATED', + 'FUTURE_SIGNAL_DETECTED', + 'FUTURE_SIGNAL_CONVERTED', + 'SHORTLIST_CREATED', + 'SHORTLIST_FINALIZED', + 'DECISION_BRIEF_CREATED', + 'SOURCE_CRAWLED', + 'DATA_QUALITY_FLAGGED', + 'REVIEW_COMPLETED', ] function formatDateTime(dateStr: string): string { diff --git a/src/services/governanceService.ts b/src/services/governanceService.ts index bc39cf1..5409832 100644 --- a/src/services/governanceService.ts +++ b/src/services/governanceService.ts @@ -8,22 +8,63 @@ export type ActivityEventType = | 'SIGNAL_VERIFIED' | 'NEED_CREATED' | 'REVIEW_REQUESTED' + | 'AI_PARSE_COMPLETED' + | 'AI_OUTPUT_REVIEWED' + | 'MATCH_GENERATED' + | 'FUTURE_SIGNAL_DETECTED' + | 'FUTURE_SIGNAL_CONVERTED' + | 'SHORTLIST_CREATED' + | 'SHORTLIST_FINALIZED' + | 'DECISION_BRIEF_CREATED' + | 'SOURCE_CRAWLED' + | 'DATA_QUALITY_FLAGGED' + | 'REVIEW_COMPLETED' + +export type ActivityCategory = 'SUCHE' | 'MATCHING' | 'INTELLIGENCE' | 'REVIEW' | 'GOVERNANCE' export interface ActivityEvent { id: string type: ActivityEventType + category: ActivityCategory entityId: string - entityType: 'PROPERTY' | 'MATCH' | 'NEED' | 'SIGNAL' + entityType: 'PROPERTY' | 'MATCH' | 'NEED' | 'SIGNAL' | 'SHORTLIST' | 'AI_OUTPUT' | 'SOURCE' performedBy: string + isAiAction: boolean organizationId: string notes?: string createdAt: string } const mockActivityLog: ActivityEvent[] = [ - { id: 'evt-001', type: 'PROPERTY_CREATED', entityId: 'prop-001', entityType: 'PROPERTY', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', createdAt: '2025-01-10T08:00:00Z' }, - { id: 'evt-002', type: 'MATCH_APPROVED', entityId: 'match-003', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', notes: 'Starker Match bestätigt', createdAt: '2025-05-10T09:00:00Z' }, - { id: 'evt-003', type: 'SIGNAL_VERIFIED', entityId: 'signal-003', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', createdAt: '2025-05-05T09:00:00Z' }, + // Day 1 — 2026-05-13 + { id: 'evt-001', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-001', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Baubewilligung-Feed Kanton ZH: 47 neue Einträge gefunden', createdAt: '2026-05-13T06:15:00Z' }, + { id: 'evt-002', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Potenzielle Grossfläche: Maag Areal Zürich — Wahrscheinlichkeit 82%', createdAt: '2026-05-13T06:18:00Z' }, + { id: 'evt-003', type: 'DATA_QUALITY_FLAGGED', category: 'GOVERNANCE', entityId: 'prop-007', entityType: 'PROPERTY', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Grundriss fehlt; Qualitätsscore 61 — unter Schwellenwert', createdAt: '2026-05-13T07:30:00Z' }, + { id: 'evt-004', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-022', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Neues Industrieobjekt Schlieren erfasst', createdAt: '2026-05-13T09:45:00Z' }, + // Day 2 — 2026-05-14 + { id: 'evt-005', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-002', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Handelsregister-Crawler: 12 Unternehmensumzüge identifiziert', createdAt: '2026-05-14T06:00:00Z' }, + { id: 'evt-006', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Alstom AG Standortanalyse — interne Dokumente verweisen auf Expansionspläne', createdAt: '2026-05-14T06:05:00Z' }, + { id: 'evt-007', type: 'PROPERTY_UPDATED', category: 'GOVERNANCE', entityId: 'prop-003', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Verfügbarkeit aktualisiert: sofort verfügbar', createdAt: '2026-05-14T10:20:00Z' }, + { id: 'evt-008', type: 'REVIEW_REQUESTED', category: 'REVIEW', entityId: 'ai-out-003', entityType: 'AI_OUTPUT', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Need-Parse-Output zur manuellen Prüfung eingereicht', createdAt: '2026-05-14T14:00:00Z' }, + { id: 'evt-009', type: 'AI_OUTPUT_REVIEWED', category: 'REVIEW', entityId: 'ai-out-003', entityType: 'AI_OUTPUT', performedBy: 'reviewer@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Output genehmigt — Kriterienextraktion korrekt', createdAt: '2026-05-14T15:30:00Z' }, + // Day 3 — 2026-05-15 + { id: 'evt-010', type: 'NEED_CREATED', category: 'SUCHE', entityId: 'need-042', entityType: 'NEED', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Natürlichsprachliche Suche: "2500m² Büro Zürich West, offen, Rep.'+ "'" + 'resentanz-qualität"', createdAt: '2026-05-15T09:10:00Z' }, + { id: 'evt-011', type: 'AI_PARSE_COMPLETED', category: 'SUCHE', entityId: 'need-042', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Konfidenz 87% — Kriterien: OFFICE 2000–3000m², Zürich West, CHF 280/m², Einzug Q3 2026', createdAt: '2026-05-15T09:10:04Z' }, + { id: 'evt-012', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-042', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: '14 Kandidaten bewertet — 3 STARK (≥80), 6 MITTEL, 5 SCHWACH', createdAt: '2026-05-15T09:10:07Z' }, + { id: 'evt-013', type: 'SIGNAL_VERIFIED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Maag Areal Signal verifiziert — Baubewilligung bestätigt', createdAt: '2026-05-15T10:00:00Z' }, + { id: 'evt-014', type: 'SHORTLIST_CREATED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: '4 Objekte auf Shortlist "Zürich West Q3 2026"', createdAt: '2026-05-15T11:45:00Z' }, + { id: 'evt-015', type: 'FUTURE_SIGNAL_CONVERTED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Signal in zukünftige Verfügbarkeit umgewandelt — erscheint im Unified Feed', createdAt: '2026-05-15T13:00:00Z' }, + // Day 4 — 2026-05-16 + { id: 'evt-016', type: 'MATCH_APPROVED', category: 'MATCHING', entityId: 'match-003', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Score 91 — Starker Match Hardturmstrasse 201 × GloboCorp bestätigt', createdAt: '2026-05-16T09:00:00Z' }, + { id: 'evt-017', type: 'REVIEW_REQUESTED', category: 'REVIEW', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Vertrauliches Signal zur Governance-Prüfung eingereicht', createdAt: '2026-05-16T10:15:00Z' }, + { id: 'evt-018', type: 'DECISION_BRIEF_CREATED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'KI-Entscheidungsbriefing für Shortlist generiert — Empfehlung: Hardturmstrasse 201', createdAt: '2026-05-16T11:30:00Z' }, + { id: 'evt-019', type: 'REVIEW_COMPLETED', category: 'REVIEW', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'reviewer@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'GENEHMIGT — Vertraulichkeitsstufe bestätigt, Signal für Demand-Pipeline freigegeben', createdAt: '2026-05-16T14:00:00Z' }, + { id: 'evt-020', type: 'SHORTLIST_FINALIZED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Shortlist finalisiert — Kundenentscheid: Objekt 1 und 3 zur Besichtigung', createdAt: '2026-05-16T16:45:00Z' }, + // Day 5 — 2026-05-17 (heute) + { id: 'evt-021', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-003', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'LinkedIn-Jobpostings-Crawler: 8 Expansionssignale gefunden', createdAt: '2026-05-17T06:00:00Z' }, + { id: 'evt-022', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-novartis', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Novartis Basel: 47 Stelleninserate für "Basel Life Sciences Hub" — Flächensignal', createdAt: '2026-05-17T06:12:00Z' }, + { id: 'evt-023', type: 'MATCH_REJECTED', category: 'MATCHING', entityId: 'match-009', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Score 64 — zu schwach, manuell abgelehnt', createdAt: '2026-05-17T08:30:00Z' }, + { id: 'evt-024', type: 'DATA_QUALITY_FLAGGED', category: 'GOVERNANCE', entityId: 'prop-015', entityType: 'PROPERTY', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Preisangabe 18 Monate alt — automatische Qualitätswarnung', createdAt: '2026-05-17T09:00:00Z' }, ] const store = [...mockActivityLog]