Files
property-match/src/pages/supply/FutureAvailability.tsx
T
Benjamin Sutter 6515acb7f0 feat: unified error handling + AI service modularisation
Error handling (Prompt 2):
- src/services/errors.ts: AppError class, normalizeError(), throwServiceError() helper
- 6 services wrapped with try/catch (property, match, need, shortlist, futureSignal, inquiry)
- inquiryService aligned from custom ServiceResult<T> to standard ServiceResponse types
- Results, MatchCenter, FutureAvailability pages show <ErrorState onRetry> on query failure

AI modularisation (Prompt 3):
- src/services/aiService.ts reduced from 755 → 19 lines (barrel re-export)
- src/services/ai/IAIService.ts: typed interface + all response types
- src/services/ai/mock/: needParser, compareBuilder, decisionBrief, listingParser, MockAIService
- src/services/ai/openrouter/OpenRouterAIService.ts: model-agnostic skeleton
- src/services/ai/prompts/: 4 prompt template files (needParsing, matchExplanation, compareSummary, decisionBrief)
- src/services/ai/index.ts: factory selects Mock or OpenRouter via VITE_USE_REAL_AI flag
- All existing import paths unchanged — zero call-site modifications

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 00:32:01 +02:00

141 lines
5.3 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 { ErrorState } from '../../components/ui'
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, isError, refetch } = 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>
) : isError ? (
<ErrorState onRetry={refetch} />
) : 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>
)
}