import { MockupPropertyProvider } from '../provider/MockupPropertyProvider' import { FreshnessStatus } from '../domain/enums' import type { DataQualitySummary } from '../domain/dashboard' import type { DataQuality, Property } from '../domain/property' export type RecommendedAction = { id: string label: string detail: string priority: 'HIGH' | 'MEDIUM' | 'LOW' field?: string } // ── Field → Action map ──────────────────────────────────────────────────────── const FIELD_ACTION_MAP: Record> = { 'Mietpreis/m²': { label: 'Mietpreis ergänzen', detail: 'Fehlender Mietpreis schließt Objekt aus Budget-Matches aus', priority: 'HIGH', field: 'Mietpreis/m²' }, 'Fläche m²': { label: 'Fläche bestätigen', detail: 'Fläche ist Hard-Kriterium für alle Matchings', priority: 'HIGH', field: 'Fläche m²' }, 'Verfügbarkeit': { label: 'Verfügbarkeit bestätigen', detail: 'Timing ist entscheidend für Nachfrager mit Deadlines', priority: 'HIGH', field: 'Verfügbarkeit' }, 'Adresse': { label: 'Adresse vervollständigen', detail: 'Für Standortbewertung und Kartenansicht notwendig', priority: 'HIGH', field: 'Adresse' }, 'Beschreibung': { label: 'Beschreibung hinzufügen', detail: 'Verbesserter Kontext erhöht Nachfrager-Vertrauen', priority: 'MEDIUM', field: 'Beschreibung' }, 'Soft Factors': { label: 'Passantenfrequenz & ESG', detail: 'Soft Factors verbessern Match-Scoring erheblich', priority: 'MEDIUM', field: 'Soft Factors' }, 'Ausbaustandard': { label: 'Ausbaustandard angeben', detail: 'SHELL/BASIC/FULL/PREMIUM beeinflusst Eignung stark', priority: 'MEDIUM', field: 'Ausbaustandard' }, 'Bilder': { label: 'Bilder hochladen', detail: 'Objektfotos steigern Anfragerate deutlich', priority: 'MEDIUM', field: 'Bilder' }, 'Jahresmiete (CHF)': { label: 'Jahresmiete angeben', detail: 'Ergänzt Mietpreis/m² für Budgetvergleiche', priority: 'LOW', field: 'Jahresmiete (CHF)' }, 'Expansionspotenzial': { label: 'Erweiterungsfläche angeben', detail: 'Wichtig für wachsende Unternehmen', priority: 'LOW', field: 'Expansionspotenzial' }, } const FRESHNESS_ACTIONS: Record = { [FreshnessStatus.OUTDATED]: { id: 'review_source', label: 'Quelle überprüfen', detail: 'Daten sind älter als 14 Tage — Verfügbarkeit könnte sich geändert haben', priority: 'HIGH', }, [FreshnessStatus.STALE]: { id: 'update_data', label: 'Daten aktualisieren', detail: 'Daten sind 2–14 Tage alt — Aktualitätsscore reduziert', priority: 'MEDIUM', }, } // ── Core Checks ─────────────────────────────────────────────────────────────── const CRITICAL_CHECKS: Array<{ field: string; present: (p: Property) => boolean }> = [ { field: 'Mietpreis/m²', present: p => p.rentPricePerSqm > 0 }, { field: 'Fläche m²', present: p => p.areaSqm > 0 }, { field: 'Verfügbarkeit', present: p => !!p.availabilityDate }, { field: 'Adresse', present: p => !!p.address?.street && !!p.address?.city }, ] const OPTIONAL_CHECKS: Array<{ field: string; present: (p: Property) => boolean }> = [ { field: 'Beschreibung', present: p => !!p.description && p.description.length > 20 }, { field: 'Soft Factors', present: p => !!(p.softFactors?.prestigeScore || p.softFactors?.footfallScore || p.softFactors?.commuterAccessScore) }, { field: 'Ausbaustandard', present: p => !!p.hardFacts?.fitOut }, { field: 'Bilder', present: p => (p.images?.length ?? 0) > 0 }, { field: 'Jahresmiete (CHF)', present: p => !!p.rentChfSqmYear }, { field: 'Expansionspotenzial', present: p => !!(p.expansionPotentialSqm || p.hardFacts) }, ] // ── Public API ──────────────────────────────────────────────────────────────── export function getMissingCriticalFields(property: Property): string[] { const fromData = property.dataQuality?.missingCriticalFields ?? [] if (fromData.length > 0) return fromData return CRITICAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) } export function getQualityWarnings(property: Property): string[] { return property.dataQuality?.warnings ?? [] } export function getRecommendedActions(quality: DataQuality, freshness?: string): RecommendedAction[] { const actions: RecommendedAction[] = [] for (const field of quality.missingCriticalFields) { const def = FIELD_ACTION_MAP[field] if (def) actions.push({ id: `fill_${field}`, ...def }) } const fn = freshness ?? quality.freshness if (fn && fn !== FreshnessStatus.FRESH) { const freshnessAction = FRESHNESS_ACTIONS[fn] if (freshnessAction) actions.push(freshnessAction) } for (const field of quality.missingOptionalFields) { const def = FIELD_ACTION_MAP[field] if (def) actions.push({ id: `fill_opt_${field}`, ...def }) } return actions } export function calculatePropertyQuality(property: Property): DataQuality { if (property.dataQuality?.qualityLevel) return property.dataQuality const missingCritical = CRITICAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) const missingOptional = OPTIONAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) const completeness = 1 - (missingCritical.length * 0.15 + missingOptional.length * 0.05) const confidence = property.confidenceScore ?? 0.5 const freshnessVal = property.dataQuality?.freshness ?? FreshnessStatus.OUTDATED const freshnessFactor = freshnessVal === FreshnessStatus.FRESH ? 1 : freshnessVal === FreshnessStatus.STALE ? 0.7 : 0.4 const score = Math.min(1, Math.max(0, completeness * 0.5 + confidence * 0.3 + freshnessFactor * 0.2)) const warnings: string[] = [] if (confidence < 0.5) warnings.push('Niedrige Daten-Vertrauensscore') if (freshnessVal === FreshnessStatus.OUTDATED) warnings.push('Daten sind veraltet (>14 Tage)') if (missingCritical.length > 0) warnings.push(`${missingCritical.length} Pflichtfeld(er) fehlen`) const qualityLevel = missingCritical.length > 0 ? 'INCOMPLETE' : score >= 0.8 ? 'HIGH' : score >= 0.6 ? 'MEDIUM' : 'LOW' return { score, qualityLevel, missingCriticalFields: missingCritical, missingOptionalFields: missingOptional, lastVerifiedAt: property.dataQuality?.lastVerifiedAt, freshness: freshnessVal, warnings, } } // ── Portfolio summary (existing) ────────────────────────────────────────────── export const dataQualityService = { async getPortfolioQualitySummary(): Promise { const properties = await MockupPropertyProvider.getAll() const avgScoreRaw = properties.length > 0 ? properties.reduce((sum, p) => sum + (p.dataQuality?.score ?? 0), 0) / properties.length : 0 const fieldCounts = properties .flatMap(p => p.dataQuality?.missingCriticalFields ?? []) .reduce>((acc, f) => { acc[f] = (acc[f] ?? 0) + 1 return acc }, {}) const topMissingFields = Object.entries(fieldCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([f]) => f) return { avgScore: Math.round(avgScoreRaw * 100), critical: properties.filter(p => (p.dataQuality?.score ?? 0) < 0.5).length, propertiesWithMissingCritical: properties.filter( p => (p.dataQuality?.missingCriticalFields?.length ?? 0) > 0, ).length, topMissingFields, } }, getMissingCriticalFields, getQualityWarnings, getRecommendedActions, calculatePropertyQuality, }