import { memo, useMemo, useState } from 'react' import { Alert, Box, Card, Chip, Drawer, IconButton, LinearProgress, Tooltip, Typography, } from '@mui/material' import { AlertTriangle, CheckCircle2, Clock, Edit2, RefreshCw } from 'lucide-react' import { LoadingPage, ErrorState } from '../../components/ui' import { DataQualityBadge, FreshnessIndicator } from '../../components/data-quality' import { PropertyDetailView } from '../../components/supply' import { useProperties } from '../../hooks/useProperties' import { getRecommendedActions } from '../../services/dataQualityService' import { DataFreshness } from '../../domain/enums' import type { Property } from '../../domain/property' // ── Types ──────────────────────────────────────────────────────────────────── type QualityFilter = 'ALL' | 'INCOMPLETE' | 'STALE' | 'LOW' | 'MEDIUM' | 'HIGH' // ── Helpers ────────────────────────────────────────────────────────────────── const FIELD_LABEL: Record = { 'Mietpreis/m²': 'Mietpreis', 'Fläche m²': 'Fläche', 'Verfügbarkeit': 'Verfügbarkeit', 'Adresse': 'Adresse', 'Beschreibung': 'Beschreibung', 'Bilder': 'Bilder', 'Ausbaustandard': 'Ausbaustandard', 'Soft Factors': 'Soft Factors', 'Jahresmiete (CHF)': 'Jahresmiete', 'Expansionspotenzial': 'Erweiterung', } function priorityOf(p: Property): number { const hasCritical = p.dataQuality.missingCriticalFields.length > 0 const isStale = p.dataQuality.freshness === DataFreshness.STALE || p.dataQuality.freshness === DataFreshness.OUTDATED if (hasCritical && isStale) return 0 if (hasCritical) return 1 if (isStale) return 2 if (p.dataQuality.score < 0.6) return 3 if (p.dataQuality.score < 0.8) return 4 return 5 } // ── Row ────────────────────────────────────────────────────────────────────── const PropertyRow = memo(function PropertyRow({ property, onEdit, }: { property: Property onEdit: (id: string) => void }) { const q = property.dataQuality const hasCritical = q.missingCriticalFields.length > 0 const isStale = q.freshness === DataFreshness.STALE || q.freshness === DataFreshness.OUTDATED const topAction = getRecommendedActions(q, q.freshness)[0] ?? null const visibleFields = q.missingCriticalFields.slice(0, 3) const overflow = q.missingCriticalFields.length - visibleFields.length return ( onEdit(property.id)} sx={{ display: 'grid', gridTemplateColumns: '2fr 110px 1fr 110px 110px 44px', alignItems: 'center', px: 2, py: 1.25, borderBottom: '1px solid #f1f5f9', cursor: 'pointer', bgcolor: hasCritical ? '#fff8f8' : 'white', '&:hover': { bgcolor: hasCritical ? '#fff0f0' : '#f8fafc' }, transition: 'background 0.1s', gap: 1, }} > {/* Objekt */} {property.title} {property.location.city} {/* Score */} {/* Fehlende Pflichtfelder */} {hasCritical ? ( <> {visibleFields.map(f => ( ))} {overflow > 0 && ( )} ) : ( } label="Vollständig" size="small" sx={{ height: 18, fontSize: '0.6rem', bgcolor: '#dcfce7', color: '#16a34a' }} /> )} {/* Aktualität */} {/* Empfehlung */} {topAction ? ( {topAction.priority === 'HIGH' ? : isStale ? : } {topAction.label} ) : ( )} {/* Edit button */} e.stopPropagation()}> onEdit(property.id)} sx={{ color: '#94a3b8', '&:hover': { color: '#152642' } }} > ) }) // ── KPI Card ───────────────────────────────────────────────────────────────── function KpiCard({ label, value, sub, color, active, onClick, }: { label: string value: string | number sub?: string color: string active: boolean onClick: () => void }) { return ( {label} {value} {sub && {sub}} ) } // ── Page ───────────────────────────────────────────────────────────────────── export default function DataQuality() { const [filter, setFilter] = useState('ALL') const [detailId, setDetailId] = useState(null) const { data: properties = [], isLoading, error } = useProperties() if (isLoading) return if (error) return const total = properties.length const avgScore = total ? properties.reduce((s, p) => s + p.dataQuality.score, 0) / total : 0 const incomplete = properties.filter(p => p.dataQuality.missingCriticalFields.length > 0) const stale = properties.filter( p => p.dataQuality.freshness === DataFreshness.STALE || p.dataQuality.freshness === DataFreshness.OUTDATED, ) const high = properties.filter(p => p.dataQuality.score >= 0.8) const medium = properties.filter(p => p.dataQuality.score >= 0.6 && p.dataQuality.score < 0.8) const low = properties.filter(p => p.dataQuality.score < 0.6) const filtered = useMemo(() => { const base = [...properties] const result = base.filter(p => { if (filter === 'ALL') return true if (filter === 'INCOMPLETE') return p.dataQuality.missingCriticalFields.length > 0 if (filter === 'STALE') return p.dataQuality.freshness === DataFreshness.STALE || p.dataQuality.freshness === DataFreshness.OUTDATED if (filter === 'HIGH') return p.dataQuality.score >= 0.8 if (filter === 'MEDIUM') return p.dataQuality.score >= 0.6 && p.dataQuality.score < 0.8 if (filter === 'LOW') return p.dataQuality.score < 0.6 return true }) return result.sort((a, b) => priorityOf(a) - priorityOf(b)) }, [properties, filter]) const scoreColor = avgScore >= 0.8 ? '#1a7a4a' : avgScore >= 0.6 ? '#d97706' : '#dc2626' function toggleFilter(f: QualityFilter) { setFilter(prev => prev === f ? 'ALL' : f) } return ( {/* Header */} Datenpflege Vollständigkeit, Aktualität und Vertrauen der Objektdaten · Klick auf KPI-Karte filtert die Liste {/* KPI cards */} setFilter('ALL')} sub={`${total} Objekte insgesamt`} /> toggleFilter('INCOMPLETE')} /> toggleFilter('STALE')} /> {/* Quality distribution */} Qualitätsverteilung {[ { label: 'Hoch (≥80%)', count: high.length, color: '#16a34a', f: 'HIGH' as QualityFilter }, { label: 'Mittel (60–79%)', count: medium.length, color: '#d97706', f: 'MEDIUM' as QualityFilter }, { label: 'Niedrig (<60%)', count: low.length, color: '#dc2626', f: 'LOW' as QualityFilter }, ].map(row => ( toggleFilter(row.f)} sx={{ display: 'flex', alignItems: 'center', gap: 2, cursor: 'pointer', p: 0.75, borderRadius: 1, bgcolor: filter === row.f ? `${row.color}10` : 'transparent', '&:hover': { bgcolor: `${row.color}08` }, transition: 'background 0.1s', }} > {row.label} 0 ? (row.count / total) * 100 : 0} sx={{ height: 10, borderRadius: 5, bgcolor: `${row.color}18`, '& .MuiLinearProgress-bar': { bgcolor: row.color, borderRadius: 5 }, }} /> {total > 0 ? Math.round((row.count / total) * 100) : 0}% ))} {/* Object list */} Objektübersicht {filtered.length} von {total} Objekten · sortiert nach Handlungsbedarf {filter !== 'ALL' && ( setFilter('ALL')} sx={{ height: 22, fontSize: '0.68rem' }} /> )} {filtered.length > 0 ? ( {/* Table header */} {['Objekt', 'Qualität', 'Fehlende Pflichtfelder', 'Aktualität', 'Empfehlung', ''].map(h => ( {h} ))} {filtered.map(p => ( ))} ) : ( Keine Objekte in dieser Kategorie — alles in Ordnung! )} {/* Edit drawer */} setDetailId(null)} slotProps={{ paper: { sx: { width: { xs: '100%', sm: 560 } } } }} > {detailId && ( setDetailId(null)} /> )} ) }