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:
@@ -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]
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user