e13fe470fb
650px right Drawer overlays the list instead of compressing it — full list always visible, detail panel has adequate space. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { Box, Chip, Drawer, Typography } from '@mui/material'
|
|
import { useFutureSignals } from '../../hooks/useFutureSignals'
|
|
import {
|
|
FutureSignalCard,
|
|
FutureSignalFilterBar,
|
|
FutureSignalDetailPanel,
|
|
FutureSignalEmptyState,
|
|
DEFAULT_SIGNAL_FILTERS,
|
|
} from '../../components/future-signals'
|
|
import { AddToShortlistDialog } from '../../components/shortlist'
|
|
import type { SignalFilterState } from '../../components/future-signals'
|
|
import type { FutureSignal } from '../../domain/futureSignal'
|
|
|
|
function applyFilters(signals: FutureSignal[], f: SignalFilterState): FutureSignal[] {
|
|
return signals.filter(s => {
|
|
if (f.signalType && s.signalType !== f.signalType) return false
|
|
if (f.minConfidence > 0 && s.confidenceScore < f.minConfidence) return false
|
|
if (f.sensitivityLevel && s.sensitivityLevel !== f.sensitivityLevel) return false
|
|
if (f.reviewStatus) {
|
|
const rs = s.reviewStatus ?? 'UNREVIEWED'
|
|
if (rs !== f.reviewStatus) return false
|
|
}
|
|
if (f.timeHorizon === 'short' && s.timeHorizonMonths > 6) return false
|
|
if (f.timeHorizon === 'medium' && (s.timeHorizonMonths <= 6 || s.timeHorizonMonths > 12)) return false
|
|
if (f.timeHorizon === 'long' && s.timeHorizonMonths <= 12) return false
|
|
return true
|
|
})
|
|
}
|
|
|
|
export default function FutureAvailability() {
|
|
const { data: signals = [], isLoading } = useFutureSignals()
|
|
const [filters, setFilters] = useState<SignalFilterState>(DEFAULT_SIGNAL_FILTERS)
|
|
const [selectedSignal, setSelectedSignal] = useState<FutureSignal | null>(null)
|
|
|
|
const filtered = applyFilters(signals, filters)
|
|
|
|
const needsReviewCount = signals.filter(s => !s.reviewStatus || s.reviewStatus === 'UNREVIEWED').length
|
|
const highConfCount = signals.filter(s => s.confidenceScore >= 0.75).length
|
|
const confidentialCount = signals.filter(s => s.sensitivityLevel === 'CONFIDENTIAL').length
|
|
|
|
function handleSelect(signal: FutureSignal) {
|
|
setSelectedSignal(prev => prev?.id === signal.id ? null : signal)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)', overflow: 'hidden' }}>
|
|
<AddToShortlistDialog />
|
|
|
|
{/* Header */}
|
|
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0', px: 3, py: 2, flexShrink: 0 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
|
<Box>
|
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>Zukunftssignale</Typography>
|
|
<Typography variant="body2" color="text.secondary">Probabilistische Markt- und Verfügbarkeitssignale</Typography>
|
|
</Box>
|
|
<Chip
|
|
label="Shadow Intelligence Layer"
|
|
size="small"
|
|
sx={{ bgcolor: '#7c3aed', color: 'white', fontWeight: 600 }}
|
|
/>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
<Chip
|
|
label={`${signals.length} Signale`}
|
|
size="small"
|
|
sx={{ bgcolor: '#f1f5f9', color: '#475569' }}
|
|
/>
|
|
<Chip
|
|
label={`${needsReviewCount} Prüfung erforderlich`}
|
|
size="small"
|
|
sx={{ bgcolor: needsReviewCount > 0 ? '#fef3c7' : '#f1f5f9', color: needsReviewCount > 0 ? '#d97706' : '#475569', fontWeight: needsReviewCount > 0 ? 600 : 400 }}
|
|
/>
|
|
<Chip
|
|
label={`${highConfCount} hohe Konfidenz`}
|
|
size="small"
|
|
sx={{ bgcolor: '#f0fdf4', color: '#1a7a4a' }}
|
|
/>
|
|
{confidentialCount > 0 && (
|
|
<Chip
|
|
label={`${confidentialCount} vertraulich`}
|
|
size="small"
|
|
sx={{ bgcolor: '#fff1f2', color: '#c0392b', fontWeight: 600 }}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Filter bar */}
|
|
<FutureSignalFilterBar
|
|
filters={filters}
|
|
onChange={setFilters}
|
|
totalCount={signals.length}
|
|
filteredCount={filtered.length}
|
|
/>
|
|
|
|
{/* Signal list — always full width */}
|
|
<Box sx={{ flex: 1, overflowY: 'auto', bgcolor: '#f8fafc' }}>
|
|
{isLoading ? (
|
|
<Box sx={{ p: 3 }}>
|
|
{[0, 1, 2, 3].map(i => (
|
|
<Box key={i} sx={{ p: 2, mb: 1, bgcolor: 'white', borderRadius: 1, height: 120 }} />
|
|
))}
|
|
</Box>
|
|
) : filtered.length === 0 ? (
|
|
<FutureSignalEmptyState context={signals.length === 0 ? 'no-signals' : 'filtered-empty'} />
|
|
) : (
|
|
<Box sx={{ bgcolor: 'white' }}>
|
|
{filtered.map(signal => (
|
|
<FutureSignalCard
|
|
key={signal.id}
|
|
signal={signal}
|
|
isSelected={selectedSignal?.id === signal.id}
|
|
onSelect={handleSelect}
|
|
/>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Detail drawer */}
|
|
<Drawer
|
|
anchor="right"
|
|
open={!!selectedSignal}
|
|
onClose={() => setSelectedSignal(null)}
|
|
slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }}
|
|
>
|
|
{selectedSignal && (
|
|
<FutureSignalDetailPanel
|
|
signal={selectedSignal}
|
|
onClose={() => setSelectedSignal(null)}
|
|
/>
|
|
)}
|
|
</Drawer>
|
|
</Box>
|
|
)
|
|
}
|