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,232 @@
|
||||
import type { Need } from '../../domain/need'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { ScoreFactor, TradeOff, Risk, MissingDataItem } from '../../domain/match'
|
||||
import { ResultType, AvailabilityStatus } from '../../domain/enums'
|
||||
import { RiskLevel } from '../../domain/enums'
|
||||
|
||||
// ── Trade-Off Detection ───────────────────────────────────────────────────────
|
||||
|
||||
export function analyzeTradeOffs(
|
||||
hardFactors: ScoreFactor[],
|
||||
softFactors: ScoreFactor[],
|
||||
need: Need,
|
||||
property: Property,
|
||||
): TradeOff[] {
|
||||
const tradeOffs: TradeOff[] = []
|
||||
const byKey = (factors: ScoreFactor[], key: string) => factors.find(f => f.criterion === key)
|
||||
|
||||
const location = byKey(hardFactors, 'location')
|
||||
const budget = byKey(hardFactors, 'budget')
|
||||
const area = byKey(hardFactors, 'area')
|
||||
const timing = byKey(hardFactors, 'timing')
|
||||
const prestige = byKey(softFactors, 'prestige')
|
||||
const flex = byKey(softFactors, 'flexibility')
|
||||
const access = byKey(softFactors, 'accessibility')
|
||||
|
||||
// Prime location at budget premium
|
||||
if (location && budget && location.score >= 85 && budget.score < 60) {
|
||||
tradeOffs.push({
|
||||
criterion: 'location-vs-budget',
|
||||
concern: `Erstklassiger Standort (${property.location.city}) zu erhöhten Mietkosten`,
|
||||
severity: budget.score < 40 ? 'HIGH' : 'MEDIUM',
|
||||
mitigation: 'Nebenkosten analysieren; längere Laufzeit für Konditionenverhandlung nutzen',
|
||||
impactOnScore: -Math.round((100 - budget.score) * budget.weight * 10),
|
||||
})
|
||||
}
|
||||
|
||||
// Large space but poor budget fit
|
||||
if (area && budget && area.score >= 80 && budget.score < 55) {
|
||||
tradeOffs.push({
|
||||
criterion: 'area-vs-budget',
|
||||
concern: 'Grosszügige Fläche übersteigt Budget — Teiluntermiete denkbar',
|
||||
severity: 'MEDIUM',
|
||||
mitigation: 'Möglichkeit für Untermiete oder Co-Working prüfen',
|
||||
impactOnScore: -5,
|
||||
})
|
||||
}
|
||||
|
||||
// Good timing but low data quality
|
||||
if (timing && timing.score >= 80 && (property.dataQuality?.score ?? 1) < 0.55) {
|
||||
tradeOffs.push({
|
||||
criterion: 'timing-vs-dataQuality',
|
||||
concern: 'Verfügbarkeit stimmt, Datenbasis ist aber noch unvollständig',
|
||||
severity: 'MEDIUM',
|
||||
mitigation: 'Objektdaten vor Zusage direkt beim Vermieter verifizieren',
|
||||
impactOnScore: -8,
|
||||
})
|
||||
}
|
||||
|
||||
// High prestige but low flexibility
|
||||
if (prestige && flex && prestige.score >= 75 && flex.score < 40) {
|
||||
tradeOffs.push({
|
||||
criterion: 'prestige-vs-flexibility',
|
||||
concern: 'Repräsentative Lage mit eingeschränkter Vertragsflexibilität',
|
||||
severity: 'LOW',
|
||||
mitigation: 'Breakclause-Option in Verhandlung einfordern',
|
||||
impactOnScore: -4,
|
||||
})
|
||||
}
|
||||
|
||||
// Future signal with good location
|
||||
if (property.resultType === ResultType.FUTURE_AVAILABILITY && location && location.score >= 85) {
|
||||
tradeOffs.push({
|
||||
criterion: 'futureSignal-vs-location',
|
||||
concern: 'Sehr guter Standort, aber Verfügbarkeit noch unbestätigt',
|
||||
severity: 'HIGH',
|
||||
mitigation: 'Frühzeitig Kontakt mit Eigentümer aufnehmen; Letter of Intent erwägen',
|
||||
impactOnScore: -12,
|
||||
})
|
||||
}
|
||||
|
||||
// Good accessibility but poor public transport
|
||||
if (access && access.score < 40 && need.softFactors?.maxPublicTransportMinutes !== undefined) {
|
||||
tradeOffs.push({
|
||||
criterion: 'accessibility-vs-commute',
|
||||
concern: 'Erreichbarkeit unter Ihren Anforderungen — Pendlererfahrung beeinträchtigt',
|
||||
severity: 'MEDIUM',
|
||||
mitigation: 'Shuttle-Service oder Mobility-Angebot als Kompensation anfragen',
|
||||
impactOnScore: -6,
|
||||
})
|
||||
}
|
||||
|
||||
return tradeOffs
|
||||
}
|
||||
|
||||
// ── Risk Analysis ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function analyzeRisks(property: Property, hardFactors: ScoreFactor[]): Risk[] {
|
||||
const risks: Risk[] = []
|
||||
const byKey = (key: string) => hardFactors.find(f => f.criterion === key)
|
||||
|
||||
// Future availability risk — always flag
|
||||
if (property.resultType === ResultType.FUTURE_AVAILABILITY) {
|
||||
risks.push({
|
||||
category: 'Verfügbarkeit',
|
||||
description: 'Zukünftiges Signal — Verfügbarkeit ist nicht bestätigt und kann sich verschieben oder entfallen',
|
||||
level: RiskLevel.HIGH,
|
||||
mitigation: 'Absichtserklärung einholen; alternative Objekte parallel prüfen',
|
||||
})
|
||||
}
|
||||
|
||||
// Data quality risk
|
||||
const dq = property.dataQuality?.score ?? 0.5
|
||||
if (dq < 0.55) {
|
||||
risks.push({
|
||||
category: 'Datenqualität',
|
||||
description: `Datenqualität ${Math.round(dq * 100)}% — Angaben unvollständig oder nicht verifiziert`,
|
||||
level: dq < 0.40 ? RiskLevel.HIGH : RiskLevel.MEDIUM,
|
||||
mitigation: 'Objektdaten direkt beim Anbieter anfordern und validieren',
|
||||
})
|
||||
}
|
||||
|
||||
// Budget risk
|
||||
const budgetFactor = byKey('budget')
|
||||
if (budgetFactor && budgetFactor.score < 50) {
|
||||
risks.push({
|
||||
category: 'Budget',
|
||||
description: 'Mietpreis liegt über dem gesetzten Budget — finanzielle Belastung prüfen',
|
||||
level: budgetFactor.score < 30 ? RiskLevel.HIGH : RiskLevel.MEDIUM,
|
||||
mitigation: 'Vollkostenrechnung inkl. Nebenkosten erstellen; Verhandlungsspielraum ausloten',
|
||||
})
|
||||
}
|
||||
|
||||
// Occupied / delayed availability
|
||||
if (property.availabilityStatus === AvailabilityStatus.OCCUPIED) {
|
||||
risks.push({
|
||||
category: 'Verfügbarkeit',
|
||||
description: 'Objekt aktuell belegt — Übergabetermin unsicher',
|
||||
level: RiskLevel.MEDIUM,
|
||||
mitigation: 'Verbindlichen Übergabetermin schriftlich vereinbaren',
|
||||
})
|
||||
}
|
||||
|
||||
// Low confidence score
|
||||
if (property.confidenceScore < 0.50) {
|
||||
risks.push({
|
||||
category: 'Datenverlässlichkeit',
|
||||
description: `Konfidenz ${Math.round(property.confidenceScore * 100)}% — Quelldaten unsicher`,
|
||||
level: RiskLevel.MEDIUM,
|
||||
mitigation: 'Unabhängige Verifikation der Objektangaben empfohlen',
|
||||
})
|
||||
}
|
||||
|
||||
// Missing critical property data
|
||||
const criticalMissing = property.dataQuality?.missingCriticalFields ?? []
|
||||
if (criticalMissing.length > 0) {
|
||||
risks.push({
|
||||
category: 'Fehlende Kerndaten',
|
||||
description: `Fehlende Pflichtfelder: ${criticalMissing.slice(0, 3).join(', ')}${criticalMissing.length > 3 ? ` +${criticalMissing.length - 3}` : ''}`,
|
||||
level: RiskLevel.MEDIUM,
|
||||
mitigation: 'Objektdaten vor Verhandlung vervollständigen lassen',
|
||||
})
|
||||
}
|
||||
|
||||
return risks
|
||||
}
|
||||
|
||||
// ── Missing Data Detection ────────────────────────────────────────────────────
|
||||
|
||||
export function identifyMissingData(property: Property, need: Need): MissingDataItem[] {
|
||||
const missing: MissingDataItem[] = []
|
||||
|
||||
if (!property.rentPricePerSqm || property.rentPricePerSqm <= 0) {
|
||||
missing.push({
|
||||
field: 'rentPricePerSqm',
|
||||
importance: 'CRITICAL',
|
||||
description: 'Mietpreis fehlt — Budget-Scoring nicht möglich',
|
||||
impact: 'Budget-Score wird neutral (50) gesetzt — Gesamtscore unzuverlässig',
|
||||
})
|
||||
}
|
||||
|
||||
if (!property.availabilityDate || property.availabilityDate === '') {
|
||||
missing.push({
|
||||
field: 'availabilityDate',
|
||||
importance: 'HIGH',
|
||||
description: 'Kein Verfügbarkeitsdatum angegeben',
|
||||
impact: 'Timing-Score reduziert auf 38/100 — Einzugsfenster nicht prüfbar',
|
||||
})
|
||||
}
|
||||
|
||||
if (!property.softFactors) {
|
||||
missing.push({
|
||||
field: 'softFactors',
|
||||
importance: 'HIGH',
|
||||
description: 'Soft Factors vollständig fehlend (Prestige, Erreichbarkeit, etc.)',
|
||||
impact: 'Alle Soft-Factor-Scores auf neutral (50) gesetzt — Matching-Qualität eingeschränkt',
|
||||
})
|
||||
} else {
|
||||
const sf = property.softFactors
|
||||
const missingFields: Array<[string, string]> = []
|
||||
if (sf.commuterAccessScore === undefined && sf.accessibility === undefined) missingFields.push(['accessibility', 'Erreichbarkeit'])
|
||||
if (sf.prestigeScore === undefined && sf.prestige === undefined) missingFields.push(['prestige', 'Prestige-Score'])
|
||||
if (sf.esgScore === undefined) missingFields.push(['esgScore', 'ESG-Bewertung'])
|
||||
if (missingFields.length > 0) {
|
||||
missing.push({
|
||||
field: missingFields.map(([k]) => k).join(', '),
|
||||
importance: 'MEDIUM',
|
||||
description: `Fehlende Soft Factors: ${missingFields.map(([, l]) => l).join(', ')}`,
|
||||
impact: 'Betroffene Scores neutral — Matching-Präzision verringert',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!property.hardFacts) {
|
||||
missing.push({
|
||||
field: 'hardFacts',
|
||||
importance: 'MEDIUM',
|
||||
description: 'Technische Objektdaten fehlen (Parkierung, ÖV-Score, etc.)',
|
||||
impact: 'Infrastruktureignung nicht prüfbar',
|
||||
})
|
||||
}
|
||||
|
||||
if (need.budgetRange?.maxPerSqm === undefined || need.budgetRange.maxPerSqm <= 0) {
|
||||
missing.push({
|
||||
field: 'need.budgetRange',
|
||||
importance: 'HIGH',
|
||||
description: 'Kein Budget im Bedarf angegeben',
|
||||
impact: 'Budget-Scoring neutralisiert — Filter unwirksam',
|
||||
})
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
Reference in New Issue
Block a user