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,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
}