Files
property-match/src/services/governanceService.ts
T
Benjamin Sutter fdf4de37db feat: Aktivitätslog tab in PropertyDetailView — per-property timeline
Adds a new "Aktivitätslog" tab (9th tab) to the supply-side property
drawer. Shows match approvals/rejections, need registrations, data
quality flags, and governance actions linked to this specific property.
Only visible to Verwaltung users in their own property view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:12:50 +02:00

119 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ListResponse, ItemResponse } from './types'
export type ActivityEventType =
| 'PROPERTY_CREATED'
| 'PROPERTY_UPDATED'
| 'MATCH_APPROVED'
| 'MATCH_REJECTED'
| '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' | 'SHORTLIST' | 'AI_OUTPUT' | 'SOURCE'
performedBy: string
isAiAction: boolean
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', 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', 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, 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 20003000m², 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', 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', 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
{ 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', 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 6478, 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]
export const governanceService = {
async getActivityLog(organizationId?: string): Promise<ListResponse<ActivityEvent>> {
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<ListResponse<ActivityEvent>> {
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<ActivityEvent, 'id' | 'createdAt'>): Promise<ItemResponse<ActivityEvent>> {
const data: ActivityEvent = { id: crypto.randomUUID(), ...event, createdAt: new Date().toISOString() }
store.push(data)
return { data }
},
}