refactor: GenericBadge + Provider-Isolation

Teil A — GenericBadge:
- Neue src/components/shared/GenericBadge.tsx mit zwei Varianten:
  transparent (${color}18 Hintergrund, farbiger Text, opt. Border)
  solid (gefüllte Farbe, weisser Text)
- Props: label, color, variant, showBorder, bold, icon, size, tooltip, ariaLabel
- 7 Badges auf GenericBadge refactored (Config-Objekt + 1-Zeiler):
  ReviewStatusBadge, ReviewPriorityBadge, ReviewEntityTypeBadge,
  AIOutputStatusBadge, AIErrorBadge, MatchStatusBadge, ShortlistStatusBadge
- Barrel-Export in src/components/shared/index.ts

Teil B — Provider-Isolation:
- src/mock-data/propertyStore.ts als neutrales Daten-Modul erstellt
- MockupPropertyProvider: importiert aus mock-data statt selbst zu definieren
- MockupUnitProvider: importiert aus mock-data statt aus MockupPropertyProvider
- matchSyncService: importiert aus mock-data statt aus MockupPropertyProvider
- Kein Provider importiert mehr einen anderen Provider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 13:13:34 +02:00
parent 7f090a48ff
commit 8f1db31683
13 changed files with 111 additions and 107 deletions
+2 -10
View File
@@ -1,4 +1,4 @@
import { Chip, Tooltip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { AIOutputError } from '../../domain/aiOutput' import type { AIOutputError } from '../../domain/aiOutput'
const ERROR_CONFIG: Record<string, { label: string; color: string }> = { const ERROR_CONFIG: Record<string, { label: string; color: string }> = {
@@ -11,13 +11,5 @@ const ERROR_CONFIG: Record<string, { label: string; color: string }> = {
export function AIErrorBadge({ error }: { error: AIOutputError }) { export function AIErrorBadge({ error }: { error: AIOutputError }) {
const { label, color } = ERROR_CONFIG[error.type] ?? { label: error.type, color: '#c0392b' } const { label, color } = ERROR_CONFIG[error.type] ?? { label: error.type, color: '#c0392b' }
return ( return <GenericBadge label={label} color={color} bold tooltip={error.message} />
<Tooltip title={error.message} arrow>
<Chip
size="small"
label={label}
sx={{ bgcolor: `${color}18`, color, fontWeight: 700, fontSize: '0.65rem', cursor: 'default' }}
/>
</Tooltip>
)
} }
@@ -1,21 +1,15 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { ReviewStatus } from '../../domain/enums' import type { ReviewStatus } from '../../domain/enums'
const CONFIG: Record<ReviewStatus, { label: string; color: string }> = { const CONFIG: Record<ReviewStatus, { label: string; color: string }> = {
UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' }, UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' },
IN_REVIEW: { label: 'In Prüfung', color: '#d97706' }, IN_REVIEW: { label: 'In Prüfung', color: '#d97706' },
APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, APPROVED: { label: 'Genehmigt', color: '#1a7a4a' },
REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, REJECTED: { label: 'Abgelehnt', color: '#c0392b' },
FLAGGED: { label: 'Markiert', color: '#ea580c' }, FLAGGED: { label: 'Markiert', color: '#ea580c' },
} }
export function AIOutputStatusBadge({ status }: { status: ReviewStatus }) { export function AIOutputStatusBadge({ status }: { status: ReviewStatus }) {
const { label, color } = CONFIG[status] ?? { label: status, color: '#64748b' } const { label, color } = CONFIG[status] ?? { label: status, color: '#64748b' }
return ( return <GenericBadge label={label} color={color} />
<Chip
size="small"
label={label}
sx={{ bgcolor: `${color}18`, color, fontWeight: 600, fontSize: '0.7rem' }}
/>
)
} }
@@ -1,4 +1,4 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { MatchStatus } from '../../domain/enums' import type { MatchStatus } from '../../domain/enums'
const STATUS_META: Record<string, { label: string; color: string }> = { const STATUS_META: Record<string, { label: string; color: string }> = {
@@ -10,9 +10,6 @@ const STATUS_META: Record<string, { label: string; color: string }> = {
export function MatchStatusBadge({ status }: { status?: MatchStatus }) { export function MatchStatusBadge({ status }: { status?: MatchStatus }) {
if (!status) return null if (!status) return null
const meta = STATUS_META[status] ?? { label: status, color: '#64748b' } const { label, color } = STATUS_META[status] ?? { label: status, color: '#64748b' }
return ( return <GenericBadge label={label} color={color} variant="solid" />
<Chip label={meta.label} size="small"
sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 11 }} />
)
} }
@@ -1,11 +1,6 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { ReviewEntityType } from '../../domain/review' import type { ReviewEntityType } from '../../domain/review'
interface ReviewEntityTypeBadgeProps {
entityType: ReviewEntityType
size?: 'small' | 'medium'
}
const CONFIG: Record<ReviewEntityType, { label: string; color: string }> = { const CONFIG: Record<ReviewEntityType, { label: string; color: string }> = {
FUTURE_SIGNAL: { label: 'Zukunftssignal', color: '#7c3aed' }, FUTURE_SIGNAL: { label: 'Zukunftssignal', color: '#7c3aed' },
MATCH_EXPLANATION: { label: 'Match-Begründung', color: '#1e3a5f' }, MATCH_EXPLANATION: { label: 'Match-Begründung', color: '#1e3a5f' },
@@ -15,18 +10,12 @@ const CONFIG: Record<ReviewEntityType, { label: string; color: string }> = {
PROPERTY_DATA_ISSUE: { label: 'Datenfehler', color: '#c0392b' }, PROPERTY_DATA_ISSUE: { label: 'Datenfehler', color: '#c0392b' },
} }
export function ReviewEntityTypeBadge({ entityType, size = 'small' }: ReviewEntityTypeBadgeProps) { interface Props {
const { label, color } = CONFIG[entityType] ?? { label: entityType, color: '#64748b' } entityType: ReviewEntityType
return ( size?: 'small' | 'medium'
<Chip }
size={size}
label={label} export function ReviewEntityTypeBadge({ entityType, size = 'small' }: Props) {
sx={{ const { label, color } = CONFIG[entityType] ?? { label: entityType, color: '#64748b' }
bgcolor: `${color}18`, return <GenericBadge label={label} color={color} size={size} />
color,
fontWeight: 600,
fontSize: size === 'small' ? '0.7rem' : '0.8125rem',
}}
/>
)
} }
+9 -21
View File
@@ -1,11 +1,6 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { ReviewPriority } from '../../domain/review' import type { ReviewPriority } from '../../domain/review'
interface ReviewPriorityBadgeProps {
priority: ReviewPriority
size?: 'small' | 'medium'
}
const CONFIG: Record<ReviewPriority, { label: string; color: string }> = { const CONFIG: Record<ReviewPriority, { label: string; color: string }> = {
LOW: { label: 'Niedrig', color: '#64748b' }, LOW: { label: 'Niedrig', color: '#64748b' },
MEDIUM: { label: 'Mittel', color: '#d97706' }, MEDIUM: { label: 'Mittel', color: '#d97706' },
@@ -13,19 +8,12 @@ const CONFIG: Record<ReviewPriority, { label: string; color: string }> = {
CRITICAL: { label: 'Kritisch', color: '#c0392b' }, CRITICAL: { label: 'Kritisch', color: '#c0392b' },
} }
export function ReviewPriorityBadge({ priority, size = 'small' }: ReviewPriorityBadgeProps) { interface Props {
const { label, color } = CONFIG[priority] ?? CONFIG.MEDIUM priority: ReviewPriority
return ( size?: 'small' | 'medium'
<Chip }
size={size}
label={label} export function ReviewPriorityBadge({ priority, size = 'small' }: Props) {
sx={{ const { label, color } = CONFIG[priority] ?? CONFIG.MEDIUM
bgcolor: `${color}18`, return <GenericBadge label={label} color={color} showBorder bold size={size} />
color,
fontWeight: 700,
fontSize: size === 'small' ? '0.7rem' : '0.8125rem',
border: `1px solid ${color}40`,
}}
/>
)
} }
+13 -25
View File
@@ -1,33 +1,21 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { ReviewTaskStatus } from '../../domain/review' import type { ReviewTaskStatus } from '../../domain/review'
interface ReviewStatusBadgeProps { const CONFIG: Record<ReviewTaskStatus, { label: string; color: string }> = {
PENDING: { label: 'Ausstehend', color: '#d97706' },
IN_REVIEW: { label: 'In Prüfung', color: '#2563eb' },
APPROVED: { label: 'Genehmigt', color: '#1a7a4a' },
REJECTED: { label: 'Abgelehnt', color: '#c0392b' },
NEEDS_MORE_DATA: { label: 'Mehr Daten nötig', color: '#7c3aed' },
ESCALATED: { label: 'Eskaliert', color: '#ea580c' },
}
interface Props {
status: ReviewTaskStatus status: ReviewTaskStatus
size?: 'small' | 'medium' size?: 'small' | 'medium'
} }
const CONFIG: Record<ReviewTaskStatus, { label: string; color: string }> = { export function ReviewStatusBadge({ status, size = 'small' }: Props) {
PENDING: { label: 'Ausstehend', color: '#d97706' },
IN_REVIEW: { label: 'In Prüfung', color: '#2563eb' },
APPROVED: { label: 'Genehmigt', color: '#1a7a4a' },
REJECTED: { label: 'Abgelehnt', color: '#c0392b' },
NEEDS_MORE_DATA: { label: 'Mehr Daten nötig', color: '#7c3aed' },
ESCALATED: { label: 'Eskaliert', color: '#ea580c' },
}
export function ReviewStatusBadge({ status, size = 'small' }: ReviewStatusBadgeProps) {
const { label, color } = CONFIG[status] ?? CONFIG.PENDING const { label, color } = CONFIG[status] ?? CONFIG.PENDING
return ( return <GenericBadge label={label} color={color} showBorder size={size} />
<Chip
size={size}
label={label}
sx={{
bgcolor: `${color}18`,
color,
fontWeight: 600,
fontSize: size === 'small' ? '0.7rem' : '0.8125rem',
border: `1px solid ${color}40`,
}}
/>
)
} }
+50
View File
@@ -0,0 +1,50 @@
import { Chip, Tooltip } from '@mui/material'
import type { ReactElement } from 'react'
export interface GenericBadgeProps {
label: string
color: string
/** transparent: semi-opaque bg (color + 18% opacity) | solid: filled bg, white text */
variant?: 'transparent' | 'solid'
/** Adds a 1px border at color 25% opacity (transparent variant only) */
showBorder?: boolean
/** fontWeight 700 instead of 600 */
bold?: boolean
/** Optional lucide / MUI icon element — sized by the caller */
icon?: ReactElement
size?: 'small' | 'medium'
/** Wraps the badge in a Tooltip */
tooltip?: string
ariaLabel?: string
}
export function GenericBadge({
label,
color,
variant = 'transparent',
showBorder = false,
bold = false,
icon,
size = 'small',
tooltip,
ariaLabel,
}: GenericBadgeProps) {
const chip = (
<Chip
size={size}
label={label}
icon={icon}
aria-label={ariaLabel}
sx={{
fontWeight: bold ? 700 : 600,
fontSize: size === 'small' ? '0.7rem' : '0.8125rem',
bgcolor: variant === 'solid' ? color : `${color}18`,
color: variant === 'solid' ? 'white' : color,
border: variant === 'transparent' && showBorder ? `1px solid ${color}40` : 'none',
'& .MuiChip-icon': { ml: 0.5 },
}}
/>
)
return tooltip ? <Tooltip title={tooltip} arrow>{chip}</Tooltip> : chip
}
+2
View File
@@ -1,3 +1,5 @@
export { GenericBadge } from './GenericBadge'
export type { GenericBadgeProps } from './GenericBadge'
export { ViewToggle } from './ViewToggle' export { ViewToggle } from './ViewToggle'
export { HeatBadge } from './HeatBadge' export { HeatBadge } from './HeatBadge'
export { LocationPreview } from './LocationPreview' export { LocationPreview } from './LocationPreview'
@@ -1,4 +1,4 @@
import { Chip } from '@mui/material' import { GenericBadge } from '../shared/GenericBadge'
import type { ShortlistStatus } from '../../domain/enums' import type { ShortlistStatus } from '../../domain/enums'
const STATUS_META: Record<string, { label: string; color: string }> = { const STATUS_META: Record<string, { label: string; color: string }> = {
@@ -12,12 +12,6 @@ const STATUS_META: Record<string, { label: string; color: string }> = {
} }
export function ShortlistStatusBadge({ status }: { status: ShortlistStatus }) { export function ShortlistStatusBadge({ status }: { status: ShortlistStatus }) {
const meta = STATUS_META[status] ?? { label: status, color: '#64748b' } const { label, color } = STATUS_META[status] ?? { label: status, color: '#64748b' }
return ( return <GenericBadge label={label} color={color} variant="solid" />
<Chip
label={meta.label}
size="small"
sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 11 }}
/>
)
} }
+11
View File
@@ -0,0 +1,11 @@
import type { Property } from '../domain/property'
import { mockProperties } from './properties'
/**
* Shared mutable store for mock Property data.
*
* Imported by MockupPropertyProvider and MockupUnitProvider.
* Real providers (REST, Supabase, …) replace the entire provider file
* and never touch this module.
*/
export const propertyStore: Property[] = [...mockProperties]
+1 -2
View File
@@ -1,8 +1,7 @@
import type { IPropertyProvider, PropertyFilters } from './IPropertyProvider' import type { IPropertyProvider, PropertyFilters } from './IPropertyProvider'
import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property' import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property'
import { mockProperties } from '../mock-data/properties' import { propertyStore } from '../mock-data/propertyStore'
export const propertyStore: Property[] = [...mockProperties]
const store = propertyStore const store = propertyStore
export const MockupPropertyProvider: IPropertyProvider = { export const MockupPropertyProvider: IPropertyProvider = {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { IUnitProvider } from './IUnitProvider' import type { IUnitProvider } from './IUnitProvider'
import type { PropertyUnit } from '../domain/property' import type { PropertyUnit } from '../domain/property'
import { propertyStore } from './MockupPropertyProvider' import { propertyStore } from '../mock-data/propertyStore'
function getAllUnits(): PropertyUnit[] { function getAllUnits(): PropertyUnit[] {
return propertyStore.flatMap(p => return propertyStore.flatMap(p =>
+1 -1
View File
@@ -4,7 +4,7 @@
* another provider directly. * another provider directly.
*/ */
import { matchStore } from '../provider/MockupMatchProvider' import { matchStore } from '../provider/MockupMatchProvider'
import { propertyStore } from '../provider/MockupPropertyProvider' import { propertyStore } from '../mock-data/propertyStore'
import { MatchStrength, MatchStatus, RiskLevel, ResultType } from '../domain/enums' import { MatchStrength, MatchStatus, RiskLevel, ResultType } from '../domain/enums'
import type { Match } from '../domain/match' import type { Match } from '../domain/match'
import type { MatchEngineOutput } from '../domain/scoring' import type { MatchEngineOutput } from '../domain/scoring'