diff --git a/src/components/supply/PropertyActivityLogPanel.tsx b/src/components/supply/PropertyActivityLogPanel.tsx new file mode 100644 index 0000000..4804417 --- /dev/null +++ b/src/components/supply/PropertyActivityLogPanel.tsx @@ -0,0 +1,256 @@ +import { Box, Chip, CircularProgress, Tooltip, Typography } from '@mui/material' +import { + AlertTriangle, + Bookmark, + BookmarkCheck, + Bot, + Building2, + CheckCircle, + ClipboardList, + Edit, + FileText, + GitMerge, + Radar, + Search, + ServerCog, + TrendingUp, + User, + XCircle, +} from 'lucide-react' +import { usePropertyActivityLog } from '../../hooks/useActivityLog' +import type { ActivityCategory, ActivityEventType } from '../../services/governanceService' + +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 registriert', + 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' }, +} + +function getEventIcon(type: ActivityEventType) { + const s = 12 + 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', { 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) +} + +interface ActivityEvent { + id: string + type: ActivityEventType + category: ActivityCategory + entityId: string + entityType: string + performedBy: string + isAiAction: boolean + organizationId: string + notes?: string + createdAt: string + propertyId?: string +} + +function groupByDate(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: formatDate(evts[0].createdAt), events: evts })) +} + +function EventRow({ event, isLast }: { event: ActivityEvent; isLast: boolean }) { + const color = EVENT_COLORS[event.type] ?? '#64748b' + const catMeta = CATEGORY_META[event.category] + + return ( + + {!isLast && ( + + )} + + {getEventIcon(event.type)} + + + + + + + {EVENT_LABELS[event.type]} + + + {event.isAiAction && ( + + + + )} + + {event.notes && ( + + {event.notes} + + )} + + {event.isAiAction + ? + : + } + + {event.isAiAction ? 'KI-System' : event.performedBy} + + + + + {formatTime(event.createdAt)} + + + + + ) +} + +interface PropertyActivityLogPanelProps { + propertyId: string +} + +export function PropertyActivityLogPanel({ propertyId }: PropertyActivityLogPanelProps) { + const { data: events = [], isLoading } = usePropertyActivityLog(propertyId) + + if (isLoading) { + return ( + + + + ) + } + + if (events.length === 0) { + return ( + + + Noch keine Aktivitäten für dieses Objekt. + + + ) + } + + const grouped = groupByDate(events) + + return ( + + + + {events.length} Ereignisse · Nur für Verwaltung + + + + {grouped.map(({ key, label, events: dayEvents }) => ( + + + + {label} + + + + {dayEvents.length}× + + + {dayEvents.map((evt, idx) => ( + + ))} + + ))} + + ) +} diff --git a/src/components/supply/PropertyDetailView.tsx b/src/components/supply/PropertyDetailView.tsx index b979090..bee79e8 100644 --- a/src/components/supply/PropertyDetailView.tsx +++ b/src/components/supply/PropertyDetailView.tsx @@ -18,6 +18,7 @@ import type { Property } from '../../domain/property' import type { Match } from '../../domain/match' import type { FutureSignal } from '../../domain/futureSignal' import { usePropertyById, usePropertyMatches, usePropertySignals } from '../../hooks/useProperties' +import { PropertyActivityLogPanel } from './PropertyActivityLogPanel' import { PropertyDetailSkeleton } from './PropertyDetailSkeleton' import { getAssetTypeColor, @@ -310,7 +311,7 @@ function SignalsPanel({ signals }: { signals: FutureSignal[] }) { // ── Main component ──────────────────────────────────────────────────────────── -const TABS = ['Übersicht', 'Verhandlung & Markt', 'Hard Facts', 'Soft Factors', 'Matchability', 'Datenqualität', 'Quelle', 'Signale'] as const +const TABS = ['Übersicht', 'Verhandlung & Markt', 'Hard Facts', 'Soft Factors', 'Matchability', 'Datenqualität', 'Quelle', 'Signale', 'Aktivitätslog'] as const export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewProps) { const [tab, setTab] = useState(0) @@ -414,6 +415,7 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr {tab === 5 && } {tab === 6 && } {tab === 7 && } + {tab === 8 && } ) diff --git a/src/hooks/useActivityLog.ts b/src/hooks/useActivityLog.ts new file mode 100644 index 0000000..df90d2c --- /dev/null +++ b/src/hooks/useActivityLog.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { governanceService } from '../services/governanceService' + +export function usePropertyActivityLog(propertyId: string) { + return useQuery({ + queryKey: ['activityLog', 'property', propertyId], + queryFn: () => governanceService.getActivityLogByProperty(propertyId), + staleTime: 30_000, + select: (res) => res.data ?? [], + enabled: !!propertyId, + }) +} diff --git a/src/services/governanceService.ts b/src/services/governanceService.ts index 5409832..cea91a0 100644 --- a/src/services/governanceService.ts +++ b/src/services/governanceService.ts @@ -33,38 +33,70 @@ export interface ActivityEvent { organizationId: string notes?: string createdAt: string + propertyId?: string } const mockActivityLog: ActivityEvent[] = [ // 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' }, + { 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', propertyId: 'prop-007' }, + { 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', propertyId: 'prop-022' }, // 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-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', propertyId: 'prop-003' }, { 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-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, Repräsentanz-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-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', propertyId: 'prop-001' }, { 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-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', propertyId: 'prop-001' }, { 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) + // Day 5 — 2026-05-17 { 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' }, + { 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', propertyId: 'prop-003' }, + { 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', propertyId: 'prop-015' }, + + // ── Property-specific events (visible in per-property activity log) ─────────── + // prop-001 + { id: 'p1-001', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-001', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Bürofläche Hardturmstrasse 201 neu erfasst', createdAt: '2026-04-02T09:00:00Z', propertyId: 'prop-001' }, + { id: 'p1-002', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-031', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Score 91 — Bedarf GloboCorp: sehr starke Übereinstimmung bei Fläche, Lage, Einzugstermin', createdAt: '2026-05-10T08:14:00Z', propertyId: 'prop-001' }, + { id: 'p1-003', type: 'NEED_CREATED', category: 'SUCHE', entityId: 'need-039', entityType: 'NEED', performedBy: 'demand@helvetia.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Helvetia Versicherungen: Bedarf 1800m² Büro Zürich West — Objekt als Kandidat vorgemerkt', createdAt: '2026-05-12T10:30:00Z', propertyId: 'prop-001' }, + { id: 'p1-004', type: 'MATCH_APPROVED', category: 'MATCHING', entityId: 'match-003', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Match GloboCorp genehmigt — Besichtigung vereinbart für 20.05.2026', createdAt: '2026-05-16T09:00:00Z', propertyId: 'prop-001' }, + { id: 'p1-005', type: 'MATCH_REJECTED', category: 'MATCHING', entityId: 'match-011', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Score 71 — Miete über Budget des Interessenten, abgelehnt', createdAt: '2026-05-17T11:00:00Z', propertyId: 'prop-001' }, + + // prop-003 + { id: 'p3-001', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-003', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Logistikfläche Pratteln initial erfasst', createdAt: '2026-03-15T08:30:00Z', propertyId: 'prop-003' }, + { id: 'p3-002', type: 'DATA_QUALITY_FLAGGED', category: 'GOVERNANCE', entityId: 'prop-003', entityType: 'PROPERTY', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Laderampen-Anzahl fehlt; Deckenhöhe nicht angegeben — Score 68', createdAt: '2026-04-10T07:45:00Z', propertyId: 'prop-003' }, + { id: 'p3-003', 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, Laderampen und Deckenhöhe ergänzt', createdAt: '2026-05-14T10:20:00Z', propertyId: 'prop-003' }, + { id: 'p3-004', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-044', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: '3 Bedarfe ausgewertet — Score-Spanne 64–78, kein STARK-Match', createdAt: '2026-05-15T14:20:00Z', propertyId: 'prop-003' }, + { id: 'p3-005', 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', propertyId: 'prop-003' }, + + // prop-007 + { id: 'p7-001', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-007', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Retail-Fläche Basel Steinenvorstadt erfasst', createdAt: '2026-02-28T11:00:00Z', propertyId: 'prop-007' }, + { id: 'p7-002', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-037', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Score 83 — Bedarf Retail-Kette Innenstadt Basel: guter Match', createdAt: '2026-04-20T09:30:00Z', propertyId: 'prop-007' }, + { id: 'p7-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', propertyId: 'prop-007' }, + + // prop-014 + { id: 'p14-001', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-014', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Logistikhalle Pratteln erfasst', createdAt: '2026-01-10T09:00:00Z', propertyId: 'prop-014' }, + { id: 'p14-002', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-040', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Score 88 — Bedarf Logistik-Unternehmen: starke Übereinstimmung', createdAt: '2026-04-28T10:15:00Z', propertyId: 'prop-014' }, + { id: 'p14-003', type: 'REVIEW_REQUESTED', category: 'REVIEW', entityId: 'match-014', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Match zur Governance-Prüfung eingereicht — Interessent aus EU-Ausland', createdAt: '2026-05-05T13:00:00Z', propertyId: 'prop-014' }, + { id: 'p14-004', type: 'REVIEW_COMPLETED', category: 'REVIEW', entityId: 'match-014', entityType: 'MATCH', performedBy: 'reviewer@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'GENEHMIGT — Bonitätsprüfung bestanden, Match freigegeben', createdAt: '2026-05-07T15:30:00Z', propertyId: 'prop-014' }, + + // prop-015 + { id: 'p15-001', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-015', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Bürofläche Zürich Oerlikon erfasst', createdAt: '2026-02-01T10:00:00Z', propertyId: 'prop-015' }, + { id: 'p15-002', type: 'PROPERTY_UPDATED', category: 'GOVERNANCE', entityId: 'prop-015', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Mietpreis angepasst: CHF 195 → 210/m²/Jahr', createdAt: '2026-03-18T14:30:00Z', propertyId: 'prop-015' }, + { id: 'p15-003', 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', propertyId: 'prop-015' }, ] const store = [...mockActivityLog] @@ -74,6 +106,10 @@ export const governanceService = { const data = organizationId ? store.filter(e => e.organizationId === organizationId) : [...store] return { data: data.sort((a, b) => b.createdAt.localeCompare(a.createdAt)), meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } }, + async getActivityLogByProperty(propertyId: string): Promise> { + const data = store.filter(e => e.propertyId === propertyId) + return { data: data.sort((a, b) => b.createdAt.localeCompare(a.createdAt)), meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, async logEvent(event: Omit): Promise> { const data: ActivityEvent = { id: crypto.randomUUID(), ...event, createdAt: new Date().toISOString() } store.push(data)