Files
property-match/src/pages/supply/MatchCenter.tsx
T
Benjamin Sutter 5035445d2e refactor: standardize page title typography across all workspaces
All page-level headers now use fontSize:1.125rem, fontWeight:700,
color:#0f172a, container py:2.5, borderBottom:#e8e7e4 — matching
the standard established in PageHeader. Removes variant="h5"/h6"
inconsistencies across Supply, Demand, and shared components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 00:01:15 +02:00

202 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react'
import {
Box,
Chip,
Drawer,
IconButton,
MenuItem,
Select,
Typography,
} from '@mui/material'
import { X } from 'lucide-react'
import { useMatches, useApproveMatch } from '../../hooks/useMatches'
import { useProperties } from '../../hooks/useProperties'
import { useNeeds } from '../../hooks/useNeeds'
import { useMatchCenterStore } from '../../stores/matchCenterStore'
import { useToastStore } from '../../stores/toastStore'
import { MatchListCard, MatchBriefingPanel, MatchCenterSkeleton } from '../../components/match-center'
import { ErrorState } from '../../components/ui'
import type { Match } from '../../domain/match'
const STRENGTH_OPTIONS = [
{ value: '', label: 'Alle Stärken' },
{ value: 'STRONG', label: 'Stark (≥80)' },
{ value: 'MODERATE', label: 'Mittel (6079)' },
{ value: 'WEAK', label: 'Schwach (<60)' },
]
const STATUS_OPTIONS = [
{ value: '', label: 'Alle Status' },
{ value: 'PENDING_REVIEW', label: 'Ausstehend' },
{ value: 'APPROVED', label: 'Genehmigt' },
{ value: 'REJECTED', label: 'Abgelehnt' },
]
export default function MatchCenter() {
const { data: matches = [], isLoading, isError, refetch } = useMatches()
const { data: properties = [] } = useProperties()
const { data: needs = [] } = useNeeds()
const { setSelectedProperty, setSelectedNeed } = useMatchCenterStore()
const approveMatch = useApproveMatch()
const showToast = useToastStore((s) => s.showToast)
const [selectedMatchId, setSelectedMatchId] = useState<string | null>(null)
const [filterStrength, setFilterStrength] = useState('')
const [filterStatus, setFilterStatus] = useState('')
const propMap = useMemo(() => new Map(properties.map(p => [p.id, p])), [properties])
const needMap = useMemo(() => new Map(needs.map(n => [n.id, n])), [needs])
const filtered = useMemo(() => {
return matches
.filter(m => {
if (filterStrength && m.matchStrength !== filterStrength) return false
if (filterStatus && m.status !== filterStatus) return false
return true
})
.sort((a, b) => b.matchScore - a.matchScore)
}, [matches, filterStrength, filterStatus])
const strongCount = matches.filter(m => m.matchScore >= 80).length
const pendingCount = matches.filter(m => m.status === 'PENDING_REVIEW').length
function handleSelectMatch(match: Match) {
setSelectedMatchId(match.id)
setSelectedProperty(match.propertyId)
setSelectedNeed(match.needId)
}
function handleCloseDrawer() {
setSelectedMatchId(null)
setSelectedProperty(null)
setSelectedNeed(null)
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)', overflow: 'hidden' }}>
{/* Header */}
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e8e7e4', px: 3, py: 2.5, flexShrink: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 2, mb: 1 }}>
<Typography sx={{ fontWeight: 700, fontSize: '1.125rem', lineHeight: 1.3, color: '#0f172a' }}>Match Center</Typography>
<Typography variant="body2" color="text.secondary">Automatisch berechnete Matches</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label={`${matches.length} Matches`} size="small" sx={{ bgcolor: '#f1f5f9', color: '#475569' }} />
<Chip
label={`${strongCount} Stark`}
size="small"
sx={{ bgcolor: '#f0fdf4', color: '#1a7a4a', fontWeight: 600 }}
/>
{pendingCount > 0 && (
<Chip
label={`${pendingCount} Ausstehend`}
size="small"
sx={{ bgcolor: '#fef3c7', color: '#d97706', fontWeight: 600 }}
/>
)}
</Box>
</Box>
{/* Filter bar */}
<Box
sx={{
bgcolor: 'white',
borderBottom: '1px solid #e2e8f0',
px: 3,
py: 1,
display: 'flex',
gap: 1.5,
alignItems: 'center',
flexShrink: 0,
}}
>
<Select
size="small"
value={filterStrength}
onChange={e => setFilterStrength(e.target.value)}
displayEmpty
sx={{ fontSize: '0.8125rem', minWidth: 160 }}
>
{STRENGTH_OPTIONS.map(o => (
<MenuItem key={o.value} value={o.value} sx={{ fontSize: '0.8125rem' }}>{o.label}</MenuItem>
))}
</Select>
<Select
size="small"
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
displayEmpty
sx={{ fontSize: '0.8125rem', minWidth: 160 }}
>
{STATUS_OPTIONS.map(o => (
<MenuItem key={o.value} value={o.value} sx={{ fontSize: '0.8125rem' }}>{o.label}</MenuItem>
))}
</Select>
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>
{filtered.length} von {matches.length} Matches
</Typography>
</Box>
{/* Match list */}
<Box sx={{ flex: 1, overflowY: 'auto', bgcolor: '#f8fafc' }}>
{isLoading ? (
<MatchCenterSkeleton />
) : isError ? (
<ErrorState onRetry={refetch} />
) : filtered.length === 0 ? (
<Box sx={{ p: 6, textAlign: 'center' }}>
<Typography color="text.secondary">Keine Matches für diese Filter.</Typography>
</Box>
) : (
<Box sx={{ bgcolor: 'white' }}>
{filtered.map(match => (
<MatchListCard
key={match.id}
match={match}
property={propMap.get(match.propertyId)}
need={needMap.get(match.needId)}
onSelect={() => handleSelectMatch(match)}
onApprove={() => approveMatch.mutate(match.id, {
onSuccess: () => showToast('Match genehmigt.'),
onError: () => showToast('Genehmigung fehlgeschlagen.', 'error'),
})}
/>
))}
</Box>
)}
</Box>
{/* Detail Drawer */}
<Drawer
anchor="right"
open={!!selectedMatchId}
onClose={handleCloseDrawer}
slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }}
>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2.5,
py: 1.5,
borderBottom: '1px solid #e2e8f0',
flexShrink: 0,
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Match-Briefing</Typography>
<IconButton size="small" onClick={handleCloseDrawer} sx={{ color: '#94a3b8' }}>
<X size={16} />
</IconButton>
</Box>
<Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<MatchBriefingPanel />
</Box>
</Box>
</Drawer>
</Box>
)
}