Initial commit

This commit is contained in:
Benjamin Sutter
2026-05-15 00:48:18 +02:00
commit 9e827c50f9
72 changed files with 10477 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
import {
Box,
Button,
Card,
Chip,
Divider,
LinearProgress,
Typography,
} from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { EmptyState, ErrorState, LoadingPage } from '../../components/ui'
import { futureSignalService } from '../../services/futureSignalService'
import { SignalType } from '../../domain/enums'
function getSignalTypeLabel(type: SignalType): string {
switch (type) {
case SignalType.EXPANSION: return 'Expansion'
case SignalType.POSSIBLE_MOVE_OUT: return 'Möglicher Auszug'
case SignalType.CONSTRUCTION_PROJECT: return 'Bauprojekt'
case SignalType.RESTRUCTURING: return 'Restrukturierung'
case SignalType.PROJECT_DEVELOPMENT: return 'Projektentwicklung'
case SignalType.SPACE_CONSOLIDATION: return 'Flächenkonsolidierung'
}
}
function getSignalTypeColor(type: SignalType): string {
switch (type) {
case SignalType.EXPANSION: return '#1a7a4a'
case SignalType.POSSIBLE_MOVE_OUT: return '#d97706'
case SignalType.CONSTRUCTION_PROJECT: return '#1e3a5f'
case SignalType.RESTRUCTURING: return '#ea580c'
case SignalType.PROJECT_DEVELOPMENT: return '#7c3aed'
case SignalType.SPACE_CONSOLIDATION: return '#6b7280'
}
}
function getProbabilityColor(prob: number): 'success' | 'warning' | 'error' {
if (prob > 0.7) return 'success'
if (prob >= 0.5) return 'warning'
return 'error'
}
function formatDate(dateStr?: string): string {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' })
}
function getSourceTypeLabel(type: string): string {
switch (type) {
case 'PRESS': return 'Pressebericht'
case 'CONSTRUCTION_PERMIT': return 'Baubewilligung'
case 'JOB_POSTING': return 'Stelleninserat'
case 'COMPANY_REPORT': return 'Geschäftsbericht'
case 'MARKET_DATA': return 'Marktdaten'
case 'MANUAL': return 'Manuell'
default: return type
}
}
export default function FutureAvailability() {
const queryClient = useQueryClient()
const { data: resp, isLoading, error } = useQuery({
queryKey: ['futureSignals'],
queryFn: () => futureSignalService.getAll(),
})
const verifyMutation = useMutation({
mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'admin@ideal-sharing.ch'),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['futureSignals'] }),
})
if (isLoading) return <LoadingPage />
if (error) return <ErrorState />
const signals = resp?.data ?? []
const totalCount = signals.length
const verifiedCount = signals.filter(s => s.isVerified).length
const highProbCount = signals.filter(s => s.probability > 0.7).length
const avgProbability = signals.length
? signals.reduce((sum, s) => sum + s.probability, 0) / signals.length
: 0
return (
<Box>
{/* Page Header */}
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid', borderColor: 'divider', px: 3, py: 2 }} className="flex items-center gap-3">
<Box className="flex-1">
<Box className="flex items-center gap-2">
<Typography variant="h5" fontWeight={700} color="text.primary">Marktchancen</Typography>
<Chip
label="Shadow Intelligence Layer"
size="small"
sx={{ bgcolor: '#7c3aed', color: 'white', fontWeight: 600 }}
/>
</Box>
<Typography variant="body2" color="text.secondary">KI-generierte Verfügbarkeitssignale</Typography>
</Box>
</Box>
{/* Content */}
<Box sx={{ px: 3, py: 3 }} className="flex flex-col gap-4">
{/* Stats Row */}
<Box className="grid grid-cols-4 gap-4">
<Card sx={{ elevation: 1, p: 2.5 }}>
<Typography variant="overline" color="text.secondary" display="block">Signale gesamt</Typography>
<Typography variant="h4" fontWeight={700}>{totalCount}</Typography>
</Card>
<Card sx={{ elevation: 1, p: 2.5 }}>
<Typography variant="overline" color="text.secondary" display="block">Verifiziert</Typography>
<Typography variant="h4" fontWeight={700} sx={{ color: '#1a7a4a' }}>{verifiedCount}</Typography>
</Card>
<Card sx={{ elevation: 1, p: 2.5 }}>
<Typography variant="overline" color="text.secondary" display="block">Hohe Wahrscheinlichkeit</Typography>
<Typography variant="h4" fontWeight={700} sx={{ color: '#1e3a5f' }}>{highProbCount}</Typography>
</Card>
<Card sx={{ elevation: 1, p: 2.5 }}>
<Typography variant="overline" color="text.secondary" display="block">Ø Wahrscheinlichkeit</Typography>
<Typography variant="h4" fontWeight={700} sx={{ color: getProbabilityColor(avgProbability) === 'success' ? '#1a7a4a' : getProbabilityColor(avgProbability) === 'warning' ? '#d97706' : '#c0392b' }}>
{Math.round(avgProbability * 100)}%
</Typography>
</Card>
</Box>
{/* Signal Cards Grid */}
{signals.length === 0 ? (
<EmptyState title="Keine Signale gefunden" description="Es sind noch keine Zukunftssignale vorhanden." />
) : (
<Box className="flex flex-wrap gap-4">
{signals.map(signal => (
<Card key={signal.id} sx={{ elevation: 1, p: 2, flex: '1 1 calc(50% - 16px)', minWidth: 320 }}>
{/* Card Header */}
<Box className="flex items-center justify-between mb-2">
<Chip
label={getSignalTypeLabel(signal.signalType)}
size="small"
sx={{ bgcolor: getSignalTypeColor(signal.signalType), color: 'white', fontWeight: 600 }}
/>
<Chip
label={signal.sensitivityLevel === 'PUBLIC' ? 'Öffentlich' : signal.sensitivityLevel === 'CONFIDENTIAL' ? 'Vertraulich' : 'Intern'}
size="small"
variant="outlined"
color={signal.sensitivityLevel === 'CONFIDENTIAL' ? 'error' : 'default'}
/>
</Box>
{/* Location */}
<Box className="mb-2">
<Typography variant="body1" fontWeight={500}>{signal.locationHint}</Typography>
{signal.companyName && (
<Typography variant="body2" color="text.secondary">{signal.companyName}</Typography>
)}
</Box>
{/* Probability */}
<Box className="mb-2">
<Box className="flex items-center justify-between mb-1">
<Typography variant="caption" color="text.secondary">Wahrscheinlichkeit</Typography>
<Typography variant="body2" fontWeight={700} sx={{ color: signal.probability > 0.7 ? '#1a7a4a' : signal.probability >= 0.5 ? '#d97706' : '#c0392b' }}>
{Math.round(signal.probability * 100)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={signal.probability * 100}
color={getProbabilityColor(signal.probability)}
sx={{ height: 6, borderRadius: 3 }}
/>
</Box>
{/* Details */}
<Box className="flex flex-wrap gap-2 mb-2">
{signal.areaSqmEstimate && (
<Typography variant="caption" color="text.secondary">
Fläche: ca. {signal.areaSqmEstimate.toLocaleString('de-CH')} m²
</Typography>
)}
<Typography variant="caption" color="text.secondary">
Zeithorizont: {signal.timeHorizonMonths} Monate
</Typography>
{signal.expiresAt && (
<Typography variant="caption" color="text.secondary">
Verfügbar ab: {formatDate(signal.expiresAt)}
</Typography>
)}
</Box>
{/* Source */}
<Box className="mb-2">
<Typography variant="caption" color="text.secondary">
Quelle: {getSourceTypeLabel(signal.source.type)} Glaubwürdigkeit:{' '}
<span style={{ color: signal.source.credibility === 'HIGH' ? '#1a7a4a' : signal.source.credibility === 'MEDIUM' ? '#d97706' : '#c0392b', fontWeight: 600 }}>
{signal.source.credibility === 'HIGH' ? 'Hoch' : signal.source.credibility === 'MEDIUM' ? 'Mittel' : 'Niedrig'}
</span>
</Typography>
</Box>
{/* Verification Status */}
<Box className="mb-2">
{signal.isVerified ? (
<Box className="flex items-center gap-2">
<Chip label="Verifiziert" color="success" size="small" />
<Typography variant="caption" color="text.secondary">{formatDate(signal.verifiedAt)}</Typography>
</Box>
) : (
<Chip label="Nicht verifiziert" color="warning" size="small" variant="outlined" />
)}
</Box>
<Divider sx={{ my: 1 }} />
{/* Footer buttons */}
<Box className="flex items-center gap-2">
{!signal.isVerified && (
<Button
variant="outlined"
size="small"
onClick={() => verifyMutation.mutate(signal.id)}
disabled={verifyMutation.isPending}
>
Verifizieren
</Button>
)}
<Button variant="outlined" size="small" disabled>
Zu Shortlist
</Button>
</Box>
</Card>
))}
</Box>
)}
</Box>
</Box>
)
}