Files
property-match/.claude/worktrees/agent-a82a3716/src/components/demand/CriteriaReviewPanel.tsx
T
Benjamin Sutter d15a13e485 feat: remove Administration workspace — keep only Verwaltung + Suche
- 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>
2026-05-19 20:32:41 +02:00

156 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}` : ''
}
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. 500800)"
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>
)
}