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>
This commit is contained in:
1 parent
c4c29ef7d3
commit
fdf4de37db
4 files changed
+316
-10
No files matched your search
@@ -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<ActivityEventType, string> = {
|
||||||
|
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<ActivityEventType, string> = {
|
||||||
|
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<ActivityCategory, { label: string; color: string }> = {
|
||||||
|
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 <Building2 size={s} color="white" />
|
||||||
|
case 'PROPERTY_UPDATED': return <Edit size={s} color="white" />
|
||||||
|
case 'MATCH_APPROVED': return <CheckCircle size={s} color="white" />
|
||||||
|
case 'MATCH_REJECTED': return <XCircle size={s} color="white" />
|
||||||
|
case 'SIGNAL_VERIFIED': return <TrendingUp size={s} color="white" />
|
||||||
|
case 'NEED_CREATED': return <Search size={s} color="white" />
|
||||||
|
case 'REVIEW_REQUESTED': return <ClipboardList size={s} color="white" />
|
||||||
|
case 'AI_PARSE_COMPLETED':
|
||||||
|
case 'AI_OUTPUT_REVIEWED':
|
||||||
|
case 'MATCH_GENERATED':
|
||||||
|
case 'DECISION_BRIEF_CREATED': return <Bot size={s} color="white" />
|
||||||
|
case 'FUTURE_SIGNAL_DETECTED':
|
||||||
|
case 'FUTURE_SIGNAL_CONVERTED': return <Radar size={s} color="white" />
|
||||||
|
case 'SHORTLIST_CREATED': return <Bookmark size={s} color="white" />
|
||||||
|
case 'SHORTLIST_FINALIZED': return <BookmarkCheck size={s} color="white" />
|
||||||
|
case 'SOURCE_CRAWLED': return <ServerCog size={s} color="white" />
|
||||||
|
case 'DATA_QUALITY_FLAGGED': return <AlertTriangle size={s} color="white" />
|
||||||
|
case 'REVIEW_COMPLETED': return <GitMerge size={s} color="white" />
|
||||||
|
default: return <FileText size={s} color="white" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, ActivityEvent[]>()
|
||||||
|
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 (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.25, position: 'relative' }}>
|
||||||
|
{!isLast && (
|
||||||
|
<Box sx={{ position: 'absolute', left: 12, top: 28, bottom: -4, width: 2, bgcolor: '#e2e8f0', zIndex: 0 }} />
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 26, height: 26, borderRadius: '50%', bgcolor: color,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
flexShrink: 0, zIndex: 1, boxShadow: '0 0 0 3px white', mt: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getEventIcon(event.type)}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0, pb: isLast ? 0 : 1.75 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 1 }}>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.775rem' }}>
|
||||||
|
{EVENT_LABELS[event.type]}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={catMeta.label}
|
||||||
|
size="small"
|
||||||
|
sx={{ height: 14, fontSize: '0.6rem', bgcolor: catMeta.color + '18', color: catMeta.color, fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
{event.isAiAction && (
|
||||||
|
<Tooltip title="KI-System">
|
||||||
|
<Chip
|
||||||
|
label="KI"
|
||||||
|
size="small"
|
||||||
|
sx={{ height: 14, fontSize: '0.6rem', bgcolor: '#f1f5f9', color: '#6366f1', fontWeight: 700, cursor: 'default' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{event.notes && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.2, lineHeight: 1.45, fontSize: '0.72rem' }}>
|
||||||
|
{event.notes}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.4 }}>
|
||||||
|
{event.isAiAction
|
||||||
|
? <Bot size={10} color="#6366f1" />
|
||||||
|
: <User size={10} color="#94a3b8" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem' }}>
|
||||||
|
{event.isAiAction ? 'KI-System' : event.performedBy}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', flexShrink: 0, pt: 0.25 }}>
|
||||||
|
{formatTime(event.createdAt)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropertyActivityLogPanelProps {
|
||||||
|
propertyId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyActivityLogPanel({ propertyId }: PropertyActivityLogPanelProps) {
|
||||||
|
const { data: events = [], isLoading } = usePropertyActivityLog(propertyId)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.85rem' }}>
|
||||||
|
Noch keine Aktivitäten für dieses Objekt.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const grouped = groupByDate(events)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||||
|
{events.length} Ereignisse · Nur für Verwaltung
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{grouped.map(({ key, label, events: dayEvents }) => (
|
||||||
|
<Box key={key} sx={{ mb: 2.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.25 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ fontWeight: 700, fontSize: '0.7rem', color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.4, whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flex: 1, height: 1, bgcolor: '#e2e8f0' }} />
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.65rem', whiteSpace: 'nowrap' }}>
|
||||||
|
{dayEvents.length}×
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{dayEvents.map((evt, idx) => (
|
||||||
|
<EventRow key={evt.id} event={evt} isLast={idx === dayEvents.length - 1} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import type { Property } from '../../domain/property'
|
|||||||
import type { Match } from '../../domain/match'
|
import type { Match } from '../../domain/match'
|
||||||
import type { FutureSignal } from '../../domain/futureSignal'
|
import type { FutureSignal } from '../../domain/futureSignal'
|
||||||
import { usePropertyById, usePropertyMatches, usePropertySignals } from '../../hooks/useProperties'
|
import { usePropertyById, usePropertyMatches, usePropertySignals } from '../../hooks/useProperties'
|
||||||
|
import { PropertyActivityLogPanel } from './PropertyActivityLogPanel'
|
||||||
import { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
|
import { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
|
||||||
import {
|
import {
|
||||||
getAssetTypeColor,
|
getAssetTypeColor,
|
||||||
@@ -310,7 +311,7 @@ function SignalsPanel({ signals }: { signals: FutureSignal[] }) {
|
|||||||
|
|
||||||
// ── Main component ────────────────────────────────────────────────────────────
|
// ── 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) {
|
export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewProps) {
|
||||||
const [tab, setTab] = useState(0)
|
const [tab, setTab] = useState(0)
|
||||||
@@ -414,6 +415,7 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
|
|||||||
{tab === 5 && <DQPanel property={property} />}
|
{tab === 5 && <DQPanel property={property} />}
|
||||||
{tab === 6 && <ProvenancePanel property={property} />}
|
{tab === 6 && <ProvenancePanel property={property} />}
|
||||||
{tab === 7 && <SignalsPanel signals={signals} />}
|
{tab === 7 && <SignalsPanel signals={signals} />}
|
||||||
|
{tab === 8 && <PropertyActivityLogPanel propertyId={propertyId} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -33,38 +33,70 @@ export interface ActivityEvent {
|
|||||||
organizationId: string
|
organizationId: string
|
||||||
notes?: string
|
notes?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
propertyId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockActivityLog: ActivityEvent[] = [
|
const mockActivityLog: ActivityEvent[] = [
|
||||||
// Day 1 — 2026-05-13
|
// 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-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-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-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' },
|
{ 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
|
// 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-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-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-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' },
|
{ 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
|
// 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-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-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-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-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' },
|
{ 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
|
// 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-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-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' },
|
{ 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-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-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-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' },
|
{ 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]
|
const store = [...mockActivityLog]
|
||||||
@@ -74,6 +106,10 @@ export const governanceService = {
|
|||||||
const data = organizationId ? store.filter(e => e.organizationId === organizationId) : [...store]
|
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 } }
|
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>> {
|
async logEvent(event: Omit<ActivityEvent, 'id' | 'createdAt'>): Promise<ItemResponse<ActivityEvent>> {
|
||||||
const data: ActivityEvent = { id: crypto.randomUUID(), ...event, createdAt: new Date().toISOString() }
|
const data: ActivityEvent = { id: crypto.randomUUID(), ...event, createdAt: new Date().toISOString() }
|
||||||
store.push(data)
|
store.push(data)
|
||||||
|
|||||||
Reference in new issue
Block a user