feat: F016 future availability workspace

2-panel workspace at /supply/future-availability: filterable signal list with FutureSignalCard (type/sensitivity/review badges + mandatory disclaimer) and collapsible detail panel with full evidence, review workflow (IN_REVIEW→APPROVED/REJECTED), shortlist action, and review task creation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-16 21:19:52 +02:00
parent 65130efca7
commit 2e68638246
13 changed files with 767 additions and 216 deletions
@@ -0,0 +1,101 @@
import { Box, Chip, LinearProgress, Typography } from '@mui/material'
import { SignalTypeBadge } from './SignalTypeBadge'
import { SensitivityBadge } from './SensitivityBadge'
import { SignalReviewStatusBadge } from './SignalReviewStatusBadge'
import { FutureSignalDisclaimer } from './FutureSignalDisclaimer'
import type { FutureSignal } from '../../domain/futureSignal'
const SOURCE_LABELS: Record<string, string> = {
PRESS: 'Presse',
CONSTRUCTION_PERMIT: 'Baubewilligung',
JOB_POSTING: 'Stelleninserat',
COMPANY_REPORT: 'Geschäftsbericht',
MARKET_DATA: 'Marktdaten',
MANUAL: 'Manuell',
}
function probColor(p: number): string {
return p >= 0.7 ? '#1a7a4a' : p >= 0.5 ? '#d97706' : '#c0392b'
}
interface Props {
signal: FutureSignal
isSelected: boolean
onSelect: (signal: FutureSignal) => void
}
export function FutureSignalCard({ signal, isSelected, onSelect }: Props) {
const isConfidential = signal.sensitivityLevel === 'CONFIDENTIAL'
return (
<Box
onClick={() => onSelect(signal)}
sx={{
p: 2,
cursor: 'pointer',
borderBottom: '1px solid #f1f5f9',
borderLeft: isSelected
? '3px solid #1e3a5f'
: isConfidential
? '3px solid #d97706'
: '3px solid transparent',
bgcolor: isSelected ? '#eff6ff' : isConfidential ? '#fffbeb' : 'transparent',
'&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' },
}}
>
{/* Row 1: type + sensitivity + review status */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1 }}>
<SignalTypeBadge type={signal.signalType} />
<SensitivityBadge level={signal.sensitivityLevel} />
<SignalReviewStatusBadge status={signal.reviewStatus} />
</Box>
{/* Row 2: title / company / location */}
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.25 }}>
{signal.title ?? signal.companyName ?? signal.locationHint}
</Typography>
{(signal.title || signal.companyName) && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.75 }}>
{signal.locationHint}
</Typography>
)}
{/* Row 3: probability */}
<Box sx={{ mb: 0.75 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">Wahrscheinlichkeit</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: probColor(signal.probability) }}>
{Math.round(signal.probability * 100)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={signal.probability * 100}
sx={{
height: 4,
borderRadius: 2,
bgcolor: '#f1f5f9',
'& .MuiLinearProgress-bar': { bgcolor: probColor(signal.probability) },
}}
/>
</Box>
{/* Row 4: meta chips */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1 }}>
<Chip label={`${signal.timeHorizonMonths}M`} size="small" sx={{ fontSize: 10, height: 18 }} />
{signal.areaSqmEstimate && (
<Chip label={`~${signal.areaSqmEstimate.toLocaleString('de-CH')}`} size="small" sx={{ fontSize: 10, height: 18 }} />
)}
<Chip
label={SOURCE_LABELS[signal.source.type] ?? signal.source.type}
size="small"
variant="outlined"
sx={{ fontSize: 10, height: 18 }}
/>
</Box>
{/* Footer: mini disclaimer */}
<FutureSignalDisclaimer mini />
</Box>
)
}
@@ -0,0 +1,254 @@
import { Box, Button, Chip, CircularProgress, Divider, IconButton, LinearProgress, Paper, Snackbar, Typography } from '@mui/material'
import { X } from 'lucide-react'
import { useState } from 'react'
import { SignalTypeBadge } from './SignalTypeBadge'
import { SensitivityBadge } from './SensitivityBadge'
import { SignalReviewStatusBadge } from './SignalReviewStatusBadge'
import { FutureSignalDisclaimer } from './FutureSignalDisclaimer'
import { useUpdateSignalReviewStatus } from '../../hooks/useFutureSignals'
import { useShortlistStore } from '../../stores/shortlistStore'
import { reviewService } from '../../services/reviewService'
import { ReviewStatus } from '../../domain/enums'
import type { FutureSignal } from '../../domain/futureSignal'
const SOURCE_LABELS: Record<string, string> = {
PRESS: 'Pressebericht',
CONSTRUCTION_PERMIT: 'Baubewilligung',
JOB_POSTING: 'Stelleninserat',
COMPANY_REPORT: 'Geschäftsbericht',
MARKET_DATA: 'Marktdaten',
MANUAL: 'Manuell erfasst',
}
const CREDIBILITY_META: Record<string, { label: string; color: string }> = {
HIGH: { label: 'Hoch', color: '#1a7a4a' },
MEDIUM: { label: 'Mittel', color: '#d97706' },
LOW: { label: 'Niedrig', color: '#c0392b' },
}
function BarRow({ label, value }: { label: string; value: number }) {
const color = value >= 0.75 ? '#1a7a4a' : value >= 0.55 ? '#d97706' : '#c0392b'
return (
<Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color }}>{Math.round(value * 100)}%</Typography>
</Box>
<LinearProgress
variant="determinate"
value={value * 100}
sx={{ height: 5, borderRadius: 2, bgcolor: '#f1f5f9', '& .MuiLinearProgress-bar': { bgcolor: color } }}
/>
</Box>
)
}
interface Props {
signal: FutureSignal
onClose: () => void
}
export function FutureSignalDetailPanel({ signal, onClose }: Props) {
const updateStatus = useUpdateSignalReviewStatus()
const { openAddDialog } = useShortlistStore()
const [snackbar, setSnackbar] = useState<string | null>(null)
const [reviewTaskSent, setReviewTaskSent] = useState(false)
const reviewStatus = signal.reviewStatus ?? ReviewStatus.UNREVIEWED
const isRejected = reviewStatus === ReviewStatus.REJECTED
const isApproved = reviewStatus === ReviewStatus.APPROVED
async function handleStatus(status: typeof ReviewStatus[keyof typeof ReviewStatus]) {
await updateStatus.mutateAsync({ id: signal.id, status })
setSnackbar(`Status aktualisiert: ${status}`)
}
async function handleSendReview() {
await reviewService.createReviewTask(signal.id)
setReviewTaskSent(true)
setSnackbar('Prüfungsaufgabe erstellt')
}
function handleShortlist() {
openAddDialog({
resultId: signal.id,
resultType: 'FUTURE_AVAILABILITY',
title: signal.title ?? signal.companyName ?? signal.locationHint,
matchScore: Math.round(signal.confidenceScore * 100),
confidenceScore: signal.confidenceScore,
sourceLabel: SOURCE_LABELS[signal.source.type] ?? signal.source.type,
addedBy: 'admin@ideal-sharing.ch',
})
}
const credMeta = CREDIBILITY_META[signal.source.credibility] ?? { label: signal.source.credibility, color: '#64748b' }
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* Header */}
<Box sx={{ px: 2.5, py: 2, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ flex: 1, mr: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }}>
{signal.title ?? signal.companyName ?? signal.locationHint}
</Typography>
{(signal.title || signal.companyName) && (
<Typography variant="caption" color="text.secondary">{signal.locationHint}</Typography>
)}
</Box>
<IconButton size="small" onClick={onClose} sx={{ color: '#94a3b8', mt: -0.5 }}>
<X size={16} />
</IconButton>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
<SignalTypeBadge type={signal.signalType} />
<SensitivityBadge level={signal.sensitivityLevel} />
<SignalReviewStatusBadge status={signal.reviewStatus} />
{signal.isVerified && (
<Chip label="Verifiziert" size="small" sx={{ bgcolor: '#1a7a4a', color: 'white', fontSize: 10 }} />
)}
</Box>
</Box>
{/* Body */}
<Box sx={{ flex: 1, overflowY: 'auto', p: 2.5 }}>
<FutureSignalDisclaimer />
{/* Probability + confidence */}
<BarRow label="Wahrscheinlichkeit" value={signal.probability} />
<BarRow label="Konfidenz" value={signal.confidenceScore} />
<Divider sx={{ my: 1.5 }} />
{/* Meta */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, mb: 1.5 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Zeithorizont</Typography>
<Typography variant="caption">~{signal.timeHorizonMonths} Monate</Typography>
</Box>
{signal.areaSqmEstimate && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Geschätzte Fläche</Typography>
<Typography variant="caption">~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²</Typography>
</Box>
)}
{signal.companyName && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Unternehmen</Typography>
<Typography variant="caption">{signal.companyName}</Typography>
</Box>
)}
{signal.riskLevel && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Risikoniveau</Typography>
<Typography variant="caption">{signal.riskLevel}</Typography>
</Box>
)}
</Box>
<Divider sx={{ my: 1.5 }} />
{/* Source */}
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 0.75 }}>Quelle</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mb: 1.5 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Typ</Typography>
<Typography variant="caption">{SOURCE_LABELS[signal.source.type] ?? signal.source.type}</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Glaubwürdigkeit</Typography>
<Chip label={credMeta.label} size="small" sx={{ bgcolor: credMeta.color, color: 'white', fontSize: 10, height: 18 }} />
</Box>
{signal.source.publishedAt && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 140 }}>Veröffentlicht</Typography>
<Typography variant="caption">{signal.source.publishedAt}</Typography>
</Box>
)}
</Box>
{/* Evidence */}
{signal.evidence?.summary && (
<>
<Divider sx={{ my: 1.5 }} />
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 0.75 }}>Evidenz</Typography>
<Paper variant="outlined" sx={{ p: 1.5, bgcolor: '#f8fafc' }}>
<Typography variant="body2" color="text.secondary">{signal.evidence.summary}</Typography>
</Paper>
</>
)}
{/* Market indicator */}
{signal.marketIndicator && (
<>
<Divider sx={{ my: 1.5 }} />
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>Marktindikator</Typography>
<Typography variant="body2" color="text.secondary">{signal.marketIndicator}</Typography>
</>
)}
<Divider sx={{ my: 1.5 }} />
{/* Actions */}
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 1 }}>Aktionen</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{reviewStatus === ReviewStatus.UNREVIEWED && (
<Button
fullWidth size="small" variant="outlined"
disabled={updateStatus.isPending}
onClick={() => handleStatus(ReviewStatus.IN_REVIEW)}
sx={{ justifyContent: 'flex-start' }}
>
Verfolgen (In Prüfung setzen)
</Button>
)}
{!isRejected && !reviewTaskSent && (
<Button
fullWidth size="small" variant="outlined"
onClick={handleSendReview}
sx={{ justifyContent: 'flex-start', color: '#d97706', borderColor: '#d97706' }}
>
Zur Prüfung senden
</Button>
)}
{(reviewStatus === ReviewStatus.IN_REVIEW || reviewStatus === ReviewStatus.FLAGGED) && !isApproved && (
<Button
fullWidth size="small" variant="contained"
disabled={updateStatus.isPending}
onClick={() => handleStatus(ReviewStatus.APPROVED)}
sx={{ bgcolor: '#1a7a4a', '&:hover': { bgcolor: '#15643c' }, justifyContent: 'flex-start' }}
endIcon={updateStatus.isPending ? <CircularProgress size={14} color="inherit" /> : undefined}
>
Genehmigen
</Button>
)}
{!isRejected && (
<Button
fullWidth size="small" variant="outlined"
disabled={updateStatus.isPending}
onClick={() => handleStatus(ReviewStatus.REJECTED)}
sx={{ justifyContent: 'flex-start', color: '#c0392b', borderColor: '#c0392b' }}
>
Ablehnen
</Button>
)}
<Button
fullWidth size="small" variant="outlined"
onClick={handleShortlist}
sx={{ justifyContent: 'flex-start' }}
>
Zu Shortlist hinzufügen
</Button>
</Box>
</Box>
<Snackbar
open={!!snackbar}
autoHideDuration={2500}
onClose={() => setSnackbar(null)}
message={snackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
/>
</Box>
)
}
@@ -0,0 +1,27 @@
import { Alert, Typography } from '@mui/material'
interface Props {
mini?: boolean
}
export function FutureSignalDisclaimer({ mini = false }: Props) {
if (mini) {
return (
<Alert severity="warning" sx={{ py: 0.25, px: 1, '& .MuiAlert-message': { py: 0.25 } }}>
<Typography variant="caption">Probabilistisches Signal keine bestätigte Fläche</Typography>
</Alert>
)
}
return (
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Hinweis: Probabilistisches Zukunftssignal
</Typography>
<Typography variant="body2">
Dieses Signal basiert auf AI-Analyse öffentlicher Daten. Es handelt sich um keine bestätigte verfügbare Fläche.
Bitte ausschliesslich für strategische Beobachtung und interne Prüfung verwenden keine verbindlichen Aussagen gegenüber Dritten.
</Typography>
</Alert>
)
}
@@ -0,0 +1,28 @@
import { Box, Typography } from '@mui/material'
import { Filter, Radio } from 'lucide-react'
type EmptyContext = 'no-signals' | 'filtered-empty'
const META: Record<EmptyContext, { icon: React.ReactNode; title: string; desc: string }> = {
'no-signals': {
icon: <Radio size={32} color="#94a3b8" />,
title: 'Keine Signale vorhanden',
desc: 'Es wurden noch keine Zukunftssignale erfasst.',
},
'filtered-empty': {
icon: <Filter size={32} color="#94a3b8" />,
title: 'Keine Signale für diese Filter',
desc: 'Passen Sie die Filtereinstellungen an, um Signale anzuzeigen.',
},
}
export function FutureSignalEmptyState({ context }: { context: EmptyContext }) {
const { icon, title, desc } = META[context]
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 1.5, py: 8, px: 3 }}>
{icon}
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} color="text.secondary">{title}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', maxWidth: 280 }}>{desc}</Typography>
</Box>
)
}
@@ -0,0 +1,143 @@
import { Box, Chip, FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material'
import { SignalType } from '../../domain/enums'
import { SIGNAL_TYPE_LABELS } from '../../lib/constants'
export interface SignalFilterState {
signalType: string
minConfidence: number
sensitivityLevel: string
reviewStatus: string
timeHorizon: string
}
export const DEFAULT_SIGNAL_FILTERS: SignalFilterState = {
signalType: '',
minConfidence: 0,
sensitivityLevel: '',
reviewStatus: '',
timeHorizon: '',
}
interface Props {
filters: SignalFilterState
onChange: (f: SignalFilterState) => void
totalCount: number
filteredCount: number
}
const SIGNAL_TYPES = Object.values(SignalType)
export function FutureSignalFilterBar({ filters, onChange, totalCount, filteredCount }: Props) {
const set = (partial: Partial<SignalFilterState>) => onChange({ ...filters, ...partial })
const activeCount = [
filters.signalType !== '',
filters.minConfidence > 0,
filters.sensitivityLevel !== '',
filters.reviewStatus !== '',
filters.timeHorizon !== '',
].filter(Boolean).length
return (
<Box sx={{ px: 3, py: 1.5, bgcolor: 'white', borderBottom: '1px solid #e2e8f0', display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}>
{/* Signal type chips */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, alignItems: 'center' }}>
<Chip
label="Alle"
size="small"
onClick={() => set({ signalType: '' })}
color={filters.signalType === '' ? 'primary' : 'default'}
sx={{ fontWeight: filters.signalType === '' ? 700 : 400 }}
/>
{SIGNAL_TYPES.map(t => (
<Chip
key={t}
label={SIGNAL_TYPE_LABELS[t] ?? t}
size="small"
onClick={() => set({ signalType: filters.signalType === t ? '' : t })}
sx={{
fontWeight: filters.signalType === t ? 700 : 400,
bgcolor: filters.signalType === t ? '#1e3a5f' : undefined,
color: filters.signalType === t ? 'white' : undefined,
}}
/>
))}
</Box>
{/* Selects */}
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', flexWrap: 'wrap' }}>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Konfidenz</InputLabel>
<Select
label="Konfidenz"
value={filters.minConfidence}
onChange={e => set({ minConfidence: Number(e.target.value) })}
>
<MenuItem value={0}>Alle</MenuItem>
<MenuItem value={0.75}>Hoch 75%</MenuItem>
<MenuItem value={0.55}>Mittel 55%</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Prüfstatus</InputLabel>
<Select
label="Prüfstatus"
value={filters.reviewStatus}
onChange={e => set({ reviewStatus: e.target.value })}
>
<MenuItem value="">Alle</MenuItem>
<MenuItem value="UNREVIEWED">Ungeprüft</MenuItem>
<MenuItem value="IN_REVIEW">In Prüfung</MenuItem>
<MenuItem value="APPROVED">Genehmigt</MenuItem>
<MenuItem value="REJECTED">Abgelehnt</MenuItem>
<MenuItem value="FLAGGED">Markiert</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Zeithorizont</InputLabel>
<Select
label="Zeithorizont"
value={filters.timeHorizon}
onChange={e => set({ timeHorizon: e.target.value })}
>
<MenuItem value="">Alle</MenuItem>
<MenuItem value="short">Kurz 6M</MenuItem>
<MenuItem value="medium">Mittel 712M</MenuItem>
<MenuItem value="long">Lang &gt;12M</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Vertraulichkeit</InputLabel>
<Select
label="Vertraulichkeit"
value={filters.sensitivityLevel}
onChange={e => set({ sensitivityLevel: e.target.value })}
>
<MenuItem value="">Alle</MenuItem>
<MenuItem value="PUBLIC">Öffentlich</MenuItem>
<MenuItem value="INTERNAL">Intern</MenuItem>
<MenuItem value="CONFIDENTIAL">Vertraulich</MenuItem>
</Select>
</FormControl>
</Box>
{/* Result count */}
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
{activeCount > 0 && (
<Chip
label={`${activeCount} Filter aktiv`}
size="small"
onDelete={() => onChange(DEFAULT_SIGNAL_FILTERS)}
sx={{ bgcolor: '#eff6ff', color: '#1e3a5f' }}
/>
)}
<Typography variant="caption" color="text.secondary">
{filteredCount} / {totalCount}
</Typography>
</Box>
</Box>
)
}
@@ -0,0 +1,19 @@
import { Chip } from '@mui/material'
const SENSITIVITY_META: Record<string, { label: string; color: string }> = {
PUBLIC: { label: 'Öffentlich', color: '#64748b' },
INTERNAL: { label: 'Intern', color: '#d97706' },
CONFIDENTIAL: { label: 'Vertraulich', color: '#c0392b' },
RESTRICTED: { label: 'Eingeschränkt', color: '#7c3aed' },
}
export function SensitivityBadge({ level }: { level: string }) {
const meta = SENSITIVITY_META[level] ?? { label: level, color: '#64748b' }
return (
<Chip
label={meta.label}
size="small"
sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 10 }}
/>
)
}
@@ -0,0 +1,22 @@
import { Chip } from '@mui/material'
import type { ReviewStatus } from '../../domain/enums'
const STATUS_META: Record<string, { label: string; color: string }> = {
UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' },
IN_REVIEW: { label: 'In Prüfung', color: '#d97706' },
APPROVED: { label: 'Genehmigt', color: '#1a7a4a' },
REJECTED: { label: 'Abgelehnt', color: '#c0392b' },
FLAGGED: { label: 'Markiert', color: '#ea580c' },
}
export function SignalReviewStatusBadge({ status }: { status?: ReviewStatus | null }) {
const key = status ?? 'UNREVIEWED'
const meta = STATUS_META[key] ?? { label: key, color: '#94a3b8' }
return (
<Chip
label={meta.label}
size="small"
sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 10 }}
/>
)
}
+9
View File
@@ -1 +1,10 @@
export { SignalTypeBadge } from './SignalTypeBadge'
export { FutureSignalDisclaimer } from './FutureSignalDisclaimer'
export { SensitivityBadge } from './SensitivityBadge'
export { SignalReviewStatusBadge } from './SignalReviewStatusBadge'
export { FutureSignalCard } from './FutureSignalCard'
export { FutureSignalFilterBar } from './FutureSignalFilterBar'
export { FutureSignalDetailPanel } from './FutureSignalDetailPanel'
export { FutureSignalEmptyState } from './FutureSignalEmptyState'
export type { SignalFilterState } from './FutureSignalFilterBar'
export { DEFAULT_SIGNAL_FILTERS } from './FutureSignalFilterBar'