feat: F023 signal-to-match pipeline & shadow market decision flow
This commit is contained in:
@@ -24,6 +24,7 @@ const Governance = lazy(() => import('./pages/ops/Governance'))
|
|||||||
const MarketIntelligence = lazy(() => import('./pages/ops/MarketIntelligence'))
|
const MarketIntelligence = lazy(() => import('./pages/ops/MarketIntelligence'))
|
||||||
const SourceMonitoring = lazy(() => import('./pages/ops/SourceMonitoring'))
|
const SourceMonitoring = lazy(() => import('./pages/ops/SourceMonitoring'))
|
||||||
const ActivityTimeline = lazy(() => import('./pages/ops/ActivityTimeline'))
|
const ActivityTimeline = lazy(() => import('./pages/ops/ActivityTimeline'))
|
||||||
|
const SignalPipeline = lazy(() => import('./pages/ops/SignalPipeline'))
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -63,6 +64,7 @@ function App() {
|
|||||||
<Route path="/ops/market-intelligence" element={<MarketIntelligence />} />
|
<Route path="/ops/market-intelligence" element={<MarketIntelligence />} />
|
||||||
<Route path="/ops/source-monitoring" element={<SourceMonitoring />} />
|
<Route path="/ops/source-monitoring" element={<SourceMonitoring />} />
|
||||||
<Route path="/ops/activity-timeline" element={<ActivityTimeline />} />
|
<Route path="/ops/activity-timeline" element={<ActivityTimeline />} />
|
||||||
|
<Route path="/ops/signal-pipeline" element={<SignalPipeline />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Radar,
|
Radar,
|
||||||
ServerCog,
|
ServerCog,
|
||||||
|
GitBranch,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { LucideIcon } from 'lucide-react'
|
import type { LucideIcon } from 'lucide-react'
|
||||||
import { OrganizationContextBadge } from './OrganizationContextBadge'
|
import { OrganizationContextBadge } from './OrganizationContextBadge'
|
||||||
@@ -102,6 +103,7 @@ const WORKSPACE_CONFIG: Record<WorkspaceType, WorkspaceConfig> = {
|
|||||||
{ path: '/ops/governance', label: 'Governance', icon: Shield },
|
{ path: '/ops/governance', label: 'Governance', icon: Shield },
|
||||||
{ path: '/ops/market-intelligence', label: 'Market Intelligence', icon: Radar },
|
{ path: '/ops/market-intelligence', label: 'Market Intelligence', icon: Radar },
|
||||||
{ path: '/ops/source-monitoring', label: 'Source Monitoring', icon: ServerCog },
|
{ path: '/ops/source-monitoring', label: 'Source Monitoring', icon: ServerCog },
|
||||||
|
{ path: '/ops/signal-pipeline', label: 'Signal Pipeline', icon: GitBranch },
|
||||||
{ path: '/ops/activity-timeline', label: 'Aktivitäts-Timeline', icon: Clock },
|
{ path: '/ops/activity-timeline', label: 'Aktivitäts-Timeline', icon: Clock },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { Box, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||||
|
import { CheckCircle2, ChevronRight, Target, XCircle } from 'lucide-react'
|
||||||
|
import type { GateEvaluation } from '../../domain/signalPipeline'
|
||||||
|
import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface ConfidenceGatePanelProps {
|
||||||
|
gate: GateEvaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfidenceBar({ value }: { value: string | undefined }) {
|
||||||
|
if (!value) return null
|
||||||
|
|
||||||
|
const score = parseFloat(value)
|
||||||
|
if (isNaN(score)) return null
|
||||||
|
|
||||||
|
const pct = Math.round(score * 100)
|
||||||
|
|
||||||
|
const barColor =
|
||||||
|
score >= 0.75 ? '#15803d'
|
||||||
|
: score >= 0.35 ? '#d97706'
|
||||||
|
: '#dc2626'
|
||||||
|
|
||||||
|
const threshold =
|
||||||
|
score >= 0.75 ? 'Hoch (≥ 75%)'
|
||||||
|
: score >= 0.35 ? 'Mittel (≥ 35%)'
|
||||||
|
: 'Niedrig (< 35%)'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 1.25 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
Konfidenz-Schwelle: {threshold}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: barColor, fontWeight: 600 }}>
|
||||||
|
{pct}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ height: 6, bgcolor: 'rgba(148,163,184,0.2)', borderRadius: 3, overflow: 'hidden' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
width: `${pct}%`,
|
||||||
|
bgcolor: barColor,
|
||||||
|
borderRadius: 3,
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 0.25 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.65rem' }}>
|
||||||
|
0%
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: '#d97706', fontSize: '0.65rem' }}>
|
||||||
|
35%
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: '#15803d', fontSize: '0.65rem' }}>
|
||||||
|
75%
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.65rem' }}>
|
||||||
|
100%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfidenceGatePanel({ gate }: ConfidenceGatePanelProps) {
|
||||||
|
const { bg, fg } = GATE_STATUS_COLORS[gate.status]
|
||||||
|
|
||||||
|
const confidenceCheck = gate.checks.find(c => c.label.startsWith('Konfidenz'))
|
||||||
|
const confidenceValue = confidenceCheck?.value
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ mb: 1.5, borderRadius: 1.5 }}>
|
||||||
|
<Box sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Target size={14} color="#64748b" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Konfidenz-Gate
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ ml: 'auto' }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={GATE_STATUS_LABELS[gate.status]}
|
||||||
|
sx={{ bgcolor: bg, color: fg, border: 'none', fontSize: '0.7rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
{gate.checks.map((check, i) => (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{check.passed
|
||||||
|
? <CheckCircle2 size={13} color="#15803d" />
|
||||||
|
: <XCircle size={13} color="#dc2626" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption">
|
||||||
|
{check.label}
|
||||||
|
{check.value && (
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
|
||||||
|
{check.value}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{check.note && (
|
||||||
|
<Box component="span" sx={{ color: '#a16207', ml: 0.5, fontStyle: 'italic' }}>
|
||||||
|
({check.note})
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
<ConfidenceBar value={confidenceValue} />
|
||||||
|
{gate.reason && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'text.secondary', mt: 1, display: 'block' }}
|
||||||
|
>
|
||||||
|
{gate.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{gate.nextAction && gate.status !== GateStatus.PASSED && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.5 }}>
|
||||||
|
<ChevronRight size={12} color="#1d4ed8" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#1d4ed8', display: 'block' }}>
|
||||||
|
{gate.nextAction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Box, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||||
|
import { CheckCircle2, ChevronRight, FileSearch, XCircle } from 'lucide-react'
|
||||||
|
import type { GateEvaluation } from '../../domain/signalPipeline'
|
||||||
|
import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface EvidenceGatePanelProps {
|
||||||
|
gate: GateEvaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EvidenceGatePanel({ gate }: EvidenceGatePanelProps) {
|
||||||
|
const { bg, fg } = GATE_STATUS_COLORS[gate.status]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ mb: 1.5, borderRadius: 1.5 }}>
|
||||||
|
<Box sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<FileSearch size={14} color="#64748b" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Evidenz-Gate
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ ml: 'auto' }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={GATE_STATUS_LABELS[gate.status]}
|
||||||
|
sx={{ bgcolor: bg, color: fg, border: 'none', fontSize: '0.7rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
{gate.checks.map((check, i) => (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{check.passed
|
||||||
|
? <CheckCircle2 size={13} color="#15803d" />
|
||||||
|
: <XCircle size={13} color="#dc2626" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption">
|
||||||
|
{check.label}
|
||||||
|
{check.value && (
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
|
||||||
|
{check.value}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{gate.reason && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'text.secondary', mt: 1, display: 'block' }}
|
||||||
|
>
|
||||||
|
{gate.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{gate.nextAction && gate.status !== GateStatus.PASSED && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.5 }}>
|
||||||
|
<ChevronRight size={12} color="#1d4ed8" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#1d4ed8', display: 'block' }}>
|
||||||
|
{gate.nextAction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Chip } from '@mui/material'
|
||||||
|
import { CheckCircle2, MinusCircle } from 'lucide-react'
|
||||||
|
import type { MarketSignal } from '../../domain/marketSignal'
|
||||||
|
import { SignalProcessingStatus } from '../../domain/marketSignal'
|
||||||
|
import { SensitivityLevel } from '../../domain/enums'
|
||||||
|
|
||||||
|
interface FeedEligibilityBadgeProps {
|
||||||
|
signal: MarketSignal
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedEligibilityBadge({ signal }: FeedEligibilityBadgeProps) {
|
||||||
|
const ELIGIBLE_STATUSES: SignalProcessingStatus[] = [
|
||||||
|
SignalProcessingStatus.APPROVED_AS_SIGNAL,
|
||||||
|
SignalProcessingStatus.CONVERTED_TO_FUTURE_AVAILABILITY,
|
||||||
|
]
|
||||||
|
const BLOCKING_SENSITIVITY: SensitivityLevel[] = [
|
||||||
|
SensitivityLevel.CONFIDENTIAL,
|
||||||
|
SensitivityLevel.RESTRICTED,
|
||||||
|
]
|
||||||
|
|
||||||
|
const isEligible =
|
||||||
|
ELIGIBLE_STATUSES.includes(signal.processingStatus) &&
|
||||||
|
!BLOCKING_SENSITIVITY.includes(signal.sensitivityLevel)
|
||||||
|
|
||||||
|
if (isEligible) {
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label="Feed-fähig"
|
||||||
|
icon={<CheckCircle2 size={11} color="#15803d" />}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(22,163,74,0.1)',
|
||||||
|
color: '#15803d',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
'& .MuiChip-icon': { ml: 0.75 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label="Nicht Feed-fähig"
|
||||||
|
icon={<MinusCircle size={11} color="#64748b" />}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(148,163,184,0.1)',
|
||||||
|
color: '#64748b',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
'& .MuiChip-icon': { ml: 0.75 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { SensitivityWarningPanel } from './SensitivityWarningPanel'
|
|||||||
import { SignalEvidenceList } from './SignalEvidenceList'
|
import { SignalEvidenceList } from './SignalEvidenceList'
|
||||||
import { SignalConversionPanel } from './SignalConversionPanel'
|
import { SignalConversionPanel } from './SignalConversionPanel'
|
||||||
import { MarketSignalEmptyState } from './MarketSignalEmptyState'
|
import { MarketSignalEmptyState } from './MarketSignalEmptyState'
|
||||||
|
import { FeedEligibilityBadge } from './FeedEligibilityBadge'
|
||||||
import { useUpdateSignalStatus, useCreateReviewTask } from '../../hooks/useMarketSignals'
|
import { useUpdateSignalStatus, useCreateReviewTask } from '../../hooks/useMarketSignals'
|
||||||
|
|
||||||
const SOURCE_ICONS: Record<string, LucideIcon> = {
|
const SOURCE_ICONS: Record<string, LucideIcon> = {
|
||||||
@@ -117,6 +118,7 @@ export function MarketSignalDetailPanel({ signal }: MarketSignalDetailPanelProps
|
|||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FeedEligibilityBadge signal={signal} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Box, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||||
|
import { CheckCircle2, ChevronRight, Layers, XCircle } from 'lucide-react'
|
||||||
|
import type { GateEvaluation } from '../../domain/signalPipeline'
|
||||||
|
import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface MatchabilityGatePanelProps {
|
||||||
|
gate: GateEvaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MatchabilityGatePanel({ gate }: MatchabilityGatePanelProps) {
|
||||||
|
const { bg, fg } = GATE_STATUS_COLORS[gate.status]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ mb: 1.5, borderRadius: 1.5 }}>
|
||||||
|
<Box sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Layers size={14} color="#64748b" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Matchbarkeits-Gate
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ ml: 'auto' }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={GATE_STATUS_LABELS[gate.status]}
|
||||||
|
sx={{ bgcolor: bg, color: fg, border: 'none', fontSize: '0.7rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
{gate.checks.map((check, i) => (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{check.passed
|
||||||
|
? <CheckCircle2 size={13} color="#15803d" />
|
||||||
|
: <XCircle size={13} color="#dc2626" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption">
|
||||||
|
{check.label}
|
||||||
|
{check.value && (
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
|
||||||
|
{check.value}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{check.note && (
|
||||||
|
<Box component="span" sx={{ color: '#dc2626', ml: 0.5, fontStyle: 'italic' }}>
|
||||||
|
– {check.note}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{gate.reason && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'text.secondary', mt: 1, display: 'block' }}
|
||||||
|
>
|
||||||
|
{gate.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{gate.nextAction && gate.status !== GateStatus.PASSED && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.5 }}>
|
||||||
|
<ChevronRight size={12} color="#1d4ed8" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#1d4ed8', display: 'block' }}>
|
||||||
|
{gate.nextAction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Box, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||||
|
import { CheckCircle2, ChevronRight, ClipboardCheck, XCircle } from 'lucide-react'
|
||||||
|
import type { GateEvaluation } from '../../domain/signalPipeline'
|
||||||
|
import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface ReviewGatePanelProps {
|
||||||
|
gate: GateEvaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReviewGatePanel({ gate }: ReviewGatePanelProps) {
|
||||||
|
const { bg, fg } = GATE_STATUS_COLORS[gate.status]
|
||||||
|
|
||||||
|
const reviewerNotes = gate.checks.filter(
|
||||||
|
c => c.value && c.label.toLowerCase().includes('review')
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ mb: 1.5, borderRadius: 1.5 }}>
|
||||||
|
<Box sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<ClipboardCheck size={14} color="#64748b" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Review-Gate
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ ml: 'auto' }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={GATE_STATUS_LABELS[gate.status]}
|
||||||
|
sx={{ bgcolor: bg, color: fg, border: 'none', fontSize: '0.7rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
{gate.checks.map((check, i) => (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{check.passed
|
||||||
|
? <CheckCircle2 size={13} color="#15803d" />
|
||||||
|
: <XCircle size={13} color="#dc2626" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption">
|
||||||
|
{check.label}
|
||||||
|
{check.value && (
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
|
||||||
|
{check.value}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{reviewerNotes.length > 0 && (
|
||||||
|
<Box sx={{ mt: 1, p: 1, bgcolor: 'rgba(30,58,95,0.04)', borderRadius: 1, border: '1px solid rgba(30,58,95,0.1)' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e3a5f', display: 'block', mb: 0.5 }}>
|
||||||
|
Reviewer-Informationen
|
||||||
|
</Typography>
|
||||||
|
{reviewerNotes.map((note, i) => (
|
||||||
|
<Typography key={i} variant="caption" sx={{ color: 'text.secondary', display: 'block' }}>
|
||||||
|
{note.label}: {note.value}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gate.reason && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'text.secondary', mt: 1, display: 'block' }}
|
||||||
|
>
|
||||||
|
{gate.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{gate.nextAction && gate.status !== GateStatus.PASSED && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.5 }}>
|
||||||
|
<ChevronRight size={12} color="#1d4ed8" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#1d4ed8', display: 'block' }}>
|
||||||
|
{gate.nextAction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Alert, Box, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||||
|
import { CheckCircle2, ChevronRight, ShieldCheck, XCircle } from 'lucide-react'
|
||||||
|
import type { GateEvaluation } from '../../domain/signalPipeline'
|
||||||
|
import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface SensitivityGatePanelProps {
|
||||||
|
gate: GateEvaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SensitivityGatePanel({ gate }: SensitivityGatePanelProps) {
|
||||||
|
const { bg, fg } = GATE_STATUS_COLORS[gate.status]
|
||||||
|
|
||||||
|
const confidentialCheck = gate.checks.find(c => c.label === 'Nicht CONFIDENTIAL' && !c.passed)
|
||||||
|
const restrictedCheck = gate.checks.find(c => c.label === 'Nicht RESTRICTED' && !c.passed)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ mb: 1.5, borderRadius: 1.5 }}>
|
||||||
|
<Box sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<ShieldCheck size={14} color="#64748b" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Sensitivitäts-Gate
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ ml: 'auto' }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={GATE_STATUS_LABELS[gate.status]}
|
||||||
|
sx={{ bgcolor: bg, color: fg, border: 'none', fontSize: '0.7rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
{gate.checks.map((check, i) => (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{check.passed
|
||||||
|
? <CheckCircle2 size={13} color="#15803d" />
|
||||||
|
: <XCircle size={13} color="#dc2626" />
|
||||||
|
}
|
||||||
|
<Typography variant="caption">
|
||||||
|
{check.label}
|
||||||
|
{check.value && (
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
|
||||||
|
{check.value}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{gate.status === GateStatus.FAILED && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
{confidentialCheck && (
|
||||||
|
<Alert severity="error" sx={{ py: 0.5, fontSize: '0.75rem', '& .MuiAlert-message': { py: 0.5 } }}>
|
||||||
|
Sensitivitätsstufe CONFIDENTIAL: Dieses Signal darf nicht im Demand Feed erscheinen.
|
||||||
|
Mieter- oder Vertragsidentitäten sind schützenswert und nur intern zugänglich.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{restrictedCheck && (
|
||||||
|
<Alert severity="error" sx={{ py: 0.5, fontSize: '0.75rem', '& .MuiAlert-message': { py: 0.5 } }}>
|
||||||
|
Sensitivitätsstufe RESTRICTED: Zugriff auf dieses Signal ist rollenbasiert eingeschränkt.
|
||||||
|
Nur autorisierte Nutzer mit entsprechender Berechtigung dürfen es einsehen.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gate.reason && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'text.secondary', mt: 1, display: 'block' }}
|
||||||
|
>
|
||||||
|
{gate.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{gate.nextAction && gate.status !== GateStatus.PASSED && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.5 }}>
|
||||||
|
<ChevronRight size={12} color="#1d4ed8" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#1d4ed8', display: 'block' }}>
|
||||||
|
{gate.nextAction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Stepper, Step, StepLabel } from '@mui/material'
|
||||||
|
import type { PipelineState } from '../../domain/signalPipeline'
|
||||||
|
import { PIPELINE_STAGE_ORDER, PIPELINE_STAGE_LABELS } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface SignalPipelineStepperProps {
|
||||||
|
pipelineState: PipelineState
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignalPipelineStepper({ pipelineState }: SignalPipelineStepperProps) {
|
||||||
|
const currentStageIndex = PIPELINE_STAGE_ORDER.indexOf(pipelineState.currentStage)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stepper
|
||||||
|
activeStep={currentStageIndex}
|
||||||
|
alternativeLabel
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
'& .MuiStepIcon-root': { fontSize: '1.6rem', color: '#cbd5e1' },
|
||||||
|
'& .MuiStepIcon-root.Mui-active': { color: '#1e3a5f' },
|
||||||
|
'& .MuiStepIcon-root.Mui-completed': { color: '#15803d' },
|
||||||
|
'& .MuiStepLabel-label': { fontSize: '0.68rem', mt: 0.5, color: '#64748b' },
|
||||||
|
'& .MuiStepLabel-label.Mui-active': { color: '#1e3a5f', fontWeight: 700 },
|
||||||
|
'& .MuiStepLabel-label.Mui-completed': { color: '#15803d' },
|
||||||
|
'& .MuiStepConnector-line': { borderTopWidth: 2, borderColor: '#e2e8f0' },
|
||||||
|
'& .MuiStepConnector-root.Mui-completed .MuiStepConnector-line': { borderColor: '#15803d' },
|
||||||
|
'& .MuiStepConnector-root.Mui-active .MuiStepConnector-line': { borderColor: '#1e3a5f' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PIPELINE_STAGE_ORDER.map((stage) => (
|
||||||
|
<Step key={stage}>
|
||||||
|
<StepLabel>{PIPELINE_STAGE_LABELS[stage]}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { Alert, Box, Button, CircularProgress, Divider, Typography } from '@mui/material'
|
||||||
|
import { Rocket } from 'lucide-react'
|
||||||
|
import type { MarketSignal } from '../../domain/marketSignal'
|
||||||
|
import { GateStatus } from '../../domain/signalPipeline'
|
||||||
|
import {
|
||||||
|
useSignalPipelineState,
|
||||||
|
useSignalAuditTrail,
|
||||||
|
usePublishToFutureAvailability,
|
||||||
|
} from '../../hooks/useSignalPipeline'
|
||||||
|
import { FeedEligibilityBadge } from './FeedEligibilityBadge'
|
||||||
|
import { SignalPipelineStepper } from './SignalPipelineStepper'
|
||||||
|
import { EvidenceGatePanel } from './EvidenceGatePanel'
|
||||||
|
import { ConfidenceGatePanel } from './ConfidenceGatePanel'
|
||||||
|
import { SensitivityGatePanel } from './SensitivityGatePanel'
|
||||||
|
import { ReviewGatePanel } from './ReviewGatePanel'
|
||||||
|
import { MatchabilityGatePanel } from './MatchabilityGatePanel'
|
||||||
|
import { SignalToMatchAuditTrail } from './SignalToMatchAuditTrail'
|
||||||
|
|
||||||
|
interface SignalPipelineViewProps {
|
||||||
|
signal: MarketSignal
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignalPipelineView({ signal }: SignalPipelineViewProps) {
|
||||||
|
const { data: pipelineState, isLoading: isLoadingPipeline } = useSignalPipelineState(signal.id)
|
||||||
|
const { data: auditTrail = [], isLoading: isLoadingAudit } = useSignalAuditTrail(signal.id)
|
||||||
|
const { mutate: publish, isPending: isPublishing } = usePublishToFutureAvailability()
|
||||||
|
|
||||||
|
const canPublish =
|
||||||
|
pipelineState !== null &&
|
||||||
|
pipelineState !== undefined &&
|
||||||
|
pipelineState.gates.REVIEW_GATE.status === GateStatus.PASSED &&
|
||||||
|
!pipelineState.publishedToFutureAvailability
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: '100%', overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ p: 3, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3, mb: 1 }}>
|
||||||
|
{signal.title}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
|
||||||
|
<FeedEligibilityBadge signal={signal} />
|
||||||
|
{pipelineState?.feedDisclaimer && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: '#a16207',
|
||||||
|
bgcolor: 'rgba(234,179,8,0.08)',
|
||||||
|
px: 1,
|
||||||
|
py: 0.25,
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid rgba(234,179,8,0.2)',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pipelineState.feedDisclaimer}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Pipeline Stepper */}
|
||||||
|
<Box sx={{ p: 3, borderBottom: '1px solid #e2e8f0', flexShrink: 0, bgcolor: '#fafafa' }}>
|
||||||
|
{isLoadingPipeline && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{pipelineState && <SignalPipelineStepper pipelineState={pipelineState} />}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Gate panels */}
|
||||||
|
<Box sx={{ p: 3, flex: 1 }}>
|
||||||
|
{isLoadingPipeline && !pipelineState && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pipelineState && (
|
||||||
|
<>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1.5 }}>
|
||||||
|
Gate-Bewertungen
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<EvidenceGatePanel gate={pipelineState.gates.EVIDENCE_GATE} />
|
||||||
|
<ConfidenceGatePanel gate={pipelineState.gates.CONFIDENCE_GATE} />
|
||||||
|
<SensitivityGatePanel gate={pipelineState.gates.SENSITIVITY_GATE} />
|
||||||
|
<ReviewGatePanel gate={pipelineState.gates.REVIEW_GATE} />
|
||||||
|
<MatchabilityGatePanel gate={pipelineState.gates.MATCHABILITY_GATE} />
|
||||||
|
|
||||||
|
{/* Feed eligibility summary */}
|
||||||
|
{pipelineState.overallEligible && pipelineState.publishedToFutureAvailability && (
|
||||||
|
<Alert
|
||||||
|
severity="success"
|
||||||
|
sx={{ mb: 2, fontSize: '0.8rem' }}
|
||||||
|
>
|
||||||
|
Signal ist im Future Availability Feed publiziert
|
||||||
|
{pipelineState.publishedAt && (
|
||||||
|
<> · {new Date(pipelineState.publishedAt).toLocaleString('de-CH', {
|
||||||
|
day: '2-digit', month: '2-digit', year: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
})}</>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pipelineState.overallEligible && !pipelineState.publishedToFutureAvailability && (
|
||||||
|
<Alert severity="info" sx={{ mb: 2, fontSize: '0.8rem' }}>
|
||||||
|
Signal ist feed-fähig – noch nicht publiziert.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Publish button */}
|
||||||
|
{canPublish && (
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={isPublishing ? <CircularProgress size={14} color="inherit" /> : <Rocket size={14} />}
|
||||||
|
disabled={isPublishing}
|
||||||
|
onClick={() => publish(signal.id)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
bgcolor: '#1e3a5f',
|
||||||
|
'&:hover': { bgcolor: '#162d4a' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
In Future Availability publizieren
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1.5 }}>
|
||||||
|
Audit Trail
|
||||||
|
</Typography>
|
||||||
|
{isLoadingAudit ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<SignalToMatchAuditTrail entries={auditTrail} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Box, Typography } from '@mui/material'
|
||||||
|
import type { AuditTrailEntry } from '../../domain/signalPipeline'
|
||||||
|
import { PIPELINE_STAGE_ORDER } from '../../domain/signalPipeline'
|
||||||
|
|
||||||
|
interface SignalToMatchAuditTrailProps {
|
||||||
|
entries: AuditTrailEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStageColor(entry: AuditTrailEntry): string {
|
||||||
|
const idx = PIPELINE_STAGE_ORDER.indexOf(entry.stage)
|
||||||
|
if (idx <= 0) return '#94a3b8'
|
||||||
|
if (idx >= 5) return '#15803d'
|
||||||
|
if (idx >= 3) return '#1e3a5f'
|
||||||
|
if (idx >= 2) return '#4f46e5'
|
||||||
|
return '#2563eb'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(iso: string): string {
|
||||||
|
return new Date(iso).toLocaleString('de-CH', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignalToMatchAuditTrail({ entries }: SignalToMatchAuditTrailProps) {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
Keine Audit-Einträge vorhanden.
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{entries.map((entry, idx) => {
|
||||||
|
const isLast = idx === entries.length - 1
|
||||||
|
const dotColor = getStageColor(entry)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={entry.id} sx={{ display: 'flex', gap: 1.5, mb: 1.5 }}>
|
||||||
|
{/* Timeline column */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: dotColor,
|
||||||
|
mt: 0.25,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!isLast && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 2,
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 16,
|
||||||
|
bgcolor: 'rgba(148,163,184,0.3)',
|
||||||
|
mt: 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content column */}
|
||||||
|
<Box sx={{ flex: 1, pb: isLast ? 0 : 0.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', lineHeight: 1.4 }}>
|
||||||
|
{entry.action}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', lineHeight: 1.4 }}>
|
||||||
|
{entry.performedBy} · {formatTimestamp(entry.timestamp)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: '#64748b', display: 'block', lineHeight: 1.4, mt: 0.25 }}
|
||||||
|
>
|
||||||
|
{entry.details}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,3 +19,12 @@ export { SourceList } from './SourceList'
|
|||||||
export { ConnectorRunTable } from './ConnectorRunTable'
|
export { ConnectorRunTable } from './ConnectorRunTable'
|
||||||
export { ConnectorRunDetailDrawer } from './ConnectorRunDetailDrawer'
|
export { ConnectorRunDetailDrawer } from './ConnectorRunDetailDrawer'
|
||||||
export { SourceDetailPanel } from './SourceDetailPanel'
|
export { SourceDetailPanel } from './SourceDetailPanel'
|
||||||
|
export { FeedEligibilityBadge } from './FeedEligibilityBadge'
|
||||||
|
export { SignalPipelineStepper } from './SignalPipelineStepper'
|
||||||
|
export { EvidenceGatePanel } from './EvidenceGatePanel'
|
||||||
|
export { ConfidenceGatePanel } from './ConfidenceGatePanel'
|
||||||
|
export { SensitivityGatePanel } from './SensitivityGatePanel'
|
||||||
|
export { ReviewGatePanel } from './ReviewGatePanel'
|
||||||
|
export { MatchabilityGatePanel } from './MatchabilityGatePanel'
|
||||||
|
export { SignalToMatchAuditTrail } from './SignalToMatchAuditTrail'
|
||||||
|
export { SignalPipelineView } from './SignalPipelineView'
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// ── Pipeline Stages ───────────────────────────────────────────────────────────
|
||||||
|
export const PipelineStage = {
|
||||||
|
STAGE_1_RAW_EVIDENCE: 'STAGE_1_RAW_EVIDENCE',
|
||||||
|
STAGE_2_NORMALIZED: 'STAGE_2_NORMALIZED',
|
||||||
|
STAGE_3_ENRICHED: 'STAGE_3_ENRICHED',
|
||||||
|
STAGE_4_REVIEW_CANDIDATE: 'STAGE_4_REVIEW_CANDIDATE',
|
||||||
|
STAGE_5_APPROVED_FUTURE: 'STAGE_5_APPROVED_FUTURE',
|
||||||
|
STAGE_6_MATCHABLE_RESULT: 'STAGE_6_MATCHABLE_RESULT',
|
||||||
|
STAGE_7_STRATEGIC_INPUT: 'STAGE_7_STRATEGIC_INPUT',
|
||||||
|
} as const
|
||||||
|
export type PipelineStage = typeof PipelineStage[keyof typeof PipelineStage]
|
||||||
|
|
||||||
|
export const PIPELINE_STAGE_LABELS: Record<PipelineStage, string> = {
|
||||||
|
STAGE_1_RAW_EVIDENCE: 'Rohe Markt-Evidenz',
|
||||||
|
STAGE_2_NORMALIZED: 'Normalisiertes Signal',
|
||||||
|
STAGE_3_ENRICHED: 'Angereichertes Signal',
|
||||||
|
STAGE_4_REVIEW_CANDIDATE: 'Review-Kandidat',
|
||||||
|
STAGE_5_APPROVED_FUTURE: 'Genehmigtes Future Signal',
|
||||||
|
STAGE_6_MATCHABLE_RESULT: 'Matchbares Ergebnis',
|
||||||
|
STAGE_7_STRATEGIC_INPUT: 'Strategischer Entscheidungs-Input',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PIPELINE_STAGE_DESCRIPTIONS: Record<PipelineStage, string> = {
|
||||||
|
STAGE_1_RAW_EVIDENCE: 'Rohe Evidenz aus Quellen gesammelt – noch unverarbeitet',
|
||||||
|
STAGE_2_NORMALIZED: 'Daten normalisiert, Felder validiert und vereinheitlicht',
|
||||||
|
STAGE_3_ENRICHED: 'Entitäten extrahiert, Kontext angereichert und bewertet',
|
||||||
|
STAGE_4_REVIEW_CANDIDATE: 'Signal bereit für manuellen Analyst-Review',
|
||||||
|
STAGE_5_APPROVED_FUTURE: 'Genehmigt als Future Availability Signal – intern sichtbar',
|
||||||
|
STAGE_6_MATCHABLE_RESULT: 'Im Unified Result Feed – Match-Engine nutzbar',
|
||||||
|
STAGE_7_STRATEGIC_INPUT: 'Eingang in Decision Briefs und strategische Analyse',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PIPELINE_STAGE_ORDER: PipelineStage[] = [
|
||||||
|
'STAGE_1_RAW_EVIDENCE',
|
||||||
|
'STAGE_2_NORMALIZED',
|
||||||
|
'STAGE_3_ENRICHED',
|
||||||
|
'STAGE_4_REVIEW_CANDIDATE',
|
||||||
|
'STAGE_5_APPROVED_FUTURE',
|
||||||
|
'STAGE_6_MATCHABLE_RESULT',
|
||||||
|
'STAGE_7_STRATEGIC_INPUT',
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── Gate Types ────────────────────────────────────────────────────────────────
|
||||||
|
export const GateType = {
|
||||||
|
EVIDENCE_GATE: 'EVIDENCE_GATE',
|
||||||
|
CONFIDENCE_GATE: 'CONFIDENCE_GATE',
|
||||||
|
SENSITIVITY_GATE: 'SENSITIVITY_GATE',
|
||||||
|
REVIEW_GATE: 'REVIEW_GATE',
|
||||||
|
MATCHABILITY_GATE: 'MATCHABILITY_GATE',
|
||||||
|
FEED_ELIGIBILITY_GATE: 'FEED_ELIGIBILITY_GATE',
|
||||||
|
} as const
|
||||||
|
export type GateType = typeof GateType[keyof typeof GateType]
|
||||||
|
|
||||||
|
export const GATE_LABELS: Record<GateType, string> = {
|
||||||
|
EVIDENCE_GATE: 'Evidenz-Gate',
|
||||||
|
CONFIDENCE_GATE: 'Konfidenz-Gate',
|
||||||
|
SENSITIVITY_GATE: 'Sensitivitäts-Gate',
|
||||||
|
REVIEW_GATE: 'Review-Gate',
|
||||||
|
MATCHABILITY_GATE: 'Matchbarkeits-Gate',
|
||||||
|
FEED_ELIGIBILITY_GATE: 'Feed-Eignung',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gate Status ───────────────────────────────────────────────────────────────
|
||||||
|
export const GateStatus = {
|
||||||
|
PASSED: 'PASSED',
|
||||||
|
FAILED: 'FAILED',
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
BLOCKED: 'BLOCKED',
|
||||||
|
SKIPPED: 'SKIPPED',
|
||||||
|
} as const
|
||||||
|
export type GateStatus = typeof GateStatus[keyof typeof GateStatus]
|
||||||
|
|
||||||
|
export const GATE_STATUS_LABELS: Record<GateStatus, string> = {
|
||||||
|
PASSED: 'Bestanden',
|
||||||
|
FAILED: 'Fehlgeschlagen',
|
||||||
|
PENDING: 'Ausstehend',
|
||||||
|
BLOCKED: 'Blockiert',
|
||||||
|
SKIPPED: 'Übersprungen',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GATE_STATUS_COLORS: Record<GateStatus, { bg: string; fg: string }> = {
|
||||||
|
PASSED: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' },
|
||||||
|
FAILED: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' },
|
||||||
|
PENDING: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' },
|
||||||
|
BLOCKED: { bg: 'rgba(239,68,68,0.08)', fg: '#dc2626' },
|
||||||
|
SKIPPED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interfaces ────────────────────────────────────────────────────────────────
|
||||||
|
export interface GateCheck {
|
||||||
|
label: string
|
||||||
|
passed: boolean
|
||||||
|
value?: string
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GateEvaluation {
|
||||||
|
gateType: GateType
|
||||||
|
status: GateStatus
|
||||||
|
reason: string
|
||||||
|
nextAction?: string
|
||||||
|
evaluatedAt: string
|
||||||
|
checks: GateCheck[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineState {
|
||||||
|
signalId: string
|
||||||
|
currentStage: PipelineStage
|
||||||
|
gates: Record<GateType, GateEvaluation>
|
||||||
|
overallEligible: boolean
|
||||||
|
publishedToFutureAvailability: boolean
|
||||||
|
publishedAt?: string
|
||||||
|
feedDisclaimer?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditTrailEntry {
|
||||||
|
id: string
|
||||||
|
signalId: string
|
||||||
|
timestamp: string
|
||||||
|
stage: PipelineStage
|
||||||
|
action: string
|
||||||
|
performedBy: string
|
||||||
|
details: string
|
||||||
|
gateType?: GateType
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { signalPipelineService } from '../services/signalPipelineService'
|
||||||
|
import type { GateType } from '../domain/signalPipeline'
|
||||||
|
|
||||||
|
const STALE_PIPELINE = 15_000
|
||||||
|
|
||||||
|
export function useSignalPipelineState(signalId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['signal-pipeline', signalId],
|
||||||
|
queryFn: () => signalPipelineService.getPipelineState(signalId!),
|
||||||
|
enabled: signalId !== null,
|
||||||
|
staleTime: STALE_PIPELINE,
|
||||||
|
select: (res) => res.data ?? null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSignalAuditTrail(signalId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['signal-audit-trail', signalId],
|
||||||
|
queryFn: () => signalPipelineService.getAuditTrail(signalId!),
|
||||||
|
enabled: signalId !== null,
|
||||||
|
staleTime: STALE_PIPELINE,
|
||||||
|
select: (res) => res.data ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvaluateGate() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ signalId, gateType }: { signalId: string; gateType: GateType }) =>
|
||||||
|
signalPipelineService.evaluateGate(signalId, gateType),
|
||||||
|
onSuccess: (_data, { signalId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['signal-pipeline', signalId] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublishToFutureAvailability() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (signalId: string) => signalPipelineService.publishToFutureAvailability(signalId),
|
||||||
|
onSuccess: (_data, signalId) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['signal-pipeline', signalId] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['signal-audit-trail', signalId] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['market-signals'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,860 @@
|
|||||||
|
import type { PipelineState, GateEvaluation, AuditTrailEntry } from '../domain/signalPipeline'
|
||||||
|
import { PipelineStage, GateType, GateStatus } from '../domain/signalPipeline'
|
||||||
|
|
||||||
|
function gate(
|
||||||
|
gateType: GateType,
|
||||||
|
status: GateStatus,
|
||||||
|
reason: string,
|
||||||
|
checks: GateEvaluation['checks'],
|
||||||
|
nextAction?: string,
|
||||||
|
): GateEvaluation {
|
||||||
|
return { gateType, status, reason, checks, nextAction, evaluatedAt: '2026-05-15T06:00:00Z' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-001: STAGE_4_REVIEW_CANDIDATE ─────────────────────────────────────────
|
||||||
|
const sig001Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-001',
|
||||||
|
currentStage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Mindestens ein valides Evidenzstück vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Neue Zürcher Zeitung' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.72 liegt über dem Mindestschwellenwert von 0.60.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.72' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.72', note: 'Mittlere Konfidenz' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Sensitivitätsstufe INTERNAL ist für interne Feed-Nutzung zulässig.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'INTERNAL' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Signal wartet auf manuellen Analyst-Review.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: true },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Analyst-Review durchführen und Signal genehmigen oder ablehnen.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Matchbarkeit kann erst nach abgeschlossenem Review bewertet werden.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Zürich' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: false },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: true },
|
||||||
|
],
|
||||||
|
'Review abschliessen, dann Matchbarkeit neu bewerten.',
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Review-Gate noch ausstehend.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
'Review und Matchbarkeits-Prüfung abschliessen.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-002: STAGE_5_APPROVED_FUTURE ─────────────────────────────────────────
|
||||||
|
const sig002Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-002',
|
||||||
|
currentStage: PipelineStage.STAGE_5_APPROVED_FUTURE,
|
||||||
|
overallEligible: true,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Baugesuchdokument als verlässliche Primärquelle vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Baugesuchregister BS' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.88 überschreitet den hohen Schwellenwert von 0.75.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.88' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.88' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Öffentliche Quelle – keine Einschränkungen.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Signal durch Analyst M. Huber am 12.05.2026 genehmigt.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: true },
|
||||||
|
{ label: 'Review abgeschlossen', passed: true, value: 'M. Huber, 12.05.2026' },
|
||||||
|
{ label: 'Genehmigt', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Alle Pflichtfelder für Match-Engine vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE, LOGISTICS, RETAIL' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Basel' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: true, value: 'Q1 2026' },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Alle Gates bestanden – Signal ist feed-fähig.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: true },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: true },
|
||||||
|
{ label: 'Sensitivität zulässig', passed: true, value: 'PUBLIC' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-003: STAGE_3_ENRICHED ─────────────────────────────────────────────────
|
||||||
|
const sig003Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-003',
|
||||||
|
currentStage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'LinkedIn-Datenauszug als Evidenz vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'LinkedIn Hiring Data' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Konfidenzwert 0.61 liegt im Grenzbereich (0.60–0.65). Zusätzliche Validierung empfohlen.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.61', note: 'Grenzbereich' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.61' },
|
||||||
|
],
|
||||||
|
'Unternehmensidentität über HR-Netzwerke verifizieren, um Konfidenz zu erhöhen.',
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Öffentliche Quelle – keine Einschränkungen.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Signal noch nicht für Review eingereicht.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: false },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Signal für Analyst-Review einreichen nach Konfidenz-Verbesserung.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Matchbarkeit noch nicht bewertet – Anreicherung läuft.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Zug' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: false },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: false, note: 'Unternehmensname unbekannt' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Mehrere vorgelagerte Gates ausstehend.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-004: STAGE_4_REVIEW_CANDIDATE ─────────────────────────────────────────
|
||||||
|
const sig004Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-004',
|
||||||
|
currentStage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Mietvertragsdokument als primäre Evidenz vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Internes ERP' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.97 – sehr hohe Verlässlichkeit (interne Vertragsdaten).',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.97' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.97' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.FAILED,
|
||||||
|
'Sensitivitätsstufe CONFIDENTIAL: Signal darf nicht im Demand Feed erscheinen. Mieteridentität ist schützenswert.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: false, value: 'CONFIDENTIAL', note: 'Interne Vertragsdaten – nur für Property Manager' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
'Daten anonymisieren oder auf aggregierter Ebene veröffentlichen.',
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Review angefordert – Sensitivitäts-Gate muss zuerst adressiert werden.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: true },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Sensitivitätsproblem lösen, dann Review abschliessen.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Matchbarkeit ausstehend – zuerst Sensitivitätsproblem lösen.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Zürich-West' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: true, value: '03/2026' },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Sensitivitäts-Gate fehlgeschlagen.',
|
||||||
|
[
|
||||||
|
{ label: 'Sensitivitäts-Gate bestanden', passed: false, value: 'CONFIDENTIAL' },
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
'Anonymisierung der Mieterdaten durchführen.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-005: STAGE_2_NORMALIZED ───────────────────────────────────────────────
|
||||||
|
const sig005Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-005',
|
||||||
|
currentStage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Handelsregistermutation als verlässliche Primärquelle.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Zefix – Handelsregister Schweiz' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Konfidenzwert 0.63 – Sitzverlegung bestätigt, aber Flächenbedarf am Zielort noch nicht validiert.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.63', note: 'Grenzbereich' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.63' },
|
||||||
|
],
|
||||||
|
'Flächenbedarf in Kloten durch Direktkontakt oder weitere Quellenrecherche bestätigen.',
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Öffentliches Handelsregister – keine Sensitivitätsbedenken.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Signal noch in Normalisierungsphase – kein Review angefordert.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: false },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Anreicherung abschliessen, dann für Review einreichen.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Anreicherung noch nicht abgeschlossen.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'LOGISTICS' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Kloten' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: false },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: false },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Signal befindet sich noch in der Normalisierungsphase.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-006: STAGE_1_RAW_EVIDENCE ────────────────────────────────────────────
|
||||||
|
const sig006Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-006',
|
||||||
|
currentStage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Inserat auf Immoscout24 als Primärquelle vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Immoscout24' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.83 – direkte Nachfrage-Ausschreibung mit hoher Relevanz.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.83' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.83' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Öffentliche Plattform – keine Einschränkungen.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Signal soeben erkannt – Normalisierung und Anreicherung noch ausstehend.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: false },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Signal normalisieren und anreichern, dann für Review einreichen.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Matchbarkeit noch nicht bewertet – Signal in früher Phase.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'LOGISTICS' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Winterthur' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: true, value: 'September 2025' },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: false },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Signal in Rohphase – mehrere Stufen offen.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-007: STAGE_3_ENRICHED ─────────────────────────────────────────────────
|
||||||
|
const sig007Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-007',
|
||||||
|
currentStage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
overallEligible: false,
|
||||||
|
publishedToFutureAvailability: false,
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'SBB-Medienmitteilung als verlässliche behördliche Quelle.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'SBB Medienmitteilung' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.79 – behördliche Bestätigung vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.79' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.79' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Öffentliche SBB-Mitteilung – keine Sensitivitätsbedenken.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PENDING,
|
||||||
|
'Matchbarkeits-Problem muss zuerst gelöst werden.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: false },
|
||||||
|
{ label: 'Review abgeschlossen', passed: false },
|
||||||
|
],
|
||||||
|
'Matchbarkeits-Frage klären (kein direktes Verfügbarkeitssignal), dann Review starten.',
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.FAILED,
|
||||||
|
'Signal Typ PROJECT_DEVELOPMENT ohne direktes Verfügbarkeitssignal – kein konkreter Flächenbedarf erkennbar.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE, PRODUCTION' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Schlieren' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: true, value: 'Dezember 2027' },
|
||||||
|
{ label: 'Direktes Verfügbarkeitssignal', passed: false, note: 'Infrastrukturverbesserung ohne konkreten Flächenbedarf' },
|
||||||
|
],
|
||||||
|
'Signal als strategischen Kontext-Input klassifizieren oder konkreten Bedarf nachweisen.',
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.BLOCKED,
|
||||||
|
'Feed-Eignung blockiert: Matchbarkeits-Gate fehlgeschlagen.',
|
||||||
|
[
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: false },
|
||||||
|
{ label: 'Review-Gate bestanden', passed: false },
|
||||||
|
],
|
||||||
|
'Signal als strategischen Input behandeln oder Matchbarkeit nachweisen.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── sig-008: STAGE_6_MATCHABLE_RESULT ────────────────────────────────────────
|
||||||
|
const sig008Pipeline: PipelineState = {
|
||||||
|
signalId: 'sig-008',
|
||||||
|
currentStage: PipelineStage.STAGE_6_MATCHABLE_RESULT,
|
||||||
|
overallEligible: true,
|
||||||
|
publishedToFutureAvailability: true,
|
||||||
|
publishedAt: '2026-05-13T11:00:00Z',
|
||||||
|
feedDisclaimer: 'Nur in aggregierter Form – Mieteridentität anonymisiert. Kein Rückschluss auf Vertragsparteien möglich.',
|
||||||
|
gates: {
|
||||||
|
EVIDENCE_GATE: gate(
|
||||||
|
GateType.EVIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Eigene Marktbeobachtung mit Vor-Ort-Begehung dokumentiert.',
|
||||||
|
[
|
||||||
|
{ label: 'Evidenz vorhanden', passed: true, value: '1 Stück' },
|
||||||
|
{ label: 'Quelle angegeben', passed: true, value: 'Eigene Beobachtung – Marktanalyse' },
|
||||||
|
{ label: 'Inhalt nicht leer', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CONFIDENCE_GATE: gate(
|
||||||
|
GateType.CONFIDENCE_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Konfidenzwert 0.68 – ausreichend für interne Verarbeitung.',
|
||||||
|
[
|
||||||
|
{ label: 'Konfidenz ≥ 0.60', passed: true, value: '0.68' },
|
||||||
|
{ label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.68', note: 'Mittlere Konfidenz akzeptiert' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SENSITIVITY_GATE: gate(
|
||||||
|
GateType.SENSITIVITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Sensitivitätsstufe INTERNAL mit Anonymisierungsauflage zulässig.',
|
||||||
|
[
|
||||||
|
{ label: 'Nicht CONFIDENTIAL', passed: true, value: 'INTERNAL' },
|
||||||
|
{ label: 'Nicht RESTRICTED', passed: true },
|
||||||
|
{ label: 'Anonymisierungsauflage gesetzt', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
REVIEW_GATE: gate(
|
||||||
|
GateType.REVIEW_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Signal durch Analyst A. Müller am 13.05.2026 genehmigt und zur Publikation freigegeben.',
|
||||||
|
[
|
||||||
|
{ label: 'Review angefordert', passed: true },
|
||||||
|
{ label: 'Review abgeschlossen', passed: true, value: 'A. Müller, 13.05.2026' },
|
||||||
|
{ label: 'Genehmigt', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MATCHABILITY_GATE: gate(
|
||||||
|
GateType.MATCHABILITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Alle notwendigen Felder für Match-Engine vorhanden.',
|
||||||
|
[
|
||||||
|
{ label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' },
|
||||||
|
{ label: 'Standort definiert', passed: true, value: 'Lausanne' },
|
||||||
|
{ label: 'Zeithorizont definiert', passed: true, value: 'Sofort verfügbar (3+ Monate leer)' },
|
||||||
|
{ label: 'Geschäftsrelevanz vorhanden', passed: true },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FEED_ELIGIBILITY_GATE: gate(
|
||||||
|
GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
GateStatus.PASSED,
|
||||||
|
'Signal im Future Availability Feed publiziert – anonymisiert und aggregiert.',
|
||||||
|
[
|
||||||
|
{ label: 'Review-Gate bestanden', passed: true },
|
||||||
|
{ label: 'Matchbarkeits-Gate bestanden', passed: true },
|
||||||
|
{ label: 'Anonymisierungsauflage umgesetzt', passed: true },
|
||||||
|
{ label: 'Im Feed publiziert', passed: true, value: '13.05.2026' },
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_PIPELINE_STATES: PipelineState[] = [
|
||||||
|
sig001Pipeline,
|
||||||
|
sig002Pipeline,
|
||||||
|
sig003Pipeline,
|
||||||
|
sig004Pipeline,
|
||||||
|
sig005Pipeline,
|
||||||
|
sig006Pipeline,
|
||||||
|
sig007Pipeline,
|
||||||
|
sig008Pipeline,
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── Audit Trails ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_AUDIT_TRAILS: AuditTrailEntry[] = [
|
||||||
|
// sig-001
|
||||||
|
{
|
||||||
|
id: 'audit-001-1',
|
||||||
|
signalId: 'sig-001',
|
||||||
|
timestamp: '2025-05-10T09:23:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / NZZ-Connector',
|
||||||
|
details: 'Signal automatisch aus NZZ-Medienbericht extrahiert. Rohtext gespeichert.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-001-2',
|
||||||
|
signalId: 'sig-001',
|
||||||
|
timestamp: '2025-05-10T10:05:00Z',
|
||||||
|
stage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
action: 'Normalisierung abgeschlossen',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Felder normalisiert: Standort "Zürich ZH", Asset-Typ OFFICE, Signaltyp EXPANSION.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-001-3',
|
||||||
|
signalId: 'sig-001',
|
||||||
|
timestamp: '2025-05-11T08:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Anreicherung abgeschlossen',
|
||||||
|
performedBy: 'System / Enrichment-Engine',
|
||||||
|
details: 'Entitäten extrahiert: UBS AG (Konfidenz 0.98), Zürich (0.95), Ende 2025 (0.87). Konfidenzwert: 0.72.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-001-4',
|
||||||
|
signalId: 'sig-001',
|
||||||
|
timestamp: '2025-05-11T08:05:00Z',
|
||||||
|
stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
action: 'Zur Prüfung eingereicht',
|
||||||
|
performedBy: 'System / Gate-Evaluator',
|
||||||
|
details: 'Evidenz-Gate, Konfidenz-Gate und Sensitivitäts-Gate bestanden. Review-Gate offen – wartet auf Analyst.',
|
||||||
|
gateType: GateType.REVIEW_GATE,
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-002
|
||||||
|
{
|
||||||
|
id: 'audit-002-1',
|
||||||
|
signalId: 'sig-002',
|
||||||
|
timestamp: '2025-05-08T14:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / Baugesuch-Connector',
|
||||||
|
details: 'Baugesuch Nr. BS-2025-0312 aus öffentlichem Register extrahiert.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-002-2',
|
||||||
|
signalId: 'sig-002',
|
||||||
|
timestamp: '2025-05-08T15:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Normalisierung und Anreicherung',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Alle Felder normalisiert und angereichert. Standort Basel-Nord, Asset-Typen: OFFICE, LOGISTICS, RETAIL. Konfidenz: 0.88.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-002-3',
|
||||||
|
signalId: 'sig-002',
|
||||||
|
timestamp: '2025-05-12T09:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
action: 'Analyst-Review gestartet',
|
||||||
|
performedBy: 'M. Huber',
|
||||||
|
details: 'Review-Kandidat angenommen. Öffentliche Quelle mit hoher Verlässlichkeit.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-002-4',
|
||||||
|
signalId: 'sig-002',
|
||||||
|
timestamp: '2025-05-12T09:45:00Z',
|
||||||
|
stage: PipelineStage.STAGE_5_APPROVED_FUTURE,
|
||||||
|
action: 'Signal genehmigt',
|
||||||
|
performedBy: 'M. Huber',
|
||||||
|
details: 'Signal als Future Availability genehmigt. Alle Gates bestanden. Bereit zur Feed-Publikation.',
|
||||||
|
gateType: GateType.REVIEW_GATE,
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-003
|
||||||
|
{
|
||||||
|
id: 'audit-003-1',
|
||||||
|
signalId: 'sig-003',
|
||||||
|
timestamp: '2025-05-09T11:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / LinkedIn-Connector',
|
||||||
|
details: 'Stellenwachstumssignal aus LinkedIn-Daten extrahiert. 80 neue Stellenausschreibungen in 6 Monaten.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-003-2',
|
||||||
|
signalId: 'sig-003',
|
||||||
|
timestamp: '2025-05-09T12:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
action: 'Normalisierung abgeschlossen',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Standort Zug, Asset-Typ OFFICE, Signaltyp EXPANSION normalisiert. Unternehmensname nicht öffentlich.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-003-3',
|
||||||
|
signalId: 'sig-003',
|
||||||
|
timestamp: '2025-05-11T14:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Anreicherung abgeschlossen',
|
||||||
|
performedBy: 'System / Enrichment-Engine',
|
||||||
|
details: 'Konfidenzwert 0.61 – Grenzbereich. Unternehmensverifizierung empfohlen. Weiterer Review nötig.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-004
|
||||||
|
{
|
||||||
|
id: 'audit-004-1',
|
||||||
|
signalId: 'sig-004',
|
||||||
|
timestamp: '2025-05-05T08:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / ERP-Connector',
|
||||||
|
details: 'Vertragslaufdaten aus internem ERP importiert. Vertrag V-2019-0481, Ablauf 31.03.2026.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-004-2',
|
||||||
|
signalId: 'sig-004',
|
||||||
|
timestamp: '2025-05-05T08:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Normalisierung und Anreicherung',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Alle Felder normalisiert. Hohe Konfidenz (0.97) da Primärquelle. Sensitivität CONFIDENTIAL erkannt.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-004-3',
|
||||||
|
signalId: 'sig-004',
|
||||||
|
timestamp: '2025-05-10T10:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
action: 'Sensitivitäts-Warnung gesetzt',
|
||||||
|
performedBy: 'System / Gate-Evaluator',
|
||||||
|
details: 'Sensitivitäts-Gate fehlgeschlagen: CONFIDENTIAL. Signal zur Review eingereicht mit Anonymisierungsauflage.',
|
||||||
|
gateType: GateType.SENSITIVITY_GATE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-004-4',
|
||||||
|
signalId: 'sig-004',
|
||||||
|
timestamp: '2025-05-10T10:05:00Z',
|
||||||
|
stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
action: 'Property Manager benachrichtigt',
|
||||||
|
performedBy: 'System / Notification-Service',
|
||||||
|
details: 'Property Manager über auslaufenden Grossmietvertrag informiert. Verlängerungsgespräch empfohlen.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-005
|
||||||
|
{
|
||||||
|
id: 'audit-005-1',
|
||||||
|
signalId: 'sig-005',
|
||||||
|
timestamp: '2025-05-07T16:20:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / Handelsregister-Connector',
|
||||||
|
details: 'Sitzverlegung aus Zefix extrahiert: 3014 Bern → 8302 Kloten, eingetragen 07.05.2025.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-005-2',
|
||||||
|
signalId: 'sig-005',
|
||||||
|
timestamp: '2025-05-07T17:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
action: 'Normalisierung abgeschlossen',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Standort Kloten (Zielort), Asset-Typ LOGISTICS normalisiert. Flächenbedarf noch nicht validiert.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-005-3',
|
||||||
|
signalId: 'sig-005',
|
||||||
|
timestamp: '2025-05-08T09:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
action: 'Anreicherung gestartet',
|
||||||
|
performedBy: 'System / Enrichment-Engine',
|
||||||
|
details: 'Konfidenzwert 0.63 – Sitzverlegung bestätigt, konkreter Flächenbedarf am Zielort ausstehend.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-006
|
||||||
|
{
|
||||||
|
id: 'audit-006-1',
|
||||||
|
signalId: 'sig-006',
|
||||||
|
timestamp: '2025-05-12T08:15:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / Immoscout-Connector',
|
||||||
|
details: 'Neue Suchanzeige auf Immoscout24 erkannt: 800 m² Lagerfläche Winterthur, sofort gesucht.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-006-2',
|
||||||
|
signalId: 'sig-006',
|
||||||
|
timestamp: '2025-05-12T08:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Evidenz-Gate bestanden',
|
||||||
|
performedBy: 'System / Gate-Evaluator',
|
||||||
|
details: 'Evidenz-Gate und Konfidenz-Gate (0.83) bestanden. Signal in Pipeline aufgenommen, Normalisierung startet.',
|
||||||
|
gateType: GateType.EVIDENCE_GATE,
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-007
|
||||||
|
{
|
||||||
|
id: 'audit-007-1',
|
||||||
|
signalId: 'sig-007',
|
||||||
|
timestamp: '2025-05-06T10:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal erkannt',
|
||||||
|
performedBy: 'System / SBB-Feed-Connector',
|
||||||
|
details: 'SBB-Medienmitteilung zur neuen Haltestelle Schlieren-West 2027 automatisch verarbeitet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-007-2',
|
||||||
|
signalId: 'sig-007',
|
||||||
|
timestamp: '2025-05-06T11:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_2_NORMALIZED,
|
||||||
|
action: 'Normalisierung abgeschlossen',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Standort Schlieren, Asset-Typen OFFICE und PRODUCTION, Zeithorizont Dezember 2027 normalisiert.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-007-3',
|
||||||
|
signalId: 'sig-007',
|
||||||
|
timestamp: '2025-05-10T15:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Matchbarkeits-Problem identifiziert',
|
||||||
|
performedBy: 'System / Gate-Evaluator',
|
||||||
|
details: 'Matchbarkeits-Gate fehlgeschlagen: PROJECT_DEVELOPMENT ohne direktes Verfügbarkeitssignal. Kein konkreter Flächenbedarf nachweisbar.',
|
||||||
|
gateType: GateType.MATCHABILITY_GATE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-007-4',
|
||||||
|
signalId: 'sig-007',
|
||||||
|
timestamp: '2025-05-10T15:05:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Analyst-Notiz hinzugefügt',
|
||||||
|
performedBy: 'K. Bauer',
|
||||||
|
details: 'Infrastrukturverbesserung dokumentiert. Empfehlung: Als strategischen Kontext-Input behandeln, nicht als direktes Matchingobjekt.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// sig-008
|
||||||
|
{
|
||||||
|
id: 'audit-008-1',
|
||||||
|
signalId: 'sig-008',
|
||||||
|
timestamp: '2025-05-03T14:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_1_RAW_EVIDENCE,
|
||||||
|
action: 'Signal manuell erfasst',
|
||||||
|
performedBy: 'A. Müller',
|
||||||
|
details: 'Vor-Ort-Begehung Tour de Berne, Lausanne. 4. OG leer stehend, keine Vermarktung erkennbar.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-008-2',
|
||||||
|
signalId: 'sig-008',
|
||||||
|
timestamp: '2025-05-03T15:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_3_ENRICHED,
|
||||||
|
action: 'Normalisierung und Anreicherung',
|
||||||
|
performedBy: 'System / NLP-Pipeline',
|
||||||
|
details: 'Standort Lausanne, Asset-Typ OFFICE, Signaltyp POSSIBLE_MOVE_OUT normalisiert. Konfidenz 0.68.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-008-3',
|
||||||
|
signalId: 'sig-008',
|
||||||
|
timestamp: '2025-05-13T10:30:00Z',
|
||||||
|
stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE,
|
||||||
|
action: 'Analyst-Review abgeschlossen',
|
||||||
|
performedBy: 'A. Müller',
|
||||||
|
details: 'Signal geprüft und genehmigt. Anonymisierungsauflage gesetzt: Mieteridentität darf nicht kommuniziert werden.',
|
||||||
|
gateType: GateType.REVIEW_GATE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-008-4',
|
||||||
|
signalId: 'sig-008',
|
||||||
|
timestamp: '2025-05-13T11:00:00Z',
|
||||||
|
stage: PipelineStage.STAGE_6_MATCHABLE_RESULT,
|
||||||
|
action: 'In Future Availability publiziert',
|
||||||
|
performedBy: 'A. Müller',
|
||||||
|
details: 'Signal als Future Availability "fs-lausanne-001" in den Feed übertragen. Anonymisierter Disclaimer aktiv.',
|
||||||
|
gateType: GateType.FEED_ELIGIBILITY_GATE,
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import { PageHeader } from '../../components/layout'
|
||||||
|
import { SignalInbox, SignalPipelineView, MarketSignalEmptyState } from '../../components/ops'
|
||||||
|
import { useMarketSignals } from '../../hooks/useMarketSignals'
|
||||||
|
import type { MarketSignalFilters } from '../../domain/marketSignal'
|
||||||
|
|
||||||
|
export default function SignalPipeline() {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const [filters, setFilters] = useState<MarketSignalFilters>({})
|
||||||
|
const { data: signals = [], isLoading } = useMarketSignals(filters)
|
||||||
|
const selectedSignal = signals.find((s) => s.id === selectedId) ?? null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
|
<PageHeader
|
||||||
|
title="Signal Pipeline"
|
||||||
|
subtitle="Von der Markt-Evidenz zum strategischen Entscheidungs-Input"
|
||||||
|
/>
|
||||||
|
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 360,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRight: '1px solid #e2e8f0',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SignalInbox
|
||||||
|
signals={signals}
|
||||||
|
isLoading={isLoading}
|
||||||
|
selectedId={selectedId}
|
||||||
|
onSelect={setSelectedId}
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
||||||
|
{selectedSignal
|
||||||
|
? <SignalPipelineView signal={selectedSignal} />
|
||||||
|
: <MarketSignalEmptyState variant="no-selection" />
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { PipelineState, AuditTrailEntry, GateType } from '../domain/signalPipeline'
|
||||||
|
|
||||||
|
export interface ISignalPipelineProvider {
|
||||||
|
getPipelineState(signalId: string): Promise<PipelineState | null>
|
||||||
|
evaluateGate(signalId: string, gateType: GateType): Promise<PipelineState>
|
||||||
|
getAuditTrail(signalId: string): Promise<AuditTrailEntry[]>
|
||||||
|
publishToFutureAvailability(signalId: string): Promise<PipelineState>
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { ISignalPipelineProvider } from './ISignalPipelineProvider'
|
||||||
|
import type { PipelineState } from '../domain/signalPipeline'
|
||||||
|
import { GateStatus, PipelineStage } from '../domain/signalPipeline'
|
||||||
|
import type { GateType } from '../domain/signalPipeline'
|
||||||
|
import { MOCK_PIPELINE_STATES, MOCK_AUDIT_TRAILS } from '../mock-data/signalPipelines'
|
||||||
|
|
||||||
|
let states: PipelineState[] = [...MOCK_PIPELINE_STATES]
|
||||||
|
|
||||||
|
export const MockupSignalPipelineProvider: ISignalPipelineProvider = {
|
||||||
|
async getPipelineState(signalId) {
|
||||||
|
return states.find(s => s.signalId === signalId) ?? null
|
||||||
|
},
|
||||||
|
async evaluateGate(signalId, gateType) {
|
||||||
|
const idx = states.findIndex(s => s.signalId === signalId)
|
||||||
|
if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`)
|
||||||
|
// Demo: mark gate as re-evaluated (no real logic change)
|
||||||
|
const updated: PipelineState = {
|
||||||
|
...states[idx],
|
||||||
|
gates: {
|
||||||
|
...states[idx].gates,
|
||||||
|
[gateType]: {
|
||||||
|
...states[idx].gates[gateType as GateType],
|
||||||
|
evaluatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
states[idx] = updated
|
||||||
|
return updated
|
||||||
|
},
|
||||||
|
async getAuditTrail(signalId) {
|
||||||
|
return MOCK_AUDIT_TRAILS.filter(e => e.signalId === signalId)
|
||||||
|
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
||||||
|
},
|
||||||
|
async publishToFutureAvailability(signalId) {
|
||||||
|
const idx = states.findIndex(s => s.signalId === signalId)
|
||||||
|
if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`)
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
states[idx] = {
|
||||||
|
...states[idx],
|
||||||
|
publishedToFutureAvailability: true,
|
||||||
|
publishedAt: now,
|
||||||
|
currentStage: PipelineStage.STAGE_6_MATCHABLE_RESULT,
|
||||||
|
gates: {
|
||||||
|
...states[idx].gates,
|
||||||
|
FEED_ELIGIBILITY_GATE: {
|
||||||
|
...states[idx].gates.FEED_ELIGIBILITY_GATE,
|
||||||
|
status: GateStatus.PASSED,
|
||||||
|
reason: 'Signal manuell in Future Availability publiziert.',
|
||||||
|
evaluatedAt: now,
|
||||||
|
checks: [{ label: 'Manuell publiziert', passed: true, value: 'Ja' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overallEligible: true,
|
||||||
|
}
|
||||||
|
return states[idx]
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { MockupSignalPipelineProvider } from '../provider/MockupSignalPipelineProvider'
|
||||||
|
import type { PipelineState, AuditTrailEntry, GateType } from '../domain/signalPipeline'
|
||||||
|
import type { ItemResponse, ListResponse } from './types'
|
||||||
|
|
||||||
|
const provider = MockupSignalPipelineProvider
|
||||||
|
|
||||||
|
export const signalPipelineService = {
|
||||||
|
async getPipelineState(signalId: string): Promise<ItemResponse<PipelineState | null>> {
|
||||||
|
const data = await provider.getPipelineState(signalId)
|
||||||
|
return { data }
|
||||||
|
},
|
||||||
|
async evaluateGate(signalId: string, gateType: GateType): Promise<ItemResponse<PipelineState>> {
|
||||||
|
const data = await provider.evaluateGate(signalId, gateType)
|
||||||
|
return { data }
|
||||||
|
},
|
||||||
|
async getAuditTrail(signalId: string): Promise<ListResponse<AuditTrailEntry>> {
|
||||||
|
const data = await provider.getAuditTrail(signalId)
|
||||||
|
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||||||
|
},
|
||||||
|
async publishToFutureAvailability(signalId: string): Promise<ItemResponse<PipelineState>> {
|
||||||
|
const data = await provider.publishToFutureAvailability(signalId)
|
||||||
|
return { data }
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user