da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { Box, Button, Card, Divider, Slider, Typography } from '@mui/material'
|
|
import { RotateCcw } from 'lucide-react'
|
|
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
|
|
import type { WeightingKey } from '../../domain/needBuilder'
|
|
import { useDefaultWeights } from '../../hooks/useWeighting'
|
|
|
|
const HARD_KEYS: WeightingKey[] = ['area', 'location', 'budget', 'timing']
|
|
const SOFT_KEYS: WeightingKey[] = [
|
|
'prestige', 'accessibility', 'expansionPotential', 'flexibility',
|
|
'visibility', 'footfall', 'talentAccess', 'esg', 'taxEnvironment',
|
|
]
|
|
|
|
interface Props {
|
|
weights: Record<WeightingKey, number>
|
|
onChange: (weights: Record<WeightingKey, number>) => void
|
|
assetType?: string
|
|
}
|
|
|
|
const IMPORTANCE_LABELS = ['', 'Unwichtig', 'Wenig wichtig', 'Wichtig', 'Sehr wichtig', 'Entscheidend']
|
|
|
|
function toRaw(w: Record<WeightingKey, number>): Record<WeightingKey, number> {
|
|
const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0))
|
|
if (max === 0) return Object.fromEntries(WEIGHTING_KEYS.map(k => [k, 3])) as Record<WeightingKey, number>
|
|
return Object.fromEntries(
|
|
WEIGHTING_KEYS.map(k => [k, Math.max(1, Math.round(((w[k] ?? 0) / max) * 5))])
|
|
) as Record<WeightingKey, number>
|
|
}
|
|
|
|
function rawToWeights(raw: Record<WeightingKey, number>): Record<WeightingKey, number> {
|
|
const total = WEIGHTING_KEYS.reduce((s, k) => s + (raw[k] ?? 1), 0)
|
|
return Object.fromEntries(
|
|
WEIGHTING_KEYS.map(k => [k, (raw[k] ?? 1) / total])
|
|
) as Record<WeightingKey, number>
|
|
}
|
|
|
|
function SliderGroup({
|
|
label, keys, raw, onSlider, color,
|
|
}: {
|
|
label: string
|
|
keys: WeightingKey[]
|
|
raw: Record<WeightingKey, number>
|
|
onSlider: (key: WeightingKey, v: number) => void
|
|
color: string
|
|
}) {
|
|
return (
|
|
<Box>
|
|
<Typography variant="caption" sx={{ fontWeight: 700, color, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.65rem', display: 'block', mb: 1.5 }}>
|
|
{label}
|
|
</Typography>
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 32px' }}>
|
|
{keys.map(key => {
|
|
const importance = raw[key] ?? 3
|
|
return (
|
|
<Box key={key}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>{WEIGHTING_LABELS[key]}</Typography>
|
|
<Typography variant="caption" color="text.secondary">{IMPORTANCE_LABELS[importance]}</Typography>
|
|
</Box>
|
|
<Slider
|
|
value={importance}
|
|
min={1} max={5} step={1} marks
|
|
onChange={(_, v) => onSlider(key, v as number)}
|
|
size="small"
|
|
sx={{ color }}
|
|
/>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
|
const [raw, setRaw] = useState<Record<WeightingKey, number>>(() => toRaw(weights))
|
|
const defaultWeights = useDefaultWeights(assetType)
|
|
|
|
function handleSlider(key: WeightingKey, value: number) {
|
|
const updated = { ...raw, [key]: value }
|
|
setRaw(updated)
|
|
onChange(rawToWeights(updated))
|
|
}
|
|
|
|
function handleReset() {
|
|
setRaw(toRaw(defaultWeights))
|
|
onChange(defaultWeights)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ maxWidth: 720, mx: 'auto' }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
|
<Box>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
|
Wichtigkeit der Kriterien
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Schieber nach rechts = wichtiger. Gewichtung wird automatisch berechnet.
|
|
</Typography>
|
|
</Box>
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
startIcon={<RotateCcw size={14} />}
|
|
onClick={handleReset}
|
|
>
|
|
Zurücksetzen
|
|
</Button>
|
|
</Box>
|
|
|
|
<Card sx={{ p: 3 }}>
|
|
<SliderGroup label="Harte Kriterien" keys={HARD_KEYS} raw={raw} onSlider={handleSlider} color="#152642" />
|
|
<Divider sx={{ my: 2.5 }} />
|
|
<SliderGroup label="Weiche Kriterien" keys={SOFT_KEYS} raw={raw} onSlider={handleSlider} color="#7c3aed" />
|
|
</Card>
|
|
</Box>
|
|
)
|
|
}
|