d15a13e485
- Delete all ops page components (ReviewQueue, AIMonitoring, Governance, SourceMonitoring, ActivityTimeline, SignalPipeline) - Remove OPERATIONS workspace from AppShell config, nav order, path detection - Remove all /ops/* routes from App.tsx - Remove WorkspaceType.OPERATIONS from allowedWorkspaces in authService, sessionStore, permissions - Keep MarketIntelligence page (already moved to /supply/market-intelligence) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
6.6 KiB
TypeScript
156 lines
6.6 KiB
TypeScript
import { Box, Card, Typography, Alert, Stack, Chip } from '@mui/material'
|
||
import type { ParseNeedResult, ParsedNeedCriteria } from '../../domain/needBuilder'
|
||
import { ExtractedFieldRow } from './ExtractedFieldRow'
|
||
|
||
interface Props {
|
||
result: ParseNeedResult
|
||
criteria: ParsedNeedCriteria
|
||
onCriteriaChange: (c: ParsedNeedCriteria) => void
|
||
}
|
||
|
||
const ASSET_LABELS: Record<string, string> = {
|
||
OFFICE: 'Büro', LOGISTICS: 'Logistik / Lager', RETAIL: 'Retail',
|
||
PRODUCTION: 'Produktion', GASTRO: 'Gastro / F&B', LIGHT_INDUSTRIAL: 'Light Industrial',
|
||
MIXED: 'Gemischt', UNKNOWN: 'Unbekannt',
|
||
}
|
||
|
||
// ── Parse helpers ──────────────────────────────────────────────────────────────
|
||
|
||
function parseAreaRange(s: string): ParsedNeedCriteria['areaRange'] {
|
||
const m = s.match(/(\d+)\s*[–\-]\s*(\d+)/)
|
||
if (m) return { min: parseInt(m[1]), max: parseInt(m[2]) }
|
||
const n = s.match(/(\d+)/)
|
||
if (n) { const v = parseInt(n[1]); return { min: Math.round(v * 0.8), max: Math.round(v * 1.2) } }
|
||
return undefined
|
||
}
|
||
|
||
function parseBudget(s: string): ParsedNeedCriteria['budgetRange'] {
|
||
const n = s.match(/(\d+)/)
|
||
if (!n) return undefined
|
||
return { maxPerSqm: parseInt(n[1]), currency: 'CHF' }
|
||
}
|
||
|
||
function parseList(s: string): string[] {
|
||
return s.split(',').map(x => x.trim()).filter(Boolean)
|
||
}
|
||
|
||
function displayAreaRange(v: ParsedNeedCriteria['areaRange']): string {
|
||
return v ? `${v.min}–${v.max} m²` : ''
|
||
}
|
||
function displayBudget(v: ParsedNeedCriteria['budgetRange']): string {
|
||
return v ? `CHF ${v.maxPerSqm}/m²` : ''
|
||
}
|
||
function displayTiming(v: ParsedNeedCriteria['timing']): string {
|
||
if (!v) return ''
|
||
return `ab ${v.earliestMoveIn}${v.flexibleTiming ? ' (flexibel)' : ''}`
|
||
}
|
||
|
||
// ── Section wrapper ────────────────────────────────────────────────────────────
|
||
|
||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||
return (
|
||
<Box sx={{ mb: 2 }}>
|
||
<Typography variant="overline" sx={{ fontWeight: 700, color: '#64748b', fontSize: 10, letterSpacing: 1 }}>
|
||
{title}
|
||
</Typography>
|
||
{children}
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
// ── Main component ─────────────────────────────────────────────────────────────
|
||
|
||
export function CriteriaReviewPanel({ result, criteria: c, onCriteriaChange: set }: Props) {
|
||
const { confidenceByField: conf, missingFields, assumptions } = result
|
||
|
||
return (
|
||
<Card sx={{ p: 3, height: '100%', overflowY: 'auto' }}>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||
Extrahierte Kriterien
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
|
||
{result.rawSummary}
|
||
</Typography>
|
||
|
||
{/* ── Hard Facts ──────────────────────────────────────────────────── */}
|
||
<Section title="Hard Facts">
|
||
<ExtractedFieldRow
|
||
label="Nutzungstyp"
|
||
value={c.assetType ? (ASSET_LABELS[c.assetType] ?? c.assetType) : ''}
|
||
confidence={conf.assetType ?? 0.2}
|
||
missing={!c.assetType}
|
||
onEdit={v => set({ ...c, assetType: (v.toUpperCase() as ParsedNeedCriteria['assetType']) })}
|
||
/>
|
||
<ExtractedFieldRow
|
||
label="Flächenbedarf (z.B. 500–800)"
|
||
value={displayAreaRange(c.areaRange)}
|
||
confidence={conf.areaRange ?? 0.2}
|
||
missing={!c.areaRange}
|
||
onEdit={v => set({ ...c, areaRange: parseAreaRange(v) })}
|
||
/>
|
||
<ExtractedFieldRow
|
||
label="Standort (Komma-getrennt)"
|
||
value={c.preferredLocations?.join(', ') ?? ''}
|
||
confidence={conf.preferredLocations ?? 0.15}
|
||
missing={!c.preferredLocations?.length}
|
||
onEdit={v => set({ ...c, preferredLocations: parseList(v) })}
|
||
/>
|
||
<ExtractedFieldRow
|
||
label="Budget (max CHF/m²)"
|
||
value={displayBudget(c.budgetRange)}
|
||
confidence={conf.budgetRange ?? 0.2}
|
||
missing={!c.budgetRange}
|
||
onEdit={v => set({ ...c, budgetRange: parseBudget(v) })}
|
||
/>
|
||
<ExtractedFieldRow
|
||
label="Verfügbarkeit (ab Datum)"
|
||
value={displayTiming(c.timing)}
|
||
confidence={conf.timing ?? 0.2}
|
||
missing={!c.timing}
|
||
onEdit={v => set({ ...c, timing: { earliestMoveIn: v, latestMoveIn: v, flexibleTiming: v.toLowerCase().includes('flex') } })}
|
||
/>
|
||
</Section>
|
||
|
||
{/* ── Must-haves ──────────────────────────────────────────────────── */}
|
||
<Section title="Must-haves">
|
||
<ExtractedFieldRow
|
||
label="Pflichtkriterien (Komma-getrennt)"
|
||
value={c.mustHaveCriteria?.join(', ') ?? ''}
|
||
missing={!c.mustHaveCriteria?.length}
|
||
onEdit={v => set({ ...c, mustHaveCriteria: parseList(v) })}
|
||
/>
|
||
<ExtractedFieldRow
|
||
label="Parkplatzbedarf"
|
||
value={c.parkingNeed === true ? 'Ja' : c.parkingNeed === false ? 'Nein' : ''}
|
||
missing={c.parkingNeed === undefined}
|
||
onEdit={v => set({ ...c, parkingNeed: v.toLowerCase().startsWith('j') })}
|
||
/>
|
||
</Section>
|
||
|
||
{/* ── AI Assumptions ──────────────────────────────────────────────── */}
|
||
{assumptions.length > 0 && (
|
||
<Section title="KI-Annahmen">
|
||
<Stack spacing={0.5}>
|
||
{assumptions.map((a, i) => (
|
||
<Alert key={i} severity="warning" sx={{ py: 0, px: 1, '& .MuiAlert-message': { fontSize: 12 } }}>
|
||
{a}
|
||
</Alert>
|
||
))}
|
||
</Stack>
|
||
</Section>
|
||
)}
|
||
|
||
{/* ── Missing Information ─────────────────────────────────────────── */}
|
||
{missingFields.length > 0 && (
|
||
<Section title="Fehlende Angaben">
|
||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||
{missingFields.map(f => (
|
||
<Chip key={f} label={f} size="small" color="warning" variant="outlined" />
|
||
))}
|
||
</Stack>
|
||
</Section>
|
||
)}
|
||
</Card>
|
||
)
|
||
}
|