feat: remove Administration workspace — keep only Verwaltung + Suche

- Delete all ops page components (ReviewQueue, AIMonitoring, Governance,
  SourceMonitoring, ActivityTimeline, SignalPipeline)
- Remove OPERATIONS workspace from AppShell config, nav order, path detection
- Remove all /ops/* routes from App.tsx
- Remove WorkspaceType.OPERATIONS from allowedWorkspaces in authService,
  sessionStore, permissions
- Keep MarketIntelligence page (already moved to /supply/market-intelligence)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-19 20:32:41 +02:00
parent d22e72f945
commit d15a13e485
378 changed files with 35441 additions and 42 deletions
@@ -0,0 +1,36 @@
import { createContext, useContext, type ReactNode } from 'react'
import { useSessionStore } from '../stores/sessionStore'
import type { MockUser } from '../stores/sessionStore'
// Placeholder AuthContext — swap for real auth (Supabase, Auth0, etc.) later.
// All call sites use this context; no component imports sessionStore directly.
interface AuthContextValue {
user: MockUser | null
isAuthenticated: boolean
isLoading: boolean
login: (user: MockUser) => void
logout: () => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const { currentUser, isAuthenticated, login, logout } = useSessionStore()
const value: AuthContextValue = {
user: currentUser,
isAuthenticated,
isLoading: false, // always resolved in mock mode
login,
logout,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}
@@ -0,0 +1,15 @@
import type { AIOutput, AIOutputType } from '../domain/aiOutput'
import type { ReviewStatus } from '../domain/enums'
export interface AIMonitoringFilters {
type?: AIOutputType
reviewStatus?: ReviewStatus
hasError?: boolean
model?: string
}
export interface IAIMonitoringProvider {
getOutputs(filters?: AIMonitoringFilters): Promise<AIOutput[]>
getOutput(id: string): Promise<AIOutput | null>
updateReviewStatus(id: string, status: ReviewStatus): Promise<AIOutput>
}
@@ -0,0 +1,18 @@
export interface DashboardStats {
totalProperties: number
verifiedProperties: number
externalMarketProperties: number
futureSignalProperties: number
totalMatches: number
pendingReviews: number
approvedMatches: number
activeNeeds: number
totalSignals: number
verifiedSignals: number
averageMatchScore: number
highConfidenceMatches: number
}
export interface IDashboardProvider {
getStats(organizationId?: string): Promise<DashboardStats>
}
@@ -0,0 +1,10 @@
import type { DataSource, ConnectorRun, SourceStatus, TermsStatus, SourceFilters } from '../domain/dataSource'
export interface IDataSourceProvider {
getSources(filters?: SourceFilters): Promise<DataSource[]>
getSource(id: string): Promise<DataSource | null>
getConnectorRuns(sourceId: string): Promise<ConnectorRun[]>
triggerMockRun(sourceId: string): Promise<ConnectorRun>
updateSourceStatus(id: string, status: SourceStatus): Promise<DataSource>
markTermsStatus(id: string, termsStatus: TermsStatus): Promise<DataSource>
}
@@ -0,0 +1,20 @@
import type { FutureSignal } from '../domain/futureSignal'
import type { SignalType, ReviewStatus } from '../domain/enums'
export interface FutureSignalFilters {
signalType?: SignalType
minProbability?: number
organizationId?: string
isVerified?: boolean
sensitivityLevel?: string
reviewStatus?: ReviewStatus
maxTimeHorizonMonths?: number
}
export interface IFutureSignalProvider {
getAll(filters?: FutureSignalFilters): Promise<FutureSignal[]>
getById(id: string): Promise<FutureSignal | null>
getByProperty(propertyId: string): Promise<FutureSignal[]>
verify(id: string, verifiedBy: string): Promise<FutureSignal>
updateReviewStatus(id: string, status: ReviewStatus): Promise<FutureSignal>
}
@@ -0,0 +1,13 @@
import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal'
export interface IMarketIntelligenceProvider {
getSignals(filters?: MarketSignalFilters): Promise<MarketSignal[]>
getSignalById(id: string): Promise<MarketSignal | null>
updateSignalStatus(id: string, status: SignalProcessingStatus): Promise<MarketSignal>
convertToFutureSignal(id: string): Promise<{ futureSignalId: string }>
linkSignalToEntity(
id: string,
entityType: 'property' | 'need',
entityId: string,
): Promise<MarketSignal>
}
@@ -0,0 +1,18 @@
import type { Match } from '../domain/match'
import type { MatchStrength } from '../domain/enums'
export interface MatchFilters {
needId?: string
propertyId?: string
minScore?: number
matchStrength?: MatchStrength
organizationId?: string
}
export interface IMatchProvider {
getAll(filters?: MatchFilters): Promise<Match[]>
getById(id: string): Promise<Match | null>
getByNeed(needId: string): Promise<Match[]>
getByProperty(propertyId: string): Promise<Match[]>
approve(id: string, reviewedBy: string): Promise<Match>
}
@@ -0,0 +1,16 @@
import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need'
import type { AssetType } from '../domain/enums'
export interface NeedFilters {
assetType?: AssetType
organizationId?: string
companyName?: string
}
export interface INeedProvider {
getAll(filters?: NeedFilters): Promise<Need[]>
getById(id: string): Promise<Need | null>
create(data: CreateNeedInput): Promise<Need>
update(id: string, data: UpdateNeedInput): Promise<Need>
remove(id: string): Promise<void>
}
@@ -0,0 +1,19 @@
import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property'
import type { AssetType, ResultType } from '../domain/enums'
export interface PropertyFilters {
assetType?: AssetType
resultType?: ResultType
city?: string
minAreaSqm?: number
maxRentPerSqm?: number
organizationId?: string
}
export interface IPropertyProvider {
getAll(filters?: PropertyFilters): Promise<Property[]>
getById(id: string): Promise<Property | null>
create(data: CreatePropertyInput): Promise<Property>
update(id: string, data: UpdatePropertyInput): Promise<Property>
remove(id: string): Promise<void>
}
@@ -0,0 +1,20 @@
import type { ReviewTask, ReviewPriority, ReviewTaskStatus, ReviewEntityType, ReviewNote } from '../domain/review'
export interface ReviewFilters {
priority?: ReviewPriority
status?: ReviewTaskStatus
entityType?: ReviewEntityType
assignedTo?: string
organizationId?: string
}
export interface IReviewProvider {
getQueue(filters?: ReviewFilters): Promise<ReviewTask[]>
getById(id: string): Promise<ReviewTask | null>
updateStatus(id: string, status: ReviewTaskStatus, userId: string, note?: string): Promise<ReviewTask>
addNote(id: string, note: Omit<ReviewNote, 'id'>): Promise<ReviewTask>
assign(id: string, assignTo: string): Promise<ReviewTask>
// Legacy actions (delegate to updateStatus internally)
approve(id: string, reviewedBy: string, notes?: string): Promise<ReviewTask>
reject(id: string, reviewedBy: string, notes?: string): Promise<ReviewTask>
}
@@ -0,0 +1,19 @@
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist'
import type { ShortlistStatus } from '../domain/enums'
export interface ShortlistFilters {
needId?: string
status?: ShortlistStatus
createdBy?: string
organizationId?: string
}
export interface IShortlistProvider {
getAll(filters?: ShortlistFilters): Promise<Shortlist[]>
getById(id: string): Promise<Shortlist | null>
create(data: CreateShortlistInput): Promise<Shortlist>
update(id: string, data: UpdateShortlistInput): Promise<Shortlist>
addItem(id: string, item: ShortlistItemInput): Promise<Shortlist>
removeItem(id: string, resultId: string): Promise<Shortlist>
remove(id: string): Promise<void>
}
@@ -0,0 +1,8 @@
import type { PipelineState, AuditTrailEntry, GateType } from '../domain/signalPipeline'
export interface ISignalPipelineProvider {
getPipelineState(signalId: string): Promise<PipelineState | null>
evaluateGate(signalId: string, gateType: GateType): Promise<PipelineState>
getAuditTrail(signalId: string): Promise<AuditTrailEntry[]>
publishToFutureAvailability(signalId: string): Promise<PipelineState>
}
@@ -0,0 +1,29 @@
import type { IAIMonitoringProvider, AIMonitoringFilters } from './IAIMonitoringProvider'
import type { AIOutput } from '../domain/aiOutput'
import type { ReviewStatus } from '../domain/enums'
import { mockAIOutputs } from '../mock-data/aiOutputs'
const store: AIOutput[] = [...mockAIOutputs]
export const MockupAIMonitoringProvider: IAIMonitoringProvider = {
async getOutputs(filters?: AIMonitoringFilters) {
let results = [...store]
if (filters?.type) results = results.filter(o => o.type === filters.type)
if (filters?.reviewStatus) results = results.filter(o => o.reviewStatus === filters.reviewStatus)
if (filters?.hasError === true) results = results.filter(o => !!o.error)
if (filters?.hasError === false) results = results.filter(o => !o.error)
if (filters?.model) results = results.filter(o => o.model === filters.model)
return results.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
},
async getOutput(id: string) {
return store.find(o => o.id === id) ?? null
},
async updateReviewStatus(id: string, status: ReviewStatus): Promise<AIOutput> {
const idx = store.findIndex(o => o.id === id)
if (idx === -1) throw new Error(`AIOutput ${id} not found`)
store[idx] = { ...store[idx], reviewStatus: status }
return store[idx]
},
}
@@ -0,0 +1,48 @@
import type { IDashboardProvider, DashboardStats } from './IDashboardProvider'
import { mockProperties } from '../mock-data/properties'
import { mockMatches } from '../mock-data/matches'
import { mockNeeds } from '../mock-data/needs'
import { mockFutureSignals } from '../mock-data/futureSignals'
import { mockReviewQueue } from '../mock-data/reviewQueue'
import { mockDelay } from '../lib/mockUtils'
export const MockupDashboardProvider: IDashboardProvider = {
async getStats(organizationId?) {
await mockDelay()
let props = mockProperties
let matches = mockMatches
let needs = mockNeeds
let signals = mockFutureSignals
let queue = mockReviewQueue
if (organizationId) {
props = props.filter(p => p.organizationId === organizationId)
matches = matches.filter(m => m.organizationId === organizationId)
needs = needs.filter(n => n.organizationId === organizationId)
signals = signals.filter(s => s.organizationId === organizationId)
queue = queue.filter(r => r.relatedOrganizationId === organizationId)
}
const avgScore =
matches.length > 0
? matches.reduce((sum, m) => sum + m.matchScore, 0) / matches.length
: 0
const stats: DashboardStats = {
totalProperties: props.length,
verifiedProperties: props.filter(p => p.resultType === 'VERIFIED_PORTFOLIO').length,
externalMarketProperties: props.filter(p => p.resultType === 'EXTERNAL_MARKET').length,
futureSignalProperties: props.filter(p => p.resultType === 'FUTURE_AVAILABILITY').length,
totalMatches: matches.length,
pendingReviews: queue.filter(r => r.status === 'PENDING').length,
approvedMatches: matches.filter(m => m.isApproved === true).length,
activeNeeds: needs.length,
totalSignals: signals.length,
verifiedSignals: signals.filter(s => s.isVerified).length,
averageMatchScore: Math.round(avgScore),
highConfidenceMatches: matches.filter(m => m.confidenceLevel >= 0.75).length,
}
return stats
},
}
@@ -0,0 +1,75 @@
import type { IDataSourceProvider } from './IDataSourceProvider'
import type { DataSource, ConnectorRun, SourceStatus, TermsStatus, SourceFilters } from '../domain/dataSource'
import { ConnectorRunStatus } from '../domain/dataSource'
import { MOCK_DATA_SOURCES, MOCK_CONNECTOR_RUNS } from '../mock-data/dataSources'
let sources: DataSource[] = [...MOCK_DATA_SOURCES]
let runs: ConnectorRun[] = [...MOCK_CONNECTOR_RUNS]
function applyFilters(items: DataSource[], filters?: SourceFilters): DataSource[] {
if (!filters) return items
return items.filter((s) => {
if (filters.sourceType && s.sourceType !== filters.sourceType) return false
if (filters.status && s.status !== filters.status) return false
if (filters.termsStatus && s.termsStatus !== filters.termsStatus) return false
if (filters.search) {
const q = filters.search.toLowerCase()
if (!s.name.toLowerCase().includes(q) && !s.legalBasis.toLowerCase().includes(q)) return false
}
return true
})
}
export const MockupDataSourceProvider: IDataSourceProvider = {
async getSources(filters?: SourceFilters) {
return applyFilters([...sources], filters)
},
async getSource(id: string) {
return sources.find((s) => s.id === id) ?? null
},
async getConnectorRuns(sourceId: string) {
return runs
.filter((r) => r.sourceId === sourceId)
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
},
async triggerMockRun(sourceId: string) {
const source = sources.find((s) => s.id === sourceId)
const now = new Date().toISOString()
const newRun: ConnectorRun = {
id: `run-${crypto.randomUUID().slice(0, 8)}`,
sourceId,
startedAt: now,
finishedAt: now,
status: ConnectorRunStatus.COMPLETED,
itemsDetected: Math.floor(Math.random() * 200) + 50,
itemsNormalized: Math.floor(Math.random() * 180) + 40,
itemsRejected: Math.floor(Math.random() * 15),
signalsCreated: Math.floor(Math.random() * 8) + 1,
errors: [],
warnings: [],
runSummary: `Demo-Run für ${source?.name ?? sourceId} erfolgreich abgeschlossen.`,
}
runs = [newRun, ...runs]
sources = sources.map((s) =>
s.id === sourceId ? { ...s, lastRunAt: now } : s
)
return newRun
},
async updateSourceStatus(id: string, status: SourceStatus) {
const idx = sources.findIndex((s) => s.id === id)
if (idx === -1) throw new Error(`Source ${id} not found`)
sources[idx] = { ...sources[idx], status }
return sources[idx]
},
async markTermsStatus(id: string, termsStatus: TermsStatus) {
const idx = sources.findIndex((s) => s.id === id)
if (idx === -1) throw new Error(`Source ${id} not found`)
sources[idx] = { ...sources[idx], termsStatus }
return sources[idx]
},
}
@@ -0,0 +1,36 @@
import type { IFutureSignalProvider, FutureSignalFilters } from './IFutureSignalProvider'
import type { FutureSignal } from '../domain/futureSignal'
import type { ReviewStatus } from '../domain/enums'
import { mockFutureSignals } from '../mock-data/futureSignals'
const store: FutureSignal[] = [...mockFutureSignals]
export const MockupFutureSignalProvider: IFutureSignalProvider = {
async getAll(filters?: FutureSignalFilters) {
let results = [...store]
if (filters?.signalType) results = results.filter(s => s.signalType === filters.signalType)
if (filters?.minProbability !== undefined) results = results.filter(s => s.probability >= filters.minProbability!)
if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId)
if (filters?.isVerified !== undefined) results = results.filter(s => s.isVerified === filters.isVerified)
if (filters?.sensitivityLevel) results = results.filter(s => s.sensitivityLevel === filters.sensitivityLevel)
if (filters?.reviewStatus) results = results.filter(s => (s.reviewStatus ?? 'UNREVIEWED') === filters.reviewStatus)
if (filters?.maxTimeHorizonMonths !== undefined) results = results.filter(s => s.timeHorizonMonths <= filters.maxTimeHorizonMonths!)
return results.sort((a, b) => b.probability - a.probability)
},
async getById(id) {
return store.find(s => s.id === id) ?? null
},
async getByProperty(propertyId) {
return store.filter(s => s.propertyId === propertyId)
},
async verify(id, verifiedBy) {
const idx = store.findIndex(s => s.id === id)
store[idx] = { ...store[idx], isVerified: true, verifiedBy, verifiedAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
return store[idx]
},
async updateReviewStatus(id, status: ReviewStatus) {
const idx = store.findIndex(s => s.id === id)
store[idx] = { ...store[idx], reviewStatus: status, updatedAt: new Date().toISOString() }
return store[idx]
},
}
@@ -0,0 +1,70 @@
import type { IMarketIntelligenceProvider } from './IMarketIntelligenceProvider'
import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal'
import { MOCK_MARKET_SIGNALS } from '../mock-data/marketSignals'
// Mutable in-memory copy for status updates
let signals: MarketSignal[] = [...MOCK_MARKET_SIGNALS]
function applyFilters(data: MarketSignal[], filters?: MarketSignalFilters): MarketSignal[] {
if (!filters) return data
return data.filter((s) => {
if (filters.sourceCategory && s.sourceCategory !== filters.sourceCategory) return false
if (filters.processingStatus && s.processingStatus !== filters.processingStatus) return false
if (filters.sensitivityLevel && s.sensitivityLevel !== filters.sensitivityLevel) return false
if (filters.signalType && s.signalType !== filters.signalType) return false
if (filters.search) {
const q = filters.search.toLowerCase()
const match = s.title.toLowerCase().includes(q)
|| s.summary.toLowerCase().includes(q)
|| s.location.toLowerCase().includes(q)
if (!match) return false
}
return true
})
}
export const MockupMarketIntelligenceProvider: IMarketIntelligenceProvider = {
async getSignals(filters?: MarketSignalFilters): Promise<MarketSignal[]> {
return applyFilters(signals, filters)
},
async getSignalById(id: string): Promise<MarketSignal | null> {
return signals.find((s) => s.id === id) ?? null
},
async updateSignalStatus(id: string, status: SignalProcessingStatus): Promise<MarketSignal> {
const idx = signals.findIndex((s) => s.id === id)
if (idx === -1) throw new Error(`Signal ${id} not found`)
signals[idx] = { ...signals[idx], processingStatus: status, updatedAt: new Date().toISOString() }
return signals[idx]
},
async convertToFutureSignal(id: string): Promise<{ futureSignalId: string }> {
const futureSignalId = `fs-${id}-${Date.now()}`
const idx = signals.findIndex((s) => s.id === id)
if (idx !== -1) {
signals[idx] = {
...signals[idx],
processingStatus: 'CONVERTED_TO_FUTURE_AVAILABILITY',
possibleFutureSignalId: futureSignalId,
updatedAt: new Date().toISOString(),
}
}
return { futureSignalId }
},
async linkSignalToEntity(
id: string,
entityType: 'property' | 'need',
entityId: string,
): Promise<MarketSignal> {
const idx = signals.findIndex((s) => s.id === id)
if (idx === -1) throw new Error(`Signal ${id} not found`)
signals[idx] = {
...signals[idx],
...(entityType === 'property' ? { linkedPropertyId: entityId } : { linkedNeedId: entityId }),
updatedAt: new Date().toISOString(),
}
return signals[idx]
},
}
@@ -0,0 +1,32 @@
import type { IMatchProvider, MatchFilters } from './IMatchProvider'
import type { Match } from '../domain/match'
import { mockMatches } from '../mock-data/matches'
export const matchStore: Match[] = [...mockMatches]
const store = matchStore
export const MockupMatchProvider: IMatchProvider = {
async getAll(filters?: MatchFilters) {
let results = [...store]
if (filters?.needId) results = results.filter(m => m.needId === filters.needId)
if (filters?.propertyId) results = results.filter(m => m.propertyId === filters.propertyId)
if (filters?.minScore) results = results.filter(m => m.matchScore >= filters.minScore!)
if (filters?.matchStrength) results = results.filter(m => m.matchStrength === filters.matchStrength)
if (filters?.organizationId) results = results.filter(m => m.organizationId === filters.organizationId)
return results.sort((a, b) => b.matchScore - a.matchScore)
},
async getById(id) {
return store.find(m => m.id === id) ?? null
},
async getByNeed(needId) {
return store.filter(m => m.needId === needId).sort((a, b) => b.matchScore - a.matchScore)
},
async getByProperty(propertyId) {
return store.filter(m => m.propertyId === propertyId).sort((a, b) => b.matchScore - a.matchScore)
},
async approve(id, reviewedBy) {
const idx = store.findIndex(m => m.id === id)
store[idx] = { ...store[idx], isApproved: true, reviewedBy, reviewedAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
return store[idx]
},
}
@@ -0,0 +1,151 @@
import type { INeedProvider, NeedFilters } from './INeedProvider'
import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need'
import { mockNeeds } from '../mock-data/needs'
import { matchStore } from './MockupMatchProvider'
import { propertyStore } from './MockupPropertyProvider'
import { MatchStrength, MatchStatus, RiskLevel } from '../domain/enums'
import type { Match } from '../domain/match'
const store: Need[] = [...mockNeeds]
// ── Location scoring ───────────────────────────────────────────────────────────
const CANTON_MAP: Record<string, string> = {
zürich: 'zh', zug: 'zg', winterthur: 'zh', uster: 'zh', bülach: 'zh', oerlikon: 'zh',
bern: 'be', biel: 'be', thun: 'be', köniz: 'be',
basel: 'bs', muttenz: 'bl', pratteln: 'bl', reinach: 'bl', allschwil: 'bl', binningen: 'bl',
genf: 'ge', genève: 'ge', carouge: 'ge', lancy: 'ge',
'st. gallen': 'sg', 'st.gallen': 'sg', rapperswil: 'sg',
}
function locationScore(propCity: string, preferredLocations: string[]): number {
const pc = propCity.toLowerCase()
for (const pref of preferredLocations) {
const p = pref.toLowerCase()
if (pc.includes(p) || p.includes(pc)) return 1.0
}
// Same canton check
const propCanton = CANTON_MAP[pc]
if (propCanton) {
for (const pref of preferredLocations) {
const prefCanton = CANTON_MAP[pref.toLowerCase()]
if (prefCanton && prefCanton === propCanton) return 0.55
}
}
return 0.30
}
function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSqm: number; location: { city: string } }, need: Need): number | null {
if (need.assetType && prop.assetType !== need.assetType) return null
const locScore = locationScore(prop.location.city, need.preferredLocations ?? [])
// Location dominates: same city → 50-90 base, different → 25-45
let score = locScore >= 0.9 ? 65 : locScore >= 0.5 ? 42 : 28
// Area overlap (+0-20)
if (need.requiredArea && prop.areaSqm) {
const { min, max } = need.requiredArea
if (prop.areaSqm >= min && prop.areaSqm <= max) score += 20
else if (prop.areaSqm >= min * 0.7 && prop.areaSqm <= max * 1.5) score += 10
else if (prop.areaSqm < min * 0.5 || prop.areaSqm > max * 2) score -= 10
}
// Budget fit (+0-10)
if (need.budgetRange?.maxPerSqm && prop.rentPricePerSqm) {
if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm) score += 10
else if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.2) score += 3
else score -= 8
}
// Small jitter so results look natural
score += Math.floor(Math.random() * 6) - 2
return Math.min(97, Math.max(22, score))
}
function strengthFromScore(s: number): string {
if (s >= 75) return MatchStrength.STRONG
if (s >= 55) return MatchStrength.MODERATE
return MatchStrength.WEAK
}
function generateSyntheticMatches(need: Need) {
const now = new Date().toISOString()
for (const prop of propertyStore) {
const score = computeScore(prop, need)
if (score === null || score < 25) continue
const locS = locationScore(prop.location.city, need.preferredLocations ?? [])
const isGoodLoc = locS >= 0.9
const match: Match = {
id: crypto.randomUUID(),
propertyId: prop.id,
needId: need.id,
resultId: prop.id,
resultType: prop.resultType ?? 'VERIFIED_PORTFOLIO',
matchScore: score,
matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength],
status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW,
scoreBreakdown: {
hardMatchScore: score + 5,
softFactorScore: score - 5,
confidenceModifier: isGoodLoc ? 0.96 : 0.82,
dataQualityModifier: 0.92,
totalScore: score,
},
positiveFactors: isGoodLoc
? [{ criterion: 'Standort', weight: 0.25, score: 92, contribution: 23, explanation: `${prop.location.city} bevorzugter Standort` }]
: [{ criterion: 'Fläche', weight: 0.25, score: 70, contribution: 17.5, explanation: `${prop.areaSqm} m² verfügbar` }],
negativeFactors: !isGoodLoc
? [{ criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: `${prop.location.city} liegt außerhalb der bevorzugten Region` }]
: [],
tradeoffs: !isGoodLoc
? [{ criterion: 'Standort', concern: `${prop.location.city} ist nicht im Präferenzgebiet`, severity: 'MEDIUM' as const }]
: [],
explainabilitySummary: isGoodLoc
? `${prop.location.city} trifft den Standortwunsch. Objekt entspricht den Kernkriterien.`
: `Abweichender Standort (${prop.location.city}). Nur bei Engpass im Zielgebiet empfohlen.`,
confidenceLevel: isGoodLoc ? 0.88 : 0.60,
riskLevel: isGoodLoc ? RiskLevel.LOW : RiskLevel.MEDIUM,
uncertaintyIndicators: isGoodLoc ? [] : ['Standort außerhalb Präferenz'],
organizationId: 'org-wincasa',
createdAt: now,
updatedAt: now,
}
matchStore.push(match)
}
}
// ── Provider ───────────────────────────────────────────────────────────────────
export const MockupNeedProvider: INeedProvider = {
async getAll(filters?: NeedFilters) {
let results = [...store]
if (filters?.assetType) results = results.filter(n => n.assetType === filters.assetType)
if (filters?.organizationId) results = results.filter(n => n.organizationId === filters.organizationId)
if (filters?.companyName) results = results.filter(n => n.companyName.toLowerCase().includes(filters.companyName!.toLowerCase()))
return results
},
async getById(id) {
return store.find(n => n.id === id) ?? null
},
async create(data: CreateNeedInput) {
const next: Need = { id: crypto.randomUUID(), ...data, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
store.push(next)
generateSyntheticMatches(next)
return next
},
async update(id, data: UpdateNeedInput) {
const idx = store.findIndex(n => n.id === id)
store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() }
return store[idx]
},
async remove(id) {
const idx = store.findIndex(n => n.id === id)
store.splice(idx, 1)
},
}
@@ -0,0 +1,36 @@
import type { IPropertyProvider, PropertyFilters } from './IPropertyProvider'
import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property'
import { mockProperties } from '../mock-data/properties'
export const propertyStore: Property[] = [...mockProperties]
const store = propertyStore
export const MockupPropertyProvider: IPropertyProvider = {
async getAll(filters?: PropertyFilters) {
let results = [...store]
if (filters?.assetType) results = results.filter(p => p.assetType === filters.assetType)
if (filters?.resultType) results = results.filter(p => p.resultType === filters.resultType)
if (filters?.city) results = results.filter(p => p.location.city.toLowerCase().includes(filters.city!.toLowerCase()))
if (filters?.minAreaSqm) results = results.filter(p => p.areaSqm >= filters.minAreaSqm!)
if (filters?.maxRentPerSqm) results = results.filter(p => p.rentPricePerSqm <= filters.maxRentPerSqm!)
if (filters?.organizationId) results = results.filter(p => p.organizationId === filters.organizationId)
return results
},
async getById(id) {
return store.find(p => p.id === id) ?? null
},
async create(data: CreatePropertyInput) {
const next: Property = { id: crypto.randomUUID(), ...data, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
store.push(next)
return next
},
async update(id, data: UpdatePropertyInput) {
const idx = store.findIndex(p => p.id === id)
store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() }
return store[idx]
},
async remove(id) {
const idx = store.findIndex(p => p.id === id)
store.splice(idx, 1)
},
}
@@ -0,0 +1,76 @@
import type { IReviewProvider, ReviewFilters } from './IReviewProvider'
import type { ReviewTask, ReviewNote } from '../domain/review'
import { mockReviewQueue } from '../mock-data/reviewQueue'
import { mockDelay } from '../lib/mockUtils'
const store: ReviewTask[] = [...mockReviewQueue]
const priorityOrder: Record<string, number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
function makeNote(content: string, createdBy: string): ReviewNote {
return {
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
content,
createdBy,
createdAt: new Date().toISOString(),
}
}
export const MockupReviewProvider: IReviewProvider = {
async getQueue(filters?: ReviewFilters) {
await mockDelay()
let results = [...store]
if (filters?.priority) results = results.filter(r => r.priority === filters.priority)
if (filters?.status) results = results.filter(r => r.status === filters.status)
if (filters?.entityType) results = results.filter(r => r.entityType === filters.entityType)
if (filters?.assignedTo) results = results.filter(r => r.assignedTo === filters.assignedTo)
if (filters?.organizationId) results = results.filter(r => r.relatedOrganizationId === filters.organizationId)
return results.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9))
},
async getById(id) {
await mockDelay()
return store.find(r => r.id === id) ?? null
},
async updateStatus(id, status, userId, note?) {
await mockDelay()
const idx = store.findIndex(r => r.id === id)
const notes = [...store[idx].reviewNotes]
if (note) notes.push(makeNote(note, userId))
store[idx] = { ...store[idx], status, reviewNotes: notes, updatedAt: new Date().toISOString() }
return store[idx]
},
async addNote(id, note) {
await mockDelay()
const idx = store.findIndex(r => r.id === id)
const newNote: ReviewNote = { id: `note-${Date.now()}`, ...note }
store[idx] = {
...store[idx],
reviewNotes: [...store[idx].reviewNotes, newNote],
updatedAt: new Date().toISOString(),
}
return store[idx]
},
async assign(id, assignTo) {
await mockDelay()
const idx = store.findIndex(r => r.id === id)
store[idx] = {
...store[idx],
assignedTo: assignTo,
status: store[idx].status === 'PENDING' ? 'IN_REVIEW' : store[idx].status,
updatedAt: new Date().toISOString(),
}
return store[idx]
},
async approve(id, reviewedBy, notes?) {
return this.updateStatus(id, 'APPROVED', reviewedBy, notes)
},
async reject(id, reviewedBy, notes?) {
return this.updateStatus(id, 'REJECTED', reviewedBy, notes)
},
}
@@ -0,0 +1,67 @@
import type { IShortlistProvider, ShortlistFilters } from './IShortlistProvider'
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist'
import { mockShortlists } from '../mock-data/shortlists'
import { mockDelay } from '../lib/mockUtils'
const store: Shortlist[] = [...mockShortlists]
export const MockupShortlistProvider: IShortlistProvider = {
async getAll(filters?: ShortlistFilters) {
await mockDelay()
let results = [...store]
if (filters?.needId) results = results.filter(s => s.needId === filters.needId)
if (filters?.status) results = results.filter(s => s.status === filters.status)
if (filters?.createdBy) results = results.filter(s => s.createdBy === filters.createdBy)
if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId)
return results
},
async getById(id) {
await mockDelay()
return store.find(s => s.id === id) ?? null
},
async create(data: CreateShortlistInput) {
await mockDelay()
const next: Shortlist = {
id: crypto.randomUUID(),
...data,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
store.push(next)
return next
},
async update(id, data: UpdateShortlistInput) {
await mockDelay()
const idx = store.findIndex(s => s.id === id)
store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() }
return store[idx]
},
async addItem(id, item: ShortlistItemInput) {
await mockDelay()
const idx = store.findIndex(s => s.id === id)
const alreadyAdded = store[idx].items.some(i => i.resultId === item.resultId)
if (!alreadyAdded) {
store[idx] = {
...store[idx],
items: [...store[idx].items, { ...item, addedAt: new Date().toISOString() }],
updatedAt: new Date().toISOString(),
}
}
return store[idx]
},
async removeItem(id, resultId) {
await mockDelay()
const idx = store.findIndex(s => s.id === id)
store[idx] = {
...store[idx],
items: store[idx].items.filter(i => i.resultId !== resultId),
updatedAt: new Date().toISOString(),
}
return store[idx]
},
async remove(id) {
await mockDelay()
const idx = store.findIndex(s => s.id === id)
store.splice(idx, 1)
},
}
@@ -0,0 +1,57 @@
import type { ISignalPipelineProvider } from './ISignalPipelineProvider'
import type { PipelineState } from '../domain/signalPipeline'
import { GateStatus, PipelineStage } from '../domain/signalPipeline'
import type { GateType } from '../domain/signalPipeline'
import { MOCK_PIPELINE_STATES, MOCK_AUDIT_TRAILS } from '../mock-data/signalPipelines'
let states: PipelineState[] = [...MOCK_PIPELINE_STATES]
export const MockupSignalPipelineProvider: ISignalPipelineProvider = {
async getPipelineState(signalId) {
return states.find(s => s.signalId === signalId) ?? null
},
async evaluateGate(signalId, gateType) {
const idx = states.findIndex(s => s.signalId === signalId)
if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`)
// Demo: mark gate as re-evaluated (no real logic change)
const updated: PipelineState = {
...states[idx],
gates: {
...states[idx].gates,
[gateType]: {
...states[idx].gates[gateType as GateType],
evaluatedAt: new Date().toISOString(),
},
},
}
states[idx] = updated
return updated
},
async getAuditTrail(signalId) {
return MOCK_AUDIT_TRAILS.filter(e => e.signalId === signalId)
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
},
async publishToFutureAvailability(signalId) {
const idx = states.findIndex(s => s.signalId === signalId)
if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`)
const now = new Date().toISOString()
states[idx] = {
...states[idx],
publishedToFutureAvailability: true,
publishedAt: now,
currentStage: PipelineStage.STAGE_6_MATCHABLE_RESULT,
gates: {
...states[idx].gates,
FEED_ELIGIBILITY_GATE: {
...states[idx].gates.FEED_ELIGIBILITY_GATE,
status: GateStatus.PASSED,
reason: 'Signal manuell in Future Availability publiziert.',
evaluatedAt: now,
checks: [{ label: 'Manuell publiziert', passed: true, value: 'Ja' }],
},
},
overallEligible: true,
}
return states[idx]
},
}