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,34 @@
export const ActivityAction = {
CREATED: 'CREATED',
UPDATED: 'UPDATED',
DELETED: 'DELETED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
SHORTLISTED: 'SHORTLISTED',
REVIEWED: 'REVIEWED',
VERIFIED: 'VERIFIED',
EXPORTED: 'EXPORTED',
SHARED: 'SHARED',
} as const
export type ActivityAction = typeof ActivityAction[keyof typeof ActivityAction]
export const ActivityEntityType = {
PROPERTY: 'PROPERTY',
NEED: 'NEED',
MATCH: 'MATCH',
FUTURE_SIGNAL: 'FUTURE_SIGNAL',
AI_OUTPUT: 'AI_OUTPUT',
SHORTLIST: 'SHORTLIST',
USER: 'USER',
} as const
export type ActivityEntityType = typeof ActivityEntityType[keyof typeof ActivityEntityType]
export interface ActivityEvent {
id: string
actorId: string // user or system ID that triggered the event
action: ActivityAction
entityType: ActivityEntityType
entityId: string
timestamp: string
metadata?: Record<string, unknown>
}
@@ -0,0 +1,44 @@
import type { ReviewStatus } from './enums'
export const AIOutputType = {
NEED_PARSE: 'NEED_PARSE',
FOLLOW_UP_QUESTIONS: 'FOLLOW_UP_QUESTIONS',
MATCH_EXPLANATION: 'MATCH_EXPLANATION',
COMPARE_SUMMARY: 'COMPARE_SUMMARY',
DECISION_BRIEF: 'DECISION_BRIEF',
DATA_QUALITY_SUMMARY: 'DATA_QUALITY_SUMMARY',
} as const
export type AIOutputType = typeof AIOutputType[keyof typeof AIOutputType]
export const AIErrorType = {
SCHEMA_VALIDATION: 'SCHEMA_VALIDATION',
PROVIDER_TIMEOUT: 'PROVIDER_TIMEOUT',
INVALID_JSON: 'INVALID_JSON',
EMPTY_RESPONSE: 'EMPTY_RESPONSE',
RATE_LIMIT: 'RATE_LIMIT',
} as const
export type AIErrorType = typeof AIErrorType[keyof typeof AIErrorType]
export interface AIOutputError {
type: AIErrorType
message: string
recoverable: boolean
}
export interface AIOutput {
id: string
type: AIOutputType
provider: string
model: string
promptVersion: string
schemaVersion: string
inputHash: string
outputPreview: string
createdAt: string
latencyMs?: number
costEstimate?: number
reviewStatus: ReviewStatus
relatedEntityType: string
relatedEntityId: string
error?: AIOutputError
}
@@ -0,0 +1,38 @@
import type { WorkspaceType } from './enums'
export interface AssistantContext {
currentRoute: string
workspace: WorkspaceType | null
selectedEntityType?: string
selectedEntityId?: string
visibleScores?: Record<string, number>
visibleRisks?: string[]
visibleMissingData?: string[]
availableActions?: string[]
userRole: string
organizationId: string
}
export interface AssistantAction {
id: string
label: string
description: string
actionType: 'NAVIGATE' | 'OPEN_REVIEW' | 'ADD_TO_SHORTLIST' | 'REQUEST_DATA' | 'SEND_TO_REVIEW'
payload?: Record<string, unknown>
}
export interface AssistantMessage {
id: string
role: 'user' | 'assistant'
content: string
createdAt: string
confidence?: number
sources?: string[]
actions?: AssistantAction[]
}
export interface SuggestedQuestion {
id: string
question: string
category: string
}
@@ -0,0 +1,64 @@
export interface KpiCardData {
id: string
label: string
value: number | string
trend?: 'up' | 'down' | 'neutral'
trendLabel?: string
accent?: string
tooltip?: string
onClick?: () => void
}
export interface StrongMatchItem {
matchId: string
propertyId: string
propertyTitle: string
propertyAddress: string
needSummary: string
matchScore: number
topReason: string
missingDataCount: number
nextBestAction: string
}
export interface TimeHorizonDistribution {
short: number // 06 months
medium: number // 612 months
long: number // 1224 months
}
export interface FutureSignalSummary {
total: number
highConfidence: number
restricted: number
needsReview: number
avgTimeHorizonMonths: number
timeHorizonDistribution: TimeHorizonDistribution
}
export interface DataQualitySummary {
avgScore: number
critical: number
propertiesWithMissingCritical: number
topMissingFields: string[]
}
export interface DashboardReviewTask {
id: string
title: string
priority: 'HIGH' | 'MEDIUM' | 'LOW'
status: string
type: string
}
export interface DashboardData {
totalProperties: number
activeProperties: number
strongMatchCount: number
avgDataQuality: number
futureSignals: FutureSignalSummary | null
dataQuality: DataQualitySummary | null
reviewTasks: DashboardReviewTask[] | null
strongMatches: StrongMatchItem[] | null
lastUpdated: string
}
@@ -0,0 +1,147 @@
import type { FreshnessStatus } from './enums'
// ── Connector / Source Type ───────────────────────────────────────────────────
export const DataSourceType = {
API_CONNECTOR: 'API_CONNECTOR',
CSV_IMPORT: 'CSV_IMPORT',
MANUAL_UPLOAD: 'MANUAL_UPLOAD',
PUBLIC_WEB_SOURCE: 'PUBLIC_WEB_SOURCE',
PARTNER_FEED: 'PARTNER_FEED',
INTERNAL_PORTFOLIO_EXPORT: 'INTERNAL_PORTFOLIO_EXPORT',
CONTRACT_METADATA_IMPORT: 'CONTRACT_METADATA_IMPORT',
ANALYST_ENTRY: 'ANALYST_ENTRY',
FUTURE_CRAWLER_STUB: 'FUTURE_CRAWLER_STUB',
} as const
export type DataSourceType = typeof DataSourceType[keyof typeof DataSourceType]
export const DATA_SOURCE_TYPE_LABELS: Record<DataSourceType, string> = {
API_CONNECTOR: 'API-Connector',
CSV_IMPORT: 'CSV-Import',
MANUAL_UPLOAD: 'Manueller Upload',
PUBLIC_WEB_SOURCE: 'Öffentliche Web-Quelle',
PARTNER_FEED: 'Partner-Feed',
INTERNAL_PORTFOLIO_EXPORT: 'Portfolio-Export',
CONTRACT_METADATA_IMPORT: 'Vertrags-Import',
ANALYST_ENTRY: 'Analysten-Eingabe',
FUTURE_CRAWLER_STUB: 'Crawler (geplant)',
}
// ── Source Status ─────────────────────────────────────────────────────────────
export const SourceStatus = {
ACTIVE: 'ACTIVE',
PAUSED: 'PAUSED',
ERROR: 'ERROR',
PENDING_REVIEW: 'PENDING_REVIEW',
DISABLED: 'DISABLED',
} as const
export type SourceStatus = typeof SourceStatus[keyof typeof SourceStatus]
export const SOURCE_STATUS_LABELS: Record<SourceStatus, string> = {
ACTIVE: 'Aktiv',
PAUSED: 'Pausiert',
ERROR: 'Fehler',
PENDING_REVIEW: 'Prüfung ausstehend',
DISABLED: 'Deaktiviert',
}
export const SOURCE_STATUS_COLORS: Record<SourceStatus, { bg: string; fg: string }> = {
ACTIVE: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' },
PAUSED: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' },
ERROR: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' },
PENDING_REVIEW: { bg: 'rgba(59,130,246,0.1)', fg: '#1d4ed8' },
DISABLED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' },
}
// ── Terms / Legal Status ──────────────────────────────────────────────────────
export const TermsStatus = {
APPROVED: 'APPROVED',
NEEDS_LEGAL_REVIEW: 'NEEDS_LEGAL_REVIEW',
RESTRICTED: 'RESTRICTED',
BLOCKED: 'BLOCKED',
UNKNOWN: 'UNKNOWN',
} as const
export type TermsStatus = typeof TermsStatus[keyof typeof TermsStatus]
export const TERMS_STATUS_LABELS: Record<TermsStatus, string> = {
APPROVED: 'Genehmigt',
NEEDS_LEGAL_REVIEW: 'Rechtliche Prüfung',
RESTRICTED: 'Eingeschränkt',
BLOCKED: 'Gesperrt',
UNKNOWN: 'Unbekannt',
}
export const TERMS_STATUS_COLORS: Record<TermsStatus, { bg: string; fg: string; border: string }> = {
APPROVED: { bg: 'rgba(22,163,74,0.08)', fg: '#15803d', border: 'rgba(22,163,74,0.3)' },
NEEDS_LEGAL_REVIEW: { bg: 'rgba(234,179,8,0.08)', fg: '#a16207', border: 'rgba(234,179,8,0.3)' },
RESTRICTED: { bg: 'rgba(249,115,22,0.08)', fg: '#c2410c', border: 'rgba(249,115,22,0.3)' },
BLOCKED: { bg: 'rgba(239,68,68,0.08)', fg: '#dc2626', border: 'rgba(239,68,68,0.3)' },
UNKNOWN: { bg: 'rgba(148,163,184,0.08)', fg: '#64748b', border: 'rgba(148,163,184,0.3)' },
}
// ── Connector Run Status ──────────────────────────────────────────────────────
export const ConnectorRunStatus = {
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
FAILED: 'FAILED',
PARTIAL: 'PARTIAL',
CANCELLED: 'CANCELLED',
} as const
export type ConnectorRunStatus = typeof ConnectorRunStatus[keyof typeof ConnectorRunStatus]
export const CONNECTOR_RUN_STATUS_LABELS: Record<ConnectorRunStatus, string> = {
RUNNING: 'Läuft',
COMPLETED: 'Abgeschlossen',
FAILED: 'Fehlgeschlagen',
PARTIAL: 'Teilweise',
CANCELLED: 'Abgebrochen',
}
export const CONNECTOR_RUN_STATUS_COLORS: Record<ConnectorRunStatus, { bg: string; fg: string }> = {
RUNNING: { bg: 'rgba(99,102,241,0.1)', fg: '#4f46e5' },
COMPLETED: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' },
FAILED: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' },
PARTIAL: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' },
CANCELLED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' },
}
// ── Interfaces ────────────────────────────────────────────────────────────────
export interface DataSource {
id: string
name: string
sourceType: DataSourceType
ownerOrganizationId?: string
legalBasis: string
termsStatus: TermsStatus
dataCategories: string[]
supportedAssetTypes: string[]
regionCoverage: string[]
reliabilityScore: number
freshnessStatus: FreshnessStatus
lastRunAt?: string
nextRunAt?: string
status: SourceStatus
errorState?: string
notes?: string
}
export interface ConnectorRun {
id: string
sourceId: string
startedAt: string
finishedAt?: string
status: ConnectorRunStatus
itemsDetected: number
itemsNormalized: number
itemsRejected: number
signalsCreated: number
errors: string[]
warnings: string[]
runSummary: string
}
export interface SourceFilters {
search?: string
sourceType?: DataSourceType
status?: SourceStatus
termsStatus?: TermsStatus
}
@@ -0,0 +1,172 @@
// ── Asset Types ───────────────────────────────────────────────────────────────
export const AssetType = {
OFFICE: 'OFFICE',
RETAIL: 'RETAIL',
GASTRO: 'GASTRO', // legacy kept for mock-data compatibility
LIGHT_INDUSTRIAL: 'LIGHT_INDUSTRIAL',
LOGISTICS: 'LOGISTICS',
PRODUCTION: 'PRODUCTION',
MIXED: 'MIXED',
UNKNOWN: 'UNKNOWN',
} as const
export type AssetType = typeof AssetType[keyof typeof AssetType]
// ── Result Types ──────────────────────────────────────────────────────────────
export const ResultType = {
VERIFIED_PORTFOLIO: 'VERIFIED_PORTFOLIO',
EXTERNAL_MARKET: 'EXTERNAL_MARKET',
FUTURE_AVAILABILITY: 'FUTURE_AVAILABILITY',
} as const
export type ResultType = typeof ResultType[keyof typeof ResultType]
// ── Match Strength ─────────────────────────────────────────────────────────────
export const MatchStrength = {
STRONG: 'STRONG',
MODERATE: 'MODERATE',
WEAK: 'WEAK',
} as const
export type MatchStrength = typeof MatchStrength[keyof typeof MatchStrength]
// ── Match Status ──────────────────────────────────────────────────────────────
export const MatchStatus = {
PENDING_REVIEW: 'PENDING_REVIEW',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
SHORTLISTED: 'SHORTLISTED',
ARCHIVED: 'ARCHIVED',
} as const
export type MatchStatus = typeof MatchStatus[keyof typeof MatchStatus]
// ── Risk Level ────────────────────────────────────────────────────────────────
export const RiskLevel = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
HIGH: 'HIGH',
CRITICAL: 'CRITICAL',
} as const
export type RiskLevel = typeof RiskLevel[keyof typeof RiskLevel]
// ── Confidence Level (qualitative) ───────────────────────────────────────────
export const ConfidenceLevel = {
VERY_HIGH: 'VERY_HIGH', // >= 0.90
HIGH: 'HIGH', // >= 0.75
MEDIUM: 'MEDIUM', // >= 0.55
LOW: 'LOW', // >= 0.35
VERY_LOW: 'VERY_LOW', // < 0.35
} as const
export type ConfidenceLevel = typeof ConfidenceLevel[keyof typeof ConfidenceLevel]
// ── Data Quality Level (qualitative) ─────────────────────────────────────────
export const DataQualityLevel = {
HIGH: 'HIGH', // >= 0.80
MEDIUM: 'MEDIUM', // >= 0.60
LOW: 'LOW', // < 0.60
INCOMPLETE: 'INCOMPLETE', // missing critical fields
} as const
export type DataQualityLevel = typeof DataQualityLevel[keyof typeof DataQualityLevel]
// ── Availability Status ───────────────────────────────────────────────────────
export const AvailabilityStatus = {
AVAILABLE_NOW: 'AVAILABLE_NOW',
AVAILABLE_SOON: 'AVAILABLE_SOON',
FUTURE_SIGNAL: 'FUTURE_SIGNAL',
OCCUPIED: 'OCCUPIED',
UNKNOWN: 'UNKNOWN',
} as const
export type AvailabilityStatus = typeof AvailabilityStatus[keyof typeof AvailabilityStatus]
// ── Availability Type (structural distinction) ────────────────────────────────
export const AvailabilityType = {
CONFIRMED: 'CONFIRMED', // verified, date known
INDICATIVE: 'INDICATIVE', // external listing, unconfirmed
PROBABILISTIC: 'PROBABILISTIC', // AI signal, no confirmed date
} as const
export type AvailabilityType = typeof AvailabilityType[keyof typeof AvailabilityType]
// ── Source Type ───────────────────────────────────────────────────────────────
export const SourceType = {
ERP_IMPORT: 'ERP_IMPORT',
MANUAL_ENTRY: 'MANUAL_ENTRY',
IMMOSCOUT_SCRAPE: 'IMMOSCOUT_SCRAPE',
HOMEGATE_SCRAPE: 'HOMEGATE_SCRAPE',
NEWHOME_SCRAPE: 'NEWHOME_SCRAPE',
MATCHOFFICE_SCRAPE: 'MATCHOFFICE_SCRAPE',
MAISON_WORK_SCRAPE: 'MAISON_WORK_SCRAPE',
AI_SIGNAL: 'AI_SIGNAL',
PARTNER_FEED: 'PARTNER_FEED',
UNKNOWN: 'UNKNOWN',
} as const
export type SourceType = typeof SourceType[keyof typeof SourceType]
// ── Freshness Status ──────────────────────────────────────────────────────────
export const FreshnessStatus = {
FRESH: 'FRESH', // updated within 48h
STALE: 'STALE', // 214 days old
OUTDATED: 'OUTDATED', // > 14 days old
} as const
export type FreshnessStatus = typeof FreshnessStatus[keyof typeof FreshnessStatus]
/** @deprecated Use FreshnessStatus — kept for mock-data backward compatibility */
export const DataFreshness = FreshnessStatus
export type DataFreshness = FreshnessStatus
// ── Review Status ─────────────────────────────────────────────────────────────
export const ReviewStatus = {
UNREVIEWED: 'UNREVIEWED',
IN_REVIEW: 'IN_REVIEW',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
FLAGGED: 'FLAGGED',
} as const
export type ReviewStatus = typeof ReviewStatus[keyof typeof ReviewStatus]
// ── Sensitivity Level ─────────────────────────────────────────────────────────
export const SensitivityLevel = {
PUBLIC: 'PUBLIC',
INTERNAL: 'INTERNAL',
CONFIDENTIAL: 'CONFIDENTIAL',
RESTRICTED: 'RESTRICTED',
} as const
export type SensitivityLevel = typeof SensitivityLevel[keyof typeof SensitivityLevel]
// ── Shortlist Status ──────────────────────────────────────────────────────────
export const ShortlistStatus = {
DRAFT: 'DRAFT',
REVIEW_READY: 'REVIEW_READY',
FINALIZED: 'FINALIZED',
ACTIVE: 'ACTIVE',
SHARED: 'SHARED',
ARCHIVED: 'ARCHIVED',
CONVERTED: 'CONVERTED',
} as const
export type ShortlistStatus = typeof ShortlistStatus[keyof typeof ShortlistStatus]
// ── User Role ─────────────────────────────────────────────────────────────────
export const UserRole = {
SUPER_ADMIN: 'SUPER_ADMIN',
ORGANIZATION_ADMIN: 'ORGANIZATION_ADMIN',
PROPERTY_MANAGER: 'PROPERTY_MANAGER',
REVIEWER: 'REVIEWER',
OWNER_VIEWER: 'OWNER_VIEWER',
DEMAND_USER: 'DEMAND_USER',
} as const
export type UserRole = typeof UserRole[keyof typeof UserRole]
// ── Signal Type ───────────────────────────────────────────────────────────────
export const SignalType = {
EXPANSION: 'EXPANSION',
POSSIBLE_MOVE_OUT: 'POSSIBLE_MOVE_OUT',
CONSTRUCTION_PROJECT: 'CONSTRUCTION_PROJECT',
RESTRUCTURING: 'RESTRUCTURING',
PROJECT_DEVELOPMENT: 'PROJECT_DEVELOPMENT',
SPACE_CONSOLIDATION: 'SPACE_CONSOLIDATION',
} as const
export type SignalType = typeof SignalType[keyof typeof SignalType]
// ── Workspace ─────────────────────────────────────────────────────────────────
export const WorkspaceType = {
SUPPLY: 'SUPPLY',
DEMAND: 'DEMAND',
OPERATIONS: 'OPERATIONS',
} as const
export type WorkspaceType = typeof WorkspaceType[keyof typeof WorkspaceType]
@@ -0,0 +1,45 @@
import type { SignalType, RiskLevel, ReviewStatus } from './enums'
export interface SignalSource {
type: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL'
url?: string
publishedAt?: string
credibility: 'LOW' | 'MEDIUM' | 'HIGH'
}
export interface SignalEvidence {
summary: string
sourceUrls?: string[]
extractedAt?: string
}
export interface FutureSignal {
id: string
signalType: SignalType
companyName?: string
propertyId?: string
locationHint: string
areaSqmEstimate?: number
probability: number
confidenceScore: number
timeHorizonMonths: number
source: SignalSource
sensitivityLevel: 'PUBLIC' | 'INTERNAL' | 'CONFIDENTIAL'
disclaimer: string
riskLevel: RiskLevel
marketIndicator?: string
relevanceScore?: number
isVerified: boolean
verifiedBy?: string
verifiedAt?: string
expiresAt?: string
organizationId?: string
createdAt: string
updatedAt: string
// F004 additions
title?: string // short human-readable headline
evidence?: SignalEvidence // structured evidence block
matchabilityScore?: number // 0100: how well this signal can be matched to needs
reviewStatus?: ReviewStatus
}
@@ -0,0 +1,10 @@
export * from './enums'
export * from './property'
export * from './need'
export * from './match'
export * from './futureSignal'
export * from './aiOutput'
export * from './activityEvent'
export * from './unifiedResult'
export * from './shortlist'
export * from './review'
@@ -0,0 +1,180 @@
import type { AssetType, SignalType, SensitivityLevel, FreshnessStatus } from './enums'
// ── Source Categories ─────────────────────────────────────────────────────────
export const MarketSignalSourceCategory = {
PUBLIC_LISTING_PLATFORM: 'PUBLIC_LISTING_PLATFORM',
BUILDING_PERMIT_REGISTER: 'BUILDING_PERMIT_REGISTER',
COMPANY_NEWS: 'COMPANY_NEWS',
JOB_GROWTH_SIGNAL: 'JOB_GROWTH_SIGNAL',
COMMERCIAL_REGISTER: 'COMMERCIAL_REGISTER',
INFRASTRUCTURE_PROJECT: 'INFRASTRUCTURE_PROJECT',
PORTFOLIO_IMPORT: 'PORTFOLIO_IMPORT',
LEASE_EXPIRY_DATA: 'LEASE_EXPIRY_DATA',
USER_DEMAND_SIGNAL: 'USER_DEMAND_SIGNAL',
MANUAL_ANALYST_SIGNAL: 'MANUAL_ANALYST_SIGNAL',
} as const
export type MarketSignalSourceCategory = typeof MarketSignalSourceCategory[keyof typeof MarketSignalSourceCategory]
export const MARKET_SIGNAL_SOURCE_LABELS: Record<MarketSignalSourceCategory, string> = {
PUBLIC_LISTING_PLATFORM: 'Listing-Plattform',
BUILDING_PERMIT_REGISTER: 'Baugesuch-Register',
COMPANY_NEWS: 'Unternehmensnachrichten',
JOB_GROWTH_SIGNAL: 'Stellenwachstum',
COMMERCIAL_REGISTER: 'Handelsregister',
INFRASTRUCTURE_PROJECT: 'Infrastrukturprojekt',
PORTFOLIO_IMPORT: 'Portfolio-Import',
LEASE_EXPIRY_DATA: 'Vertragslaufdaten',
USER_DEMAND_SIGNAL: 'Nutzernachfrage',
MANUAL_ANALYST_SIGNAL: 'Analyst-Signal',
}
// LEASE_EXPIRY_DATA and PORTFOLIO_IMPORT are sensitive — never expose raw to Demand Users
export const SENSITIVE_SOURCE_CATEGORIES: MarketSignalSourceCategory[] = [
MarketSignalSourceCategory.LEASE_EXPIRY_DATA,
MarketSignalSourceCategory.PORTFOLIO_IMPORT,
]
// ── Processing Status ─────────────────────────────────────────────────────────
export const SignalProcessingStatus = {
DETECTED: 'DETECTED',
NORMALIZED: 'NORMALIZED',
ENRICHED: 'ENRICHED',
NEEDS_REVIEW: 'NEEDS_REVIEW',
APPROVED_AS_SIGNAL: 'APPROVED_AS_SIGNAL',
REJECTED: 'REJECTED',
CONVERTED_TO_FUTURE_AVAILABILITY: 'CONVERTED_TO_FUTURE_AVAILABILITY',
ARCHIVED: 'ARCHIVED',
} as const
export type SignalProcessingStatus = typeof SignalProcessingStatus[keyof typeof SignalProcessingStatus]
export const SIGNAL_PROCESSING_STATUS_LABELS: Record<SignalProcessingStatus, string> = {
DETECTED: 'Erkannt',
NORMALIZED: 'Normalisiert',
ENRICHED: 'Angereichert',
NEEDS_REVIEW: 'Prüfung erforderlich',
APPROVED_AS_SIGNAL: 'Genehmigt',
REJECTED: 'Abgelehnt',
CONVERTED_TO_FUTURE_AVAILABILITY: 'Konvertiert',
ARCHIVED: 'Archiviert',
}
export const SIGNAL_PROCESSING_STATUS_COLORS: Record<SignalProcessingStatus, { bg: string; fg: string }> = {
DETECTED: { bg: 'rgba(148,163,184,0.15)', fg: '#64748b' },
NORMALIZED: { bg: 'rgba(59,130,246,0.12)', fg: '#2563eb' },
ENRICHED: { bg: 'rgba(99,102,241,0.12)', fg: '#4f46e5' },
NEEDS_REVIEW: { bg: 'rgba(245,158,11,0.12)', fg: '#d97706' },
APPROVED_AS_SIGNAL: { bg: 'rgba(34,197,94,0.12)', fg: '#16a34a' },
REJECTED: { bg: 'rgba(239,68,68,0.12)', fg: '#dc2626' },
CONVERTED_TO_FUTURE_AVAILABILITY: { bg: 'rgba(139,92,246,0.12)', fg: '#7c3aed' },
ARCHIVED: { bg: 'rgba(148,163,184,0.10)', fg: '#94a3b8' },
}
// ── Evidence & Entities ───────────────────────────────────────────────────────
export const ExtractedEntityType = {
COMPANY: 'COMPANY',
PERSON: 'PERSON',
LOCATION: 'LOCATION',
ASSET: 'ASSET',
DATE: 'DATE',
} as const
export type ExtractedEntityType = typeof ExtractedEntityType[keyof typeof ExtractedEntityType]
export interface ExtractedEntity {
type: ExtractedEntityType
value: string
confidence: number
}
export const EvidenceType = {
TEXT_EXCERPT: 'TEXT_EXCERPT',
URL_REFERENCE: 'URL_REFERENCE',
ANALYST_NOTE: 'ANALYST_NOTE',
DOCUMENT: 'DOCUMENT',
} as const
export type EvidenceType = typeof EvidenceType[keyof typeof EvidenceType]
export interface SignalEvidence {
id: string
signalId: string
evidenceType: EvidenceType
content: string
sourceUrl?: string
retrievedAt: string
confidence: number
}
// ── Core Signal ───────────────────────────────────────────────────────────────
export interface MarketSignal {
id: string
title: string
summary: string
sourceCategory: MarketSignalSourceCategory
sourceLabel: string
sourceUrl?: string
detectedAt: string
location: string
affectedAssetTypes: AssetType[]
signalType: SignalType
rawEvidenceSummary: string
extractedEntities: ExtractedEntity[]
evidence: SignalEvidence[]
sourceReliabilityScore: number // 01
confidenceScore: number // 01
sensitivityLevel: SensitivityLevel
freshnessStatus: FreshnessStatus
processingStatus: SignalProcessingStatus
linkedPropertyId?: string
linkedNeedId?: string
possibleFutureSignalId?: string
analystNotes: string
createdAt: string
updatedAt: string
}
// ── Intelligence Aggregates ───────────────────────────────────────────────────
export interface MarketInsight {
id: string
title: string
summary: string
signalIds: string[]
location: string
affectedAssetTypes: AssetType[]
confidenceScore: number
createdAt: string
analystId: string
}
export interface IntelligenceRun {
id: string
triggeredAt: string
completedAt?: string
signalsDetected: number
signalsProcessed: number
status: 'RUNNING' | 'COMPLETED' | 'FAILED'
sourceCategories: MarketSignalSourceCategory[]
}
export interface SignalConversionCandidate {
signalId: string
proposedTitle: string
proposedSummary: string
estimatedAvailabilityDate?: string
proposedConfidence: number
conversionRationale: string
requiresReview: boolean
}
// ── Filters ───────────────────────────────────────────────────────────────────
export interface MarketSignalFilters {
sourceCategory?: MarketSignalSourceCategory
processingStatus?: SignalProcessingStatus
sensitivityLevel?: SensitivityLevel
signalType?: SignalType
search?: string
}
@@ -0,0 +1,109 @@
import type { MatchStrength, MatchStatus, RiskLevel, ResultType, ConfidenceLevel } from './enums'
// ── Score Building Blocks ─────────────────────────────────────────────────────
export interface ScoreBreakdown {
hardMatchScore: number // 0100 hard criteria score
softFactorScore: number // 0100 soft factors score
confidenceModifier: number // -20 to +5 adjustment
dataQualityModifier: number // -15 to 0 adjustment
totalScore: number // final 0100
}
export interface ScoreFactor {
criterion: string
weight: number // 01 relative weight
score: number // 0100
contribution: number // weighted points added to total
explanation: string
}
// ── Explainability Types ──────────────────────────────────────────────────────
/** Canonical tradeoff type (F004) */
export interface TradeOff {
criterion: string
concern: string
severity: 'LOW' | 'MEDIUM' | 'HIGH'
mitigation?: string
impactOnScore?: number
}
/** @deprecated Use TradeOff — kept for backward compatibility */
export type Tradeoff = TradeOff
export interface Risk {
category: string // e.g. "Datenverfügbarkeit", "Standortrisiko"
description: string
level: RiskLevel
mitigation?: string
}
export interface MissingDataItem {
field: string
importance: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
description: string
impact: string // how it affects the match score
}
export interface NextBestAction {
label: string // e.g. "Besichtigung anfragen"
description?: string
priority: 'HIGH' | 'MEDIUM' | 'LOW'
actionType: 'CONTACT' | 'VERIFY' | 'REVIEW' | 'SHORTLIST' | 'COMPARE' | 'SCHEDULE'
externalUrl?: string
}
export interface AlternativeStrategy {
title: string
description: string
expectedScore?: number
reasoning?: string
}
// ── Match (core entity) ───────────────────────────────────────────────────────
export interface Match {
id: string
needId: string
// Result reference — works for all three result types
resultId?: string // preferred: generic result ID
resultType?: ResultType
propertyId: string // legacy alias for resultId (VERIFIED_PORTFOLIO)
// Scoring
matchScore: number // 0100
matchStrength: MatchStrength
scoreBreakdown: ScoreBreakdown
confidenceLevel: number // 01 numeric
confidenceLevelLabel?: ConfidenceLevel
dataConfidenceScore?: number // 01 data quality contribution
// Explainability
positiveFactors: ScoreFactor[]
negativeFactors: ScoreFactor[]
tradeoffs: TradeOff[]
tradeOffs?: TradeOff[] // alias for F004 naming convention
risks?: Risk[]
missingData?: MissingDataItem[]
nextBestActions?: NextBestAction[]
explainabilitySummary: string
explanation?: string // alias / longer form
// Risk
riskLevel: RiskLevel
uncertaintyIndicators: string[]
// Alternatives
alternativeStrategies?: AlternativeStrategy[]
// Review / lifecycle
status?: MatchStatus
isApproved?: boolean // legacy — prefer status
reviewedBy?: string
reviewedAt?: string
organizationId?: string
createdAt: string
updatedAt: string
}
@@ -0,0 +1,91 @@
import type { AssetType, MatchStatus } from './enums'
export interface AreaRange {
min: number
max: number
}
export interface BudgetRange {
minPerSqm?: number
maxPerSqm: number
maxMonthlyTotal?: number
currency: string
}
export interface Timing {
earliestMoveIn: string
latestMoveIn: string
contractDurationMonths?: number
flexibleTiming: boolean
}
export interface WeightingProfile {
area: number
location: number
budget: number
timing: number
prestige: number
accessibility: number
expansionPotential: number
flexibility: number
[key: string]: number
}
export interface SoftFactorPreferences {
minPrestige?: number
minAccessibility?: number
requireParking?: boolean
maxPublicTransportMinutes?: number
preferredEsgRating?: string
requireHighVisibility?: boolean
}
export interface SizeRange {
minSqm: number
maxSqm: number
}
export interface MustHaveCriterion {
criterion: string
description?: string
weight?: number // 01, how much a miss hurts the score
}
export interface WeightedPreference {
criterion: string
weight: number // 01
idealValue?: string | number
description?: string
}
export interface Need {
id: string
companyName: string
contactName?: string
assetType: AssetType
requiredArea: AreaRange
// F004 additions
desiredLocation?: string[] // preferred city/district list
sizeRange?: SizeRange // structured alias for requiredArea
mustHaveCriteria?: MustHaveCriterion[]
weightedPreferences?: WeightedPreference[]
status?: MatchStatus | 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'CLOSED'
preferredLocations: string[]
excludedLocations?: string[]
budgetRange: BudgetRange
timing: Timing
mustCriteriaText?: string[] // legacy — prefer mustHaveCriteria
softFactors?: SoftFactorPreferences
weightingProfile: WeightingProfile
confidenceInCriteria: number
extractedFromText?: string
notes?: string
organizationId?: string
createdAt: string
updatedAt: string
}
export type CreateNeedInput = Omit<Need, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateNeedInput = Partial<CreateNeedInput>
@@ -0,0 +1,69 @@
import type { AssetType } from './enums'
export interface ParsedNeedCriteria {
assetType?: AssetType
areaRange?: { min: number; max: number }
preferredLocations?: string[]
budgetRange?: { maxPerSqm: number; maxMonthlyTotal?: number; currency: string }
timing?: { earliestMoveIn: string; latestMoveIn?: string; contractDurationMonths?: number; flexibleTiming: boolean }
mustHaveCriteria?: string[]
softFactors?: { minPrestige?: number; requireParking?: boolean; maxPublicTransportMinutes?: number; requireHighVisibility?: boolean }
infrastructureRequirements?: string[]
accessibilityRequirements?: string[]
prestigeImportance?: 'LOW' | 'MEDIUM' | 'HIGH'
flexibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
expansionPotential?: boolean
parkingNeed?: boolean
visibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
footfallNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
companyName?: string
notes?: string
}
export interface FollowUpQuestion {
id: string
questionText: string
targetField: string
reason: string
suggestedAnswerOptions?: string[]
importance: 'required' | 'recommended' | 'optional'
}
export interface ParseNeedResult {
extractedCriteria: ParsedNeedCriteria
confidenceByField: Record<string, number>
missingFields: string[]
assumptions: string[]
suggestedWeights: Record<string, number>
followUpQuestionCandidates: FollowUpQuestion[]
rawSummary: string
promptVersion: string
schemaVersion: string
}
export const NeedBuilderStep = {
IDLE: 'idle',
PARSING: 'parsing',
PARSED_REQUIRES_REVIEW: 'parsed_requires_review',
CLARIFICATION_REQUIRED: 'clarification_required',
WEIGHTING_REVIEW: 'weighting_review',
READY_TO_SAVE: 'ready_to_save',
SAVING: 'saving',
SAVED: 'saved',
ERROR: 'error',
} as const
export type NeedBuilderStep = typeof NeedBuilderStep[keyof typeof NeedBuilderStep]
export const WEIGHTING_KEYS = ['area', 'location', 'budget', 'timing', 'prestige', 'accessibility', 'expansionPotential', 'flexibility'] as const
export type WeightingKey = typeof WEIGHTING_KEYS[number]
export const WEIGHTING_LABELS: Record<WeightingKey, string> = {
area: 'Fläche',
location: 'Standort',
budget: 'Budget',
timing: 'Verfügbarkeit',
prestige: 'Prestige',
accessibility: 'Erreichbarkeit',
expansionPotential: 'Expansionspotenzial',
flexibility: 'Flexibilität',
}
@@ -0,0 +1,139 @@
import type {
AssetType, ResultType, AvailabilityStatus, AvailabilityType,
FreshnessStatus, RiskLevel, SourceType, DataQualityLevel,
} from './enums'
// ── Location / Address ────────────────────────────────────────────────────────
export interface Location {
city: string
district?: string
canton?: string
region?: string
country: string
coordinates?: { lat: number; lng: number }
}
export interface Address {
street: string
houseNumber: string
postalCode: string
city: string
country: string
}
// ── Source Metadata ───────────────────────────────────────────────────────────
export interface SourceMeta {
sourceType: SourceType | string
sourceLabel?: string
sourceUrl?: string
sourceUpdatedAt?: string
externalId?: string
}
// ── Data Quality ──────────────────────────────────────────────────────────────
export interface DataQuality {
score: number
qualityLevel?: DataQualityLevel
missingCriticalFields: string[]
missingOptionalFields: string[]
lastVerifiedAt?: string
freshness: FreshnessStatus
warnings: string[]
}
// ── Hard Facts ────────────────────────────────────────────────────────────────
export interface PropertyHardFacts {
floor?: number
fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM'
parking?: number
publicTransportScore?: number
usageType?: string
ceilingHeightM?: number
loadingDocksCount?: number
powerSupplyKva?: number
hasServerRoom?: boolean
isBarrierFree?: boolean
}
// ── Soft Factors ──────────────────────────────────────────────────────────────
export interface SoftFactors {
prestigeScore?: number
visibilityScore?: number
footfallScore?: number
commuterAccessScore?: number
talentAccessScore?: number
esgScore?: number
flexibilityScore?: number
expansionPotentialScore?: number
taxEnvironmentScore?: number
// Legacy aliases kept for mock-data backward compatibility
prestige?: number
accessibility?: number
talentAccess?: number
esgRating?: string
passerbyFrequency?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
parkingSpots?: number
publicTransportMinutes?: number
infrastructureNotes?: string
}
// ── Property ──────────────────────────────────────────────────────────────────
export interface Property {
id: string
organizationId?: string
title: string
assetType: AssetType
resultType: ResultType
location: Location
address: Address
areaSqm: number
areaSqmMin?: number
areaSqmMax?: number
rentPricePerSqm: number
rentChfSqmYear?: number
totalRentMonthly?: number
ancillaryCosts?: number
availabilityDate: string
availabilityStatus: AvailabilityStatus
availabilityType?: AvailabilityType
sourceType: string
sourceLabel?: string
sourceUrl?: string
sourceUpdatedAt?: string
sourceMeta?: SourceMeta
confidenceScore: number
dataQuality: DataQuality
softFactors?: SoftFactors
hardFacts?: PropertyHardFacts
// Legacy fields — kept for backward compat
floorLevel?: number
expansionPotentialSqm?: number
contractDurationMonths?: number
riskLevel?: RiskLevel
description?: string
images?: string[]
status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED'
lastReviewedAt?: string
createdAt: string
updatedAt: string
}
export type CreatePropertyInput = Omit<Property, 'id' | 'createdAt' | 'updatedAt'>
export type UpdatePropertyInput = Partial<CreatePropertyInput>
@@ -0,0 +1,79 @@
// ── Entity Types ──────────────────────────────────────────────────────────────
export const ReviewEntityType = {
FUTURE_SIGNAL: 'FUTURE_SIGNAL',
MATCH_EXPLANATION: 'MATCH_EXPLANATION',
LOW_CONFIDENCE_MATCH:'LOW_CONFIDENCE_MATCH',
CONTACT_RELEASE: 'CONTACT_RELEASE',
AI_OUTPUT: 'AI_OUTPUT',
PROPERTY_DATA_ISSUE: 'PROPERTY_DATA_ISSUE',
} as const
export type ReviewEntityType = typeof ReviewEntityType[keyof typeof ReviewEntityType]
// ── Task Status ───────────────────────────────────────────────────────────────
export const ReviewTaskStatus = {
PENDING: 'PENDING',
IN_REVIEW: 'IN_REVIEW',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
NEEDS_MORE_DATA: 'NEEDS_MORE_DATA',
ESCALATED: 'ESCALATED',
} as const
export type ReviewTaskStatus = typeof ReviewTaskStatus[keyof typeof ReviewTaskStatus]
// ── Priority ──────────────────────────────────────────────────────────────────
export const ReviewPriority = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
HIGH: 'HIGH',
CRITICAL: 'CRITICAL',
} as const
export type ReviewPriority = typeof ReviewPriority[keyof typeof ReviewPriority]
// ── Note ──────────────────────────────────────────────────────────────────────
export interface ReviewNote {
id: string
content: string
createdBy: string
createdAt: string
}
// ── Task ──────────────────────────────────────────────────────────────────────
export interface ReviewTask {
id: string
entityType: ReviewEntityType
entityId: string
title: string
description?: string
priority: ReviewPriority
status: ReviewTaskStatus
assignedTo?: string
createdBy: string
createdAt: string
updatedAt: string
dueDate?: string
reviewNotes: ReviewNote[]
relatedOrganizationId?: string
// Risk / confidence context
confidenceScore?: number
riskLevel?: string
// AI-output context
promptVersion?: string
// Legacy compat (match-based items)
matchId?: string
needId?: string
propertyId?: string
matchScore?: number
}
// ── Backward-compat aliases ───────────────────────────────────────────────────
export type ReviewQueueItem = ReviewTask
export type ReviewQueueStatus = ReviewTaskStatus
/** @deprecated use ReviewTaskStatus */
export const ReviewQueueStatus = ReviewTaskStatus
@@ -0,0 +1,143 @@
import type { ScoreFactor, TradeOff, Risk, MissingDataItem, NextBestAction } from './match'
// ── Hard Filter Thresholds ────────────────────────────────────────────────────
export const HARD_FILTER = {
AREA_MIN_TOLERANCE: 0.85, // exclude if property < 85% of need's min area
AREA_MAX_RATIO: 2.50, // severe penalty if property > 2.5× need's max area
BUDGET_EXCLUSION_RATIO: 1.50, // exclude if rent > 150% of max budget/m²
BUDGET_SEVERE_RATIO: 1.25, // severe penalty if 125150% over budget
BUDGET_MODERATE_RATIO: 1.10, // mild penalty if 110125% over budget
TIMING_GRACE_DAYS: 90, // allow ±90 days window flexibility
} as const
// ── Score Architecture ────────────────────────────────────────────────────────
// Within each group, scores are weighted and normalized to 0100.
// Final = baseScore + dataQualityModifier + confidenceModifier, clamped 0100.
export const SCORE_SPLIT = {
HARD_CRITERIA: 0.60, // expected contribution from hard criteria group
SOFT_FACTORS: 0.40, // expected contribution from soft factors group
} as const
export const HARD_CRITERION_KEYS = ['area', 'location', 'budget', 'timing'] as const
export type HardCriterionKey = typeof HARD_CRITERION_KEYS[number]
export const SOFT_FACTOR_KEYS = [
'prestige', 'accessibility', 'expansionPotential', 'flexibility',
'visibility', 'footfall', 'talentAccess', 'esg', 'taxEnvironment',
] as const
export type SoftFactorKey = typeof SOFT_FACTOR_KEYS[number]
// ── Modifier Tables ───────────────────────────────────────────────────────────
export const DATA_QUALITY_MODIFIER = {
EXCELLENT: +5, // dataQuality.score >= 0.85
GOOD: 0, // >= 0.70
FAIR: -5, // >= 0.55
POOR: -10, // >= 0.40
CRITICAL: -15, // < 0.40
} as const
export const CONFIDENCE_MODIFIER = {
VERIFIED_HIGH: +3, // VERIFIED_PORTFOLIO + confidenceScore >= 0.80
VERIFIED_MEDIUM: 0, // VERIFIED_PORTFOLIO + confidenceScore < 0.80
EXTERNAL_MARKET: -3, // EXTERNAL_MARKET result type
FUTURE_AVAILABILITY: -15, // FUTURE_AVAILABILITY — never treat as confirmed availability
LOW_CONFIDENCE: -10, // confidenceScore < 0.50 (stacks with above)
} as const
// ── Scoring Weight Profile ────────────────────────────────────────────────────
export interface ScoringWeightProfile {
// Hard criteria
area: number
location: number
budget: number
timing: number
// Soft factors
prestige: number
accessibility: number
expansionPotential: number
flexibility: number
visibility: number
footfall: number
talentAccess: number
esg: number
taxEnvironment: number
[key: string]: number
}
// ── Default Profiles per Asset Type ──────────────────────────────────────────
// Each profile sums to 1.00. No magic numbers — weights reflect domain logic.
export const DEFAULT_SCORING_PROFILES: Record<string, ScoringWeightProfile> = {
// Büro: ÖV-Anbindung, Talent Access, Prestige, ESG stark gewichtet
OFFICE: {
area: 0.18, location: 0.18, budget: 0.15, timing: 0.09,
prestige: 0.07, accessibility: 0.11, expansionPotential: 0.05,
flexibility: 0.05, visibility: 0.02, footfall: 0.01,
talentAccess: 0.07, esg: 0.02, taxEnvironment: 0.00,
},
// Retail: Frequenz, Sichtbarkeit und Standort dominieren
RETAIL: {
area: 0.10, location: 0.15, budget: 0.13, timing: 0.06,
prestige: 0.04, accessibility: 0.07, expansionPotential: 0.03,
flexibility: 0.08, visibility: 0.14, footfall: 0.18,
talentAccess: 0.01, esg: 0.01, taxEnvironment: 0.00,
},
// Light Industrial: Fläche, Andienung (accessibility), Infrastruktur
LIGHT_INDUSTRIAL: {
area: 0.20, location: 0.12, budget: 0.18, timing: 0.10,
prestige: 0.01, accessibility: 0.14, expansionPotential: 0.07,
flexibility: 0.05, visibility: 0.01, footfall: 0.00,
talentAccess: 0.04, esg: 0.04, taxEnvironment: 0.04,
},
// Logistik: Autobahnanbindung (accessibility), Andienung, Fläche, Verfügbarkeit
LOGISTICS: {
area: 0.18, location: 0.18, budget: 0.14, timing: 0.13,
prestige: 0.01, accessibility: 0.18, expansionPotential: 0.06,
flexibility: 0.04, visibility: 0.01, footfall: 0.00,
talentAccess: 0.02, esg: 0.02, taxEnvironment: 0.03,
},
PRODUCTION: {
area: 0.22, location: 0.13, budget: 0.18, timing: 0.10,
prestige: 0.01, accessibility: 0.13, expansionPotential: 0.08,
flexibility: 0.04, visibility: 0.01, footfall: 0.00,
talentAccess: 0.04, esg: 0.03, taxEnvironment: 0.03,
},
DEFAULT: {
area: 0.20, location: 0.18, budget: 0.18, timing: 0.10,
prestige: 0.05, accessibility: 0.09, expansionPotential: 0.05,
flexibility: 0.05, visibility: 0.02, footfall: 0.02,
talentAccess: 0.03, esg: 0.02, taxEnvironment: 0.01,
},
}
// ── Engine IO Types ───────────────────────────────────────────────────────────
export interface HardFilterResult {
excluded: boolean
reason?: string
severePenalty: number // extra points deducted on top of criterion score (030)
}
export interface MatchEngineOutput {
propertyId: string
needId: string
excluded: boolean
excludedReason?: string
finalScore: number // 0100 clamped
hardMatchScore: number // 0100 normalized
softFactorScore: number // 0100 normalized
dataQualityModifier: number
confidenceModifier: number
positiveFactors: ScoreFactor[]
negativeFactors: ScoreFactor[]
allHardFactors: ScoreFactor[]
allSoftFactors: ScoreFactor[]
tradeOffs: TradeOff[]
risks: Risk[]
missingData: MissingDataItem[]
nextBestActions: NextBestAction[]
}
@@ -0,0 +1,47 @@
import type { ShortlistStatus } from './enums'
export interface ShortlistItem {
resultId: string
resultType: string
title: string
matchScore: number
confidenceScore?: number
dataQualityScore?: number
sourceLabel?: string
addedAt: string
addedBy: string
note?: string
propertyId?: string
}
export interface ShortlistItemInput {
resultId: string
resultType: string
title: string
matchScore: number
confidenceScore?: number
dataQualityScore?: number
sourceLabel?: string
addedBy: string
note?: string
propertyId?: string
}
export interface Shortlist {
id: string
title: string
description?: string
needId?: string
ownerUserId?: string
decisionBriefId?: string
items: ShortlistItem[]
status: ShortlistStatus
createdBy: string
organizationId?: string
sharedWith?: string[]
createdAt: string
updatedAt: string
}
export type CreateShortlistInput = Omit<Shortlist, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateShortlistInput = Partial<CreateShortlistInput>
@@ -0,0 +1,125 @@
// ── Pipeline Stages ───────────────────────────────────────────────────────────
export const PipelineStage = {
STAGE_1_RAW_EVIDENCE: 'STAGE_1_RAW_EVIDENCE',
STAGE_2_NORMALIZED: 'STAGE_2_NORMALIZED',
STAGE_3_ENRICHED: 'STAGE_3_ENRICHED',
STAGE_4_REVIEW_CANDIDATE: 'STAGE_4_REVIEW_CANDIDATE',
STAGE_5_APPROVED_FUTURE: 'STAGE_5_APPROVED_FUTURE',
STAGE_6_MATCHABLE_RESULT: 'STAGE_6_MATCHABLE_RESULT',
STAGE_7_STRATEGIC_INPUT: 'STAGE_7_STRATEGIC_INPUT',
} as const
export type PipelineStage = typeof PipelineStage[keyof typeof PipelineStage]
export const PIPELINE_STAGE_LABELS: Record<PipelineStage, string> = {
STAGE_1_RAW_EVIDENCE: 'Rohe Markt-Evidenz',
STAGE_2_NORMALIZED: 'Normalisiertes Signal',
STAGE_3_ENRICHED: 'Angereichertes Signal',
STAGE_4_REVIEW_CANDIDATE: 'Review-Kandidat',
STAGE_5_APPROVED_FUTURE: 'Genehmigtes Future Signal',
STAGE_6_MATCHABLE_RESULT: 'Matchbares Ergebnis',
STAGE_7_STRATEGIC_INPUT: 'Strategischer Entscheidungs-Input',
}
export const PIPELINE_STAGE_DESCRIPTIONS: Record<PipelineStage, string> = {
STAGE_1_RAW_EVIDENCE: 'Rohe Evidenz aus Quellen gesammelt noch unverarbeitet',
STAGE_2_NORMALIZED: 'Daten normalisiert, Felder validiert und vereinheitlicht',
STAGE_3_ENRICHED: 'Entitäten extrahiert, Kontext angereichert und bewertet',
STAGE_4_REVIEW_CANDIDATE: 'Signal bereit für manuellen Analyst-Review',
STAGE_5_APPROVED_FUTURE: 'Genehmigt als Future Availability Signal intern sichtbar',
STAGE_6_MATCHABLE_RESULT: 'Im Unified Result Feed Match-Engine nutzbar',
STAGE_7_STRATEGIC_INPUT: 'Eingang in Decision Briefs und strategische Analyse',
}
export const PIPELINE_STAGE_ORDER: PipelineStage[] = [
'STAGE_1_RAW_EVIDENCE',
'STAGE_2_NORMALIZED',
'STAGE_3_ENRICHED',
'STAGE_4_REVIEW_CANDIDATE',
'STAGE_5_APPROVED_FUTURE',
'STAGE_6_MATCHABLE_RESULT',
'STAGE_7_STRATEGIC_INPUT',
]
// ── Gate Types ────────────────────────────────────────────────────────────────
export const GateType = {
EVIDENCE_GATE: 'EVIDENCE_GATE',
CONFIDENCE_GATE: 'CONFIDENCE_GATE',
SENSITIVITY_GATE: 'SENSITIVITY_GATE',
REVIEW_GATE: 'REVIEW_GATE',
MATCHABILITY_GATE: 'MATCHABILITY_GATE',
FEED_ELIGIBILITY_GATE: 'FEED_ELIGIBILITY_GATE',
} as const
export type GateType = typeof GateType[keyof typeof GateType]
export const GATE_LABELS: Record<GateType, string> = {
EVIDENCE_GATE: 'Evidenz-Gate',
CONFIDENCE_GATE: 'Konfidenz-Gate',
SENSITIVITY_GATE: 'Sensitivitäts-Gate',
REVIEW_GATE: 'Review-Gate',
MATCHABILITY_GATE: 'Matchbarkeits-Gate',
FEED_ELIGIBILITY_GATE: 'Feed-Eignung',
}
// ── Gate Status ───────────────────────────────────────────────────────────────
export const GateStatus = {
PASSED: 'PASSED',
FAILED: 'FAILED',
PENDING: 'PENDING',
BLOCKED: 'BLOCKED',
SKIPPED: 'SKIPPED',
} as const
export type GateStatus = typeof GateStatus[keyof typeof GateStatus]
export const GATE_STATUS_LABELS: Record<GateStatus, string> = {
PASSED: 'Bestanden',
FAILED: 'Fehlgeschlagen',
PENDING: 'Ausstehend',
BLOCKED: 'Blockiert',
SKIPPED: 'Übersprungen',
}
export const GATE_STATUS_COLORS: Record<GateStatus, { bg: string; fg: string }> = {
PASSED: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' },
FAILED: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' },
PENDING: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' },
BLOCKED: { bg: 'rgba(239,68,68,0.08)', fg: '#dc2626' },
SKIPPED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' },
}
// ── Interfaces ────────────────────────────────────────────────────────────────
export interface GateCheck {
label: string
passed: boolean
value?: string
note?: string
}
export interface GateEvaluation {
gateType: GateType
status: GateStatus
reason: string
nextAction?: string
evaluatedAt: string
checks: GateCheck[]
}
export interface PipelineState {
signalId: string
currentStage: PipelineStage
gates: Record<GateType, GateEvaluation>
overallEligible: boolean
publishedToFutureAvailability: boolean
publishedAt?: string
feedDisclaimer?: string
}
export interface AuditTrailEntry {
id: string
signalId: string
timestamp: string
stage: PipelineStage
action: string
performedBy: string
details: string
gateType?: GateType
}
@@ -0,0 +1,38 @@
import type { ResultType } from './enums'
import type { Property } from './property'
import type { FutureSignal } from './futureSignal'
import type { Match } from './match'
// ── Unified Match Result (discriminated union) ────────────────────────────────
// Wraps the three result types so UI components can handle all three paths
// without type confusion. Discriminate on `resultType`.
interface UnifiedResultBase {
matchId: string
needId: string
matchScore: number
resultType: ResultType
}
export interface VerifiedPortfolioResult extends UnifiedResultBase {
resultType: 'VERIFIED_PORTFOLIO'
property: Property
match: Match
}
export interface ExternalMarketResult extends UnifiedResultBase {
resultType: 'EXTERNAL_MARKET'
property: Property
match: Match
}
export interface FutureAvailabilityResult extends UnifiedResultBase {
resultType: 'FUTURE_AVAILABILITY'
signal: FutureSignal
match: Match
}
export type UnifiedMatchResult =
| VerifiedPortfolioResult
| ExternalMarketResult
| FutureAvailabilityResult