fix: resolve all TypeScript errors and refactor oversized components

- Fix MUI v9 API: PaperProps/InputLabelProps/inputProps → slotProps in 6 components
- Add ExternalMarketResult alias, PropertyUnit import, OPERATIONS workspace config
- Fix TradeOff.description → .concern, ScoreFactor.label → .criterion
- Make schattenmarktRelease.leadTimeMonths optional, fix mock-data enum values
- Fix useMatchDetailData query typing, weightingService missing WeightProfile keys
- Split Pipeline/Compare/MatchDetail/IntelligenceMatchCard into sub-components
- Fix all test fixtures (CreateNeedInput, CreatePropertyInput, TradeOffInput, etc.)
- Add vercel.json for deployment, zero tsc errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-26 00:08:35 +02:00
parent 98d2770054
commit 36570c5bdc
52 changed files with 269 additions and 143 deletions
+3 -1
View File
@@ -3,7 +3,9 @@
"allow": [
"Bash(git commit -m ' *)",
"Bash(git commit *)",
"Bash(node -e ' *)"
"Bash(node -e ' *)",
"Bash(git stash *)",
"Read(//c/Users/beni_/.claude/projects/c--Users-beni--OneDrive-Desktop-property-match/8e388ddd-e02e-47fe-8bb9-cdb8164c9fc3/tool-results/**)"
]
}
}
+11
View File
@@ -0,0 +1,11 @@
> Why do I have a folder named ".vercel" in my project?
The ".vercel" folder is created when you link a directory to a Vercel project.
> What does the "project.json" file contain?
The "project.json" file contains:
- The ID of the Vercel project that you linked ("projectId")
- The ID of the user or team your Vercel project is owned by ("orgId")
> Should I commit the ".vercel" folder?
No, you should not share the ".vercel" folder with anyone.
Upon creation, it will be automatically added to your ".gitignore" file.
+1
View File
@@ -0,0 +1 @@
{"projectId":"prj_0UhtBM4Jg83OXPDkzhOM6x0gYfEY","orgId":"team_Quh2pScdkVSiGHkdwh6ntKZ5","projectName":"property-match"}
@@ -123,7 +123,6 @@ export function InquiryReplyComposer({
display: 'flex',
alignItems: 'center',
gap: 0.75,
bgcolor: 'white',
border: '1px solid',
borderColor: a.generated ? '#bfdbfe' : '#cbd5e1',
borderRadius: 1,
@@ -104,7 +104,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
}
return (
<Dialog open fullWidth maxWidth="lg" PaperProps={{ sx: { height: '90vh', display: 'flex', flexDirection: 'column' } }}>
<Dialog open fullWidth maxWidth="lg" slotProps={{ paper: { sx: { height: '90vh', display: 'flex', flexDirection: 'column' } } }}>
<Box sx={{ px: 3, pt: 2.5, pb: 1.5, borderBottom: '1px solid #e2e8f0', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Angebot erstellen</Typography>
<Button size="small" onClick={onClose} sx={{ minWidth: 0, p: 0.5 }}><X size={18} /></Button>
@@ -187,7 +187,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
size="small"
value={a.date}
onChange={e => updateAppointment(a.id, 'date', e.target.value)}
InputLabelProps={{ shrink: true }}
slotProps={{ inputLabel: { shrink: true } }}
sx={{ width: 160 }}
/>
<TextField
@@ -228,7 +228,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
progress={progress}
ready={ready}
draft={draft}
property={property}
property={property ?? undefined}
onDownload={() => showToast('PDF wird heruntergeladen…', 'info')}
onAttach={() => {
const label = `Angebot_${property?.title ?? 'Objekt'}.pdf`
@@ -48,13 +48,15 @@ export function OfferWizard() {
fullScreen={fullScreen}
maxWidth="lg"
fullWidth
PaperProps={{
slotProps={{
paper: {
sx: {
height: fullScreen ? '100vh' : '85vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
},
},
}}
>
{/* Header */}
@@ -6,7 +6,6 @@ import {
} from '@mui/material'
import { Download, Send, X } from 'lucide-react'
import type { Inquiry } from '../../domain/inquiry'
import type { Property } from '../../domain/property'
import type { InquiryPreparationReportDraft, ReportObjectFieldSelection } from '../../domain/inquiryReport'
import { useCreateInquiryReport, useUpdateInquiryReport, useFinalizeInquiryReport } from '../../hooks/useInquiryReport'
import { useToastStore } from '../../stores/toastStore'
@@ -40,10 +39,6 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
const portfolioProps = allProperties.filter(p => p.resultType === ResultType.VERIFIED_PORTFOLIO)
const selectedProperties = draft?.selectedPropertyIds
.map(id => allProperties.find(p => p.id === id))
.filter((p): p is Property => !!p) ?? []
const toggleProperty = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id],
@@ -112,7 +107,7 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
}
return (
<Dialog open fullWidth maxWidth="lg" PaperProps={{ sx: { height: '90vh', display: 'flex', flexDirection: 'column' } }}>
<Dialog open fullWidth maxWidth="lg" slotProps={{ paper: { sx: { height: '90vh', display: 'flex', flexDirection: 'column' } } }}>
<Box sx={{ px: 3, pt: 2.5, pb: 1.5, borderBottom: '1px solid #e2e8f0', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Vorbereitung starten</Typography>
<Button size="small" onClick={onClose} sx={{ minWidth: 0, p: 0.5 }}><X size={18} /></Button>
@@ -82,8 +82,6 @@ const FIELD_GROUPS: FieldGroup[] = [
},
]
const MANDATORY: ReportObjectFieldKey[] = ['title', 'location', 'mapImageUrl', 'images']
export function ReportObjectFieldSelector({ propertyTitle, value, onChange }: Props) {
const isSelected = (key: ReportObjectFieldKey) =>
value.selectedOptionalFields.includes(key)
@@ -115,7 +113,6 @@ export function ReportObjectFieldSelector({ propertyTitle, value, onChange }: Pr
{FIELD_GROUPS.map(group => {
const groupKeys = group.fields.map(f => f.key)
const allSelected = groupKeys.every(k => value.selectedOptionalFields.includes(k))
return (
<Accordion key={group.label} disableGutters elevation={0} sx={{ border: '1px solid #e2e8f0', mb: 0.5, '&:before': { display: 'none' } }}>
<AccordionSummary expandIcon={<ChevronDown size={16} />} sx={{ minHeight: 40, '& .MuiAccordionSummary-content': { my: 0.5 } }}>
@@ -0,0 +1,24 @@
import { memo } from 'react'
import type { ReactNode } from 'react'
import { TableCell, TableRow } from '@mui/material'
import { LABEL_SX, DATA_SX } from './compareUtils'
// ── Props ─────────────────────────────────────────────────────────────────────
export interface CompareMetricRowProps {
label: string
cells: ReactNode[]
}
// ── Component ─────────────────────────────────────────────────────────────────
export const CompareMetricRow = memo(function CompareMetricRow({ label, cells }: CompareMetricRowProps) {
return (
<TableRow hover>
<TableCell sx={LABEL_SX}>{label}</TableCell>
{cells.map((cell, i) => (
<TableCell key={i} sx={DATA_SX}>{cell}</TableCell>
))}
</TableRow>
)
})
+12
View File
@@ -12,6 +12,7 @@ import {
MessageSquare,
Kanban,
BellRing,
Settings,
} from 'lucide-react'
// ---------------------------------------------------------------------------
@@ -68,6 +69,17 @@ export const WORKSPACE_CONFIG: Record<WorkspaceType, WorkspaceConfig> = {
{ path: '/demand/anfragen', label: 'Anfragen', icon: MessageSquare },
],
},
[WorkspaceType.OPERATIONS]: {
label: 'Operations',
abbreviation: 'OP',
icon: Settings,
firstPath: '/ops/review-queue',
chipColor: '#6b21a8',
navItems: [
{ path: '/ops/review-queue', label: 'Review Queue', icon: CheckSquare },
{ path: '/ops/audit', label: 'Audit Log', icon: ClipboardList },
],
},
}
// Ordered list for rendering workspace tabs
@@ -1,3 +1,4 @@
import { memo } from 'react'
import { Box, Button, Divider, Typography } from '@mui/material'
import { AlertCircle, CheckCircle2, ShieldCheck } from 'lucide-react'
import { useNavigate } from 'react-router'
@@ -9,7 +10,7 @@ import type { MatchCardViewModel } from './MatchCardViewModel'
// ── Future Availability Card ──────────────────────────────────────────────────
export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
export const FutureAvailabilityCard = memo(function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
const isControlled = vm.signalIsControlled ?? false
const navigate = useNavigate()
@@ -243,4 +244,4 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
</Box>
</Box>
)
}
})
@@ -182,7 +182,7 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5, mt: 0.5 }}>
<AlertTriangle size={12} color="#d97706" style={{ marginTop: 2, flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.69rem', color: '#92400e', lineHeight: 1.3 }}>
{vm.tradeoffs[0].description}
{vm.tradeoffs[0].concern}
</Typography>
</Box>
)}
+1
View File
@@ -11,6 +11,7 @@ export { MatchDataQualitySummary } from './MatchDataQualitySummary'
export { MatchActionToolbar } from './MatchActionToolbar'
export { MatchCardSkeleton } from './MatchCardSkeleton'
export { MatchCardRestrictedState } from './MatchCardRestrictedState'
export { FutureAvailabilityCard } from './FutureAvailabilityCard'
export type {
MatchCardViewModel,
MatchCardVariant,
@@ -5,7 +5,7 @@ import { ResultType } from '../../domain/enums'
import type { Property } from '../../domain/property'
import { FLOOR_LABEL, ASSET_LABELS, RISK_LABELS, SOURCE_LABELS, PASSERBY_LABELS, KeyFactRow, UnitRow } from './MatchDetailPropertyDetails'
import { FloorPlanSection } from './FloorPlanSection'
import { DS_TEXT, DS_BG, DS_SURFACE, DS_BORDER, DS_MARKET_SIGNAL, DS_PRE_MARKET } from '../../lib/ds'
import { DS_TEXT, DS_BG, DS_SURFACE, DS_BORDER, DS_MARKET_SIGNAL } from '../../lib/ds'
type Match = NonNullable<ReturnType<typeof useMatchDetail>['data']>
@@ -0,0 +1,92 @@
import { memo, useState } from 'react'
import { Box, Button, Paper, Typography } from '@mui/material'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { PropertyMap } from '../shared'
import {
ExecutiveSummaryPanel,
NeedAlignmentPanel,
LocationIntelligencePanel,
TradeoffPanel,
RiskPanel,
MissingInformationPanel,
FutureAvailabilityContextPanel,
ScoreBreakdownPanel,
} from '.'
import { MatchDetailPropertySections } from './MatchDetailPropertySections'
import { useMatchDetail } from '../../hooks/useMatches'
import type { Property } from '../../domain/property'
import type { Need } from '../../domain/need'
import type { FutureSignal } from '../../domain/futureSignal'
import { DS_TEXT, DS_BORDER, DS_BG } from '../../lib/ds'
type Match = NonNullable<ReturnType<typeof useMatchDetail>['data']>
interface Props {
match: Match
property: Property | null
need: Need | null
signal: FutureSignal | null
isFuture: boolean
taxCalculatorUrl?: string
}
export const MatchDetailScoreBreakdown = memo(function MatchDetailScoreBreakdown({
match,
property,
need,
signal,
isFuture,
taxCalculatorUrl,
}: Props) {
const [showFullAnalysis, setShowFullAnalysis] = useState(false)
return (
<>
{/* ── Full analysis toggle ── */}
<Button
variant="outlined"
onClick={() => setShowFullAnalysis(v => !v)}
fullWidth
endIcon={showFullAnalysis ? <ChevronUp size={15} /> : <ChevronDown size={15} />}
sx={{
borderStyle: 'dashed', color: DS_TEXT.muted, borderColor: DS_BORDER.strong,
textTransform: 'none', fontWeight: 500,
'&:hover': { borderColor: DS_TEXT.muted, bgcolor: DS_BG.subtle },
}}
>
{showFullAnalysis ? 'Weniger anzeigen' : 'Vollständige Analyse anzeigen'}
</Button>
{/* ── Full analysis (hidden by default) ── */}
{showFullAnalysis && (
<>
<ExecutiveSummaryPanel match={match} />
<NeedAlignmentPanel match={match} need={need} property={property} />
{!isFuture && property && <MatchDetailPropertySections property={property} match={match} />}
<LocationIntelligencePanel property={property} />
{!isFuture && property?.images?.[0] && property?.location?.coordinates && (
<Paper sx={{ overflow: 'hidden', p: 0 }}>
<Box sx={{ px: 2.5, pt: 2, pb: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Standort</Typography>
<Typography variant="caption" color="text.secondary">
{property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
</Typography>
</Box>
<PropertyMap
lat={property.location.coordinates.lat}
lng={property.location.coordinates.lng}
label={property.title}
height={220}
/>
</Paper>
)}
<TradeoffPanel match={match} />
<RiskPanel match={match} />
<MissingInformationPanel match={match} />
{isFuture && <FutureAvailabilityContextPanel match={match} signal={signal} />}
<ScoreBreakdownPanel match={match} taxCalculatorUrl={taxCalculatorUrl} isFuture={isFuture} signal={signal} />
</>
)}
</>
)
})
@@ -11,7 +11,6 @@ import {
ExternalLink,
Info,
Layers,
ShieldCheck,
Tag,
Train,
TrendingUp,
+2
View File
@@ -1,5 +1,7 @@
export { LocationIntelligencePanel } from './LocationIntelligencePanel'
export { MatchDetailHeader } from './MatchDetailHeader'
export { MatchDetailHero } from './MatchDetailHero'
export { MatchDetailScoreBreakdown } from './MatchDetailScoreBreakdown'
export { ExecutiveSummaryPanel } from './ExecutiveSummaryPanel'
export { PropertyOverviewPanel } from './PropertyOverviewPanel'
export { NeedAlignmentPanel } from './NeedAlignmentPanel'
@@ -37,13 +37,13 @@ export function AreaDetailsSection({
<TextField
label="Fläche (m²)" value={areaSqm}
onChange={e => onAreaSqmChange(e.target.value)}
size="small" type="number" inputProps={{ min: 1 }} fullWidth
size="small" type="number" slotProps={{ htmlInput: { min: 1 } }} fullWidth
/>
<TextField
label="Mietpreis (CHF/m²/Jahr)" value={rentPerSqm}
onChange={e => onRentPerSqmChange(e.target.value)}
size="small" type="number" inputProps={{ min: 1 }} fullWidth
size="small" type="number" slotProps={{ htmlInput: { min: 1 } }} fullWidth
/>
<TextField
@@ -38,12 +38,12 @@ export function TechnicalDetailsSection({
<TextField
label="Parkplätze" value={parking}
onChange={e => onParkingChange(e.target.value)}
size="small" type="number" inputProps={{ min: 0 }} fullWidth
size="small" type="number" slotProps={{ htmlInput: { min: 0 } }} fullWidth
/>
<TextField
label="Deckenhöhe (m)" value={ceilingHeight}
onChange={e => onCeilingHeightChange(e.target.value)}
size="small" type="number" inputProps={{ step: 0.1, min: 2 }} fullWidth
size="small" type="number" slotProps={{ htmlInput: { step: 0.1, min: 2 } }} fullWidth
/>
</Box>
</Card>
@@ -146,7 +146,6 @@ export function WelcomeDialog() {
open={open}
maxWidth="sm"
fullWidth
disableEscapeKeyDown
>
<DialogContent sx={{ pt: 4, pb: 2 }}>
{SLIDES[activeStep]}
+5
View File
@@ -0,0 +1,5 @@
export { STAGES, NEXT_STAGE, MOCK_DOCS, RESULT_TYPE_META } from './pipelineConstants'
export { scoreColor, detailPath, getKiInsight } from './pipelineUtils'
export { DraggableCard } from './PipelineCard'
export { DroppableColumn } from './PipelineColumn'
export { DetailPanel } from './PipelineDetailPanel'
+1 -1
View File
@@ -78,7 +78,7 @@ export function BerichtDialog({ onClose, report, propertyId }: BerichtDialogProp
onClose={ready ? onClose : undefined}
maxWidth="md"
fullWidth
PaperProps={{ sx: { height: '88vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
slotProps={{ paper: { sx: { height: '88vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' } } }}
>
{/* Header */}
<Box sx={{ px: 3, py: 1.5, borderBottom: `1px solid ${DS_BORDER.default}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
@@ -1,5 +1,5 @@
import { Box, Button, Divider, LinearProgress, TextField, Typography } from '@mui/material'
import { ExternalLink, FileText, Plus } from 'lucide-react'
import { ExternalLink, FileText } from 'lucide-react'
import type { Property, UpdatePropertyInput } from '../../domain/property'
import { PropertyMap } from '../shared'
import { getAssetTypeLabel, qualityColor } from './propertyHelpers'
@@ -20,7 +20,7 @@ import { ReminderPriorityBadge } from './ReminderPriorityBadge'
import { ReminderTypeBadge } from './ReminderTypeBadge'
import { ReminderDaysIndicator } from './ReminderDaysIndicator'
import { ReminderStatus } from '../../domain/reminder'
import { SHADOW_RISK_COLOR, SHADOW_RISK_LABEL, STATUS_CHIP_COLOR, STATUS_LABEL, ACTION_LABEL, SectionTitle, DateRow, ActivityEntry } from './reminderDetailHelpers'
import { SHADOW_RISK_COLOR, SHADOW_RISK_LABEL, STATUS_CHIP_COLOR, STATUS_LABEL, SectionTitle, DateRow, ActivityEntry } from './reminderDetailHelpers'
export function ReminderDetailDrawer() {
const { selectedId, drawerOpen, setDrawerOpen, setSelectedId } = useReminderStore()
+1 -1
View File
@@ -95,7 +95,7 @@ export function ReminderFeed() {
if (viewMode === 'card') {
return (
<Box className="flex flex-col gap-4">
{HORIZON_CONFIG.map(({ key, label, color, bg, border }) => {
{HORIZON_CONFIG.map(({ key, label, color }) => {
const group = grouped[key]
if (!group || group.length === 0) return null
return (
+2 -1
View File
@@ -3,6 +3,7 @@ import type {
FreshnessStatus, RiskLevel, SourceType, DataQualityLevel,
} from './enums'
import { AvailabilityStatus as AS } from './enums'
import type { PropertyUnit } from './unit'
// Re-export unit types so all existing imports from 'property' continue to work
export type { PropertyUnit, UnitBundle, UnitNeedMatch } from './unit'
@@ -152,7 +153,7 @@ export interface Property {
importedAt?: string
lastUpdatedAt?: string
schattenmarktRelease?: { enabled: boolean; leadTimeMonths: number }
schattenmarktRelease?: { enabled: boolean; leadTimeMonths?: number }
status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED'
lastReviewedAt?: string
+3
View File
@@ -29,6 +29,9 @@ export interface FutureAvailabilityResult extends UnifiedResultBase {
unit?: PropertyUnit // specific rentable unit (when signal has a unitId)
}
/** @deprecated Alias kept for backward compatibility — use VerifiedPortfolioResult */
export type ExternalMarketResult = VerifiedPortfolioResult
export type UnifiedMatchResult =
| VerifiedPortfolioResult
| FutureAvailabilityResult
@@ -44,9 +44,9 @@ describe('buildMatchCardViewModel — VERIFIED_PORTFOLIO', () => {
it('scoreBreakdown reflects hard and soft components from the engine', () => {
const vm = buildMatchCardViewModel(result, [])
expect(vm.scoreBreakdown.hardMatchScore).toBe(match.scoreBreakdown.hardMatchScore)
expect(vm.scoreBreakdown.softFactorScore).toBe(match.scoreBreakdown.softFactorScore)
expect(vm.scoreBreakdown.totalScore).toBe(match.scoreBreakdown.totalScore)
expect(vm.scoreBreakdown!.hardMatchScore).toBe(match.scoreBreakdown.hardMatchScore)
expect(vm.scoreBreakdown!.softFactorScore).toBe(match.scoreBreakdown.softFactorScore)
expect(vm.scoreBreakdown!.totalScore).toBe(match.scoreBreakdown.totalScore)
})
it('does NOT set a disclaimer for verified portfolio results', () => {
@@ -65,7 +65,7 @@ describe('buildMatchCardViewModel — VERIFIED_PORTFOLIO', () => {
})
it('passes through supplied actions unchanged', () => {
const actions = [{ label: 'Shortlist', actionType: 'SHORTLIST' as const, primary: true }]
const actions = [{ id: 'shortlist', label: 'Shortlist', actionType: 'SAVE_SHORTLIST' as const, primary: true, onClick: () => {} }]
const vm = buildMatchCardViewModel(result, actions)
expect(vm.actions).toBe(actions)
})
+3
View File
@@ -61,6 +61,9 @@ describe('useCreateNeed', () => {
preferredLocations: ['Zürich'],
budgetRange: { maxPerSqm: 400, maxMonthlyTotal: 10000, currency: 'CHF' },
organizationId: 'org-test',
timing: { earliestMoveIn: '2025-01-01', latestMoveIn: '2026-01-01', flexibleTiming: true },
weightingProfile: { area: 0.3, location: 0.3, budget: 0.2, timing: 0.1, prestige: 0, accessibility: 0, expansionPotential: 0, flexibility: 0, visibility: 0, footfall: 0, talentAccess: 0, esg: 0, taxEnvironment: 0 },
confidenceInCriteria: 0.8,
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data?.data.companyName).toBe('Hook Test GmbH')
@@ -71,9 +71,14 @@ describe('useCreateProperty', () => {
assetType: 'LOGISTICS',
resultType: 'VERIFIED_PORTFOLIO',
location: { city: 'Bern', country: 'CH' },
address: { street: 'Teststrasse', houseNumber: '1', postalCode: '3000', city: 'Bern', country: 'CH' },
areaSqm: 300,
rentPricePerSqm: 100,
availabilityDate: '2025-01-01',
availabilityStatus: 'AVAILABLE_NOW',
sourceType: 'DIRECT',
confidenceScore: 0.8,
dataQuality: { score: 0.8, missingCriticalFields: [], missingOptionalFields: [], freshness: 'FRESH', warnings: [] },
organizationId: 'org-test',
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
+3 -2
View File
@@ -3,6 +3,7 @@ import { useMatchDetail } from './useMatches'
import { propertyService } from '../services/propertyService'
import { needService } from '../services/needService'
import { futureSignalService } from '../services/futureSignalService'
import type { ItemResponse } from '../services/types'
import type { Property } from '../domain/property'
import type { Need } from '../domain/need'
import type { FutureSignal } from '../domain/futureSignal'
@@ -13,14 +14,14 @@ export function useMatchDetailData(matchId: string) {
const { data: match, isLoading } = useMatchDetail(matchId)
const isFuture = match?.resultType === 'FUTURE_AVAILABILITY'
const { data: property = null } = useQuery<Property | null>({
const { data: property = null } = useQuery<ItemResponse<Property | null>, Error, Property | null>({
queryKey: ['property', match?.propertyId],
queryFn: () => propertyService.getById(match!.propertyId),
enabled: !!match && !isFuture,
select: r => r.data ?? null,
})
const { data: need = null } = useQuery<Need | null>({
const { data: need = null } = useQuery<ItemResponse<Need | null>, Error, Need | null>({
queryKey: ['need', match?.needId],
queryFn: () => needService.getById(match!.needId),
enabled: !!match?.needId,
+1
View File
@@ -13,6 +13,7 @@ interface PropertyFilter {
minAreaSqm?: number
maxRentPerSqm?: number
organizationId?: string
sourceType?: string
}
export function useProperties(filter?: PropertyFilter) {
+2 -2
View File
@@ -22,13 +22,13 @@ export function useSchattenmarktSignals(properties: Property[]): FutureSignal[]
for (const unit of releasedUnits) {
const availableFrom = unit.schattenmarktRelease?.availableFrom ?? unit.leaseEndDate ?? p.leaseEndDate
if (!availableFrom) continue
const triggerDate = getTriggerDate(availableFrom, rel.leadTimeMonths)
const triggerDate = getTriggerDate(availableFrom, rel.leadTimeMonths ?? 0)
if (MOCK_TODAY < triggerDate) continue
signals.push(buildUnitSignal(p, unit, availableFrom))
}
} else {
// Backward compat: property-level signal (no explicit unit releases defined)
const triggerDate = getEarliestTriggerDate(p, rel.leadTimeMonths)
const triggerDate = getEarliestTriggerDate(p, rel.leadTimeMonths ?? 0)
if (!triggerDate || MOCK_TODAY < triggerDate) continue
signals.push(buildPropertySignal(p))
}
+1 -2
View File
@@ -1,12 +1,11 @@
import { useMemo } from 'react'
import { useMatches, useMatchesByNeed } from './useMatches'
import { useMatchesByNeed } from './useMatches'
import { useProperties } from './useProperties'
import { useFutureSignals } from './useFutureSignals'
import { useSchattenmarktSignals } from './useSchattenmarktSignals'
import type {
UnifiedMatchResult,
VerifiedPortfolioResult,
FutureAvailabilityResult,
} from '../domain/unifiedResult'
export function useUnifiedResults(needId?: string) {
+4
View File
@@ -67,6 +67,10 @@ const WORKSPACE_ROLES: Record<WorkspaceType, UserRole[]> = {
UserRole.PROPERTY_MANAGER,
UserRole.DEMAND_USER,
],
[WorkspaceType.OPERATIONS]: [
UserRole.SUPER_ADMIN,
UserRole.ORGANIZATION_ADMIN,
],
}
// ── Core Functions ────────────────────────────────────────────────────────────
+8 -8
View File
@@ -608,7 +608,7 @@ export const mockProperties: Property[] = [
prestige: 56,
accessibility: 88,
visibilityScore: 72,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 65,
parkingSpots: 1,
publicTransportMinutes: 5,
@@ -830,7 +830,7 @@ export const mockProperties: Property[] = [
prestige: 60,
accessibility: 88,
visibilityScore: 75,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 70,
parkingSpots: 2,
publicTransportMinutes: 5,
@@ -2017,7 +2017,7 @@ export const mockProperties: Property[] = [
prestige: 48,
accessibility: 78,
visibilityScore: 68,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 60,
parkingSpots: 2,
publicTransportMinutes: 5,
@@ -2230,7 +2230,7 @@ export const mockProperties: Property[] = [
prestige: 58,
accessibility: 85,
visibilityScore: 70,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 65,
parkingSpots: 2,
publicTransportMinutes: 5,
@@ -2373,7 +2373,7 @@ export const mockProperties: Property[] = [
prestige: 55,
accessibility: 82,
visibilityScore: 70,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 65,
parkingSpots: 0,
publicTransportMinutes: 5,
@@ -3453,7 +3453,7 @@ export const mockProperties: Property[] = [
prestige: 85,
accessibility: 95,
visibilityScore: 88,
passerbyFrequency: 'MEDIUM_HIGH',
passerbyFrequency: 'HIGH',
talentAccess: 76,
parkingSpots: 0,
publicTransportMinutes: 3,
@@ -4586,7 +4586,7 @@ export const mockProperties: Property[] = [
availabilityStatus: AvailabilityStatus.AVAILABLE_SOON,
sourceType: 'DIRECT',
confidenceScore: 0.72,
dataQuality: { score: 0.68, missingCriticalFields: ['contactPerson'], missingOptionalFields: ['softFactors'], lastVerifiedAt: '2026-02-15', freshness: DataFreshness.AGING, warnings: ['Kein Ansprechpartner hinterlegt'] },
dataQuality: { score: 0.68, missingCriticalFields: ['contactPerson'], missingOptionalFields: ['softFactors'], lastVerifiedAt: '2026-02-15', freshness: DataFreshness.STALE, warnings: ['Kein Ansprechpartner hinterlegt'] },
softFactors: { prestige: 68, accessibility: 85, visibilityScore: 80, publicTransportMinutes: 5 },
hardFacts: { floor: 0, fitOut: 'BASIC', hasStorefront: true },
floorLevel: 0,
@@ -4640,7 +4640,7 @@ export const mockProperties: Property[] = [
availabilityStatus: AvailabilityStatus.AVAILABLE_SOON,
sourceType: 'DIRECT',
confidenceScore: 0.70,
dataQuality: { score: 0.65, missingCriticalFields: ['rentPricePerSqm'], missingOptionalFields: ['images', 'description'], lastVerifiedAt: '2026-03-01', freshness: DataFreshness.AGING, warnings: ['Mietpreis nicht verifiziert'] },
dataQuality: { score: 0.65, missingCriticalFields: ['rentPricePerSqm'], missingOptionalFields: ['images', 'description'], lastVerifiedAt: '2026-03-01', freshness: DataFreshness.STALE, warnings: ['Mietpreis nicht verifiziert'] },
softFactors: { prestige: 55, accessibility: 70, publicTransportMinutes: 12, parkingSpots: 8 },
hardFacts: { floor: 1, fitOut: 'BASIC', parking: 8 },
floorLevel: 1,
+2 -3
View File
@@ -8,7 +8,6 @@ import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'luci
import { mockDemandInquiries } from '../../mock-data/demandInquiries'
import { useInquiryStore } from '../../stores/inquiryStore'
import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline'
import { useToastStore } from '../../stores/toastStore'
import type { InquiryMessage } from '../../domain/inquiry'
import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection'
import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble'
@@ -32,7 +31,7 @@ export default function Anfragen() {
const [searchParams] = useSearchParams()
const { data: pipelineItems = [] } = usePipelineItems()
const { mutate: moveStage } = useMoveStage()
const showToast = useToastStore(s => s.showToast)
const preselectedId = searchParams.get('inquiry')
@@ -156,7 +155,7 @@ export default function Anfragen() {
<TextField
size="small" placeholder="Suchen..." fullWidth
value={search} onChange={e => setSearch(e.target.value)}
InputProps={{ startAdornment: <InputAdornment position="start"><Search size={14} color={DS_TEXT.disabled} /></InputAdornment> }}
slotProps={{ input: { startAdornment: <InputAdornment position="start"><Search size={14} color={DS_TEXT.disabled} /></InputAdornment> } }}
sx={{ mb: 1.25 }}
/>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
-1
View File
@@ -41,7 +41,6 @@ export default function Compare() {
overallWinnerIdx,
bestScoreIdx,
worstConfIdx,
dqScores,
worstDQIdx,
missingCriticalCounts,
maxMissingCritical,
+12 -61
View File
@@ -1,31 +1,19 @@
import { useState } from 'react'
import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog'
import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material'
import { AlertTriangle, ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react'
import { AlertTriangle, ArrowLeft } from 'lucide-react'
import { useNavigate, useParams } from 'react-router'
import { useCompareStore } from '../../stores/compareStore'
import { usePipelineStore } from '../../stores/pipelineStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { AddToPipelineDialog } from '../../components/shortlist'
import { MatchReasonList } from '../../components/match-card/MatchReasonList'
import { PropertyMap } from '../../components/shared'
import { getCityIntelligence } from '../../lib/locationIntelligence'
import {
LocationIntelligencePanel,
ExecutiveSummaryPanel,
NeedAlignmentPanel,
ScoreBreakdownPanel,
TradeoffPanel,
RiskPanel,
MissingInformationPanel,
FutureAvailabilityContextPanel,
NextActionsPanel,
} from '../../components/match-detail'
import { NextActionsPanel } from '../../components/match-detail'
import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel'
import { useMatchDetailData } from '../../hooks/useMatchDetailData'
import { useMatchDetail } from '../../hooks/useMatches'
import { MatchDetailHero } from '../../components/match-detail/MatchDetailHero'
import { MatchDetailPropertySections } from '../../components/match-detail/MatchDetailPropertySections'
import { MatchDetailScoreBreakdown } from '../../components/match-detail/MatchDetailScoreBreakdown'
import { RESULT_TYPE_META, DS_TEXT, DS_SURFACE, DS_BORDER, DS_BG } from '../../lib/ds'
// ── helpers ───────────────────────────────────────────────────────────────────
@@ -49,7 +37,6 @@ export default function MatchDetail() {
const { addToCompare } = useCompareStore()
const { openSavedDialog } = usePipelineStore()
const { openInquiryDialog } = useInquiryStore()
const [showFullAnalysis, setShowFullAnalysis] = useState(false)
const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '')
@@ -91,7 +78,7 @@ export default function MatchDetail() {
const handleCompare = () => {
if (match && !isFuture && property) {
addToCompare({
resultType: property.resultType ?? 'VERIFIED_PORTFOLIO',
resultType: (property.resultType ?? 'VERIFIED_PORTFOLIO') as 'VERIFIED_PORTFOLIO' | 'MAISON_WORK',
matchId: match.id, needId: match.needId, matchScore: match.matchScore, match, property,
})
} else if (match && isFuture && signal) {
@@ -210,51 +197,15 @@ export default function MatchDetail() {
})}
/>
{/* ── Full analysis toggle ── */}
<Button
variant="outlined"
onClick={() => setShowFullAnalysis(v => !v)}
fullWidth
endIcon={showFullAnalysis ? <ChevronUp size={15} /> : <ChevronDown size={15} />}
sx={{
borderStyle: 'dashed', color: DS_TEXT.muted, borderColor: DS_BORDER.strong,
textTransform: 'none', fontWeight: 500,
'&:hover': { borderColor: DS_TEXT.muted, bgcolor: DS_BG.subtle },
}}
>
{showFullAnalysis ? 'Weniger anzeigen' : 'Vollständige Analyse anzeigen'}
</Button>
{/* ── Full analysis (hidden by default) ── */}
{showFullAnalysis && (
<>
<ExecutiveSummaryPanel match={match} />
<NeedAlignmentPanel match={match} need={need} property={property} />
{!isFuture && property && <MatchDetailPropertySections property={property} match={match} />}
<LocationIntelligencePanel property={property} />
{!isFuture && property?.images?.[0] && property?.location?.coordinates && (
<Paper sx={{ overflow: 'hidden', p: 0 }}>
<Box sx={{ px: 2.5, pt: 2, pb: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Standort</Typography>
<Typography variant="caption" color="text.secondary">
{property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
</Typography>
</Box>
<PropertyMap
lat={property.location.coordinates.lat}
lng={property.location.coordinates.lng}
label={property.title}
height={220}
{/* ── Full analysis toggle + expanded panels ── */}
<MatchDetailScoreBreakdown
match={match}
property={property}
need={need}
signal={signal}
isFuture={isFuture}
taxCalculatorUrl={taxCalculatorUrl}
/>
</Paper>
)}
<TradeoffPanel match={match} />
<RiskPanel match={match} />
<MissingInformationPanel match={match} />
{isFuture && <FutureAvailabilityContextPanel match={match} signal={signal} />}
<ScoreBreakdownPanel match={match} taxCalculatorUrl={taxCalculatorUrl} isFuture={isFuture} signal={signal} />
</>
)}
</Box>
</Box>
)
+1 -1
View File
@@ -2,7 +2,7 @@ import { memo, useState, useEffect, useCallback, useMemo } from 'react'
import { useNavigate } from 'react-router'
import {
Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle,
Divider, FormControl, FormControlLabel, IconButton,
Divider, FormControl, FormControlLabel,
InputLabel, MenuItem, Select as MuiSelect,
Skeleton, Switch, Tab, Tabs, TextField, ToggleButton, ToggleButtonGroup,
Tooltip, Typography,
+1 -1
View File
@@ -53,7 +53,7 @@ export default function SupplyDashboard() {
<Paper sx={{ p: 0, overflow: 'hidden', border: `1px solid ${DS_BORDER.default}` }}>
{/* KPI strip */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr 1fr', md: 'repeat(3, 1fr)' }, divideX: true }}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr 1fr', md: 'repeat(3, 1fr)' } }}>
{/* Starke Matches */}
<Box sx={{ p: 2.5, borderRight: `1px solid ${DS_BORDER.default}`, bgcolor: strongMatchCount > 0 ? DS_SURFACE.success.bg : 'white' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
@@ -7,7 +7,7 @@ describe('futureSignalService', () => {
const result = await futureSignalService.getAll()
expect(Array.isArray(result.data)).toBe(true)
expect(result.data.length).toBeGreaterThan(0)
expect(result.meta.total).toBe(result.data.length)
expect(result.meta!.total).toBe(result.data.length)
})
it('each signal has required fields', async () => {
+1 -1
View File
@@ -7,7 +7,7 @@ describe('matchService', () => {
const result = await matchService.getAll()
expect(Array.isArray(result.data)).toBe(true)
expect(result.data.length).toBeGreaterThan(0)
expect(result.meta.total).toBe(result.data.length)
expect(result.meta!.total).toBe(result.data.length)
})
it('each match has required fields', async () => {
+9 -3
View File
@@ -7,9 +7,9 @@ describe('needService', () => {
const result = await needService.getAll()
expect(Array.isArray(result.data)).toBe(true)
expect(result.data.length).toBeGreaterThan(0)
expect(result.meta.total).toBe(result.data.length)
expect(result.meta.page).toBe(1)
expect(result.meta.hasMore).toBe(false)
expect(result.meta!.total).toBe(result.data.length)
expect(result.meta!.page).toBe(1)
expect(result.meta!.hasMore).toBe(false)
})
it('each need has required fields', async () => {
@@ -49,6 +49,9 @@ describe('needService', () => {
preferredLocations: ['Zürich'],
budgetRange: { maxPerSqm: 400, maxMonthlyTotal: 10000, currency: 'CHF' as const },
organizationId: 'org-test',
timing: { earliestMoveIn: '2025-01-01', latestMoveIn: '2026-01-01', flexibleTiming: true },
weightingProfile: { area: 0.3, location: 0.3, budget: 0.2, timing: 0.1, prestige: 0, accessibility: 0, expansionPotential: 0, flexibility: 0, visibility: 0, footfall: 0, talentAccess: 0, esg: 0, taxEnvironment: 0 },
confidenceInCriteria: 0.8,
}
const result = await needService.create(input)
expect(result.data.companyName).toBe('Test GmbH')
@@ -79,6 +82,9 @@ describe('needService', () => {
preferredLocations: ['Bern'],
budgetRange: { maxPerSqm: 300, maxMonthlyTotal: 5000, currency: 'CHF' as const },
organizationId: 'org-test',
timing: { earliestMoveIn: '2025-01-01', latestMoveIn: '2026-01-01', flexibleTiming: true },
weightingProfile: { area: 0.3, location: 0.3, budget: 0.2, timing: 0.1, prestige: 0, accessibility: 0, expansionPotential: 0, flexibility: 0, visibility: 0, footfall: 0, talentAccess: 0, esg: 0, taxEnvironment: 0 },
confidenceInCriteria: 0.8,
})
const id = created.data.id
await needService.remove(id)
@@ -7,8 +7,8 @@ describe('propertyService', () => {
const result = await propertyService.getAll()
expect(Array.isArray(result.data)).toBe(true)
expect(result.data.length).toBeGreaterThan(0)
expect(result.meta.total).toBe(result.data.length)
expect(result.meta.hasMore).toBe(false)
expect(result.meta!.total).toBe(result.data.length)
expect(result.meta!.hasMore).toBe(false)
})
it('each property has required fields', async () => {
@@ -62,9 +62,14 @@ describe('propertyService', () => {
assetType: 'LOGISTICS' as const,
resultType: 'VERIFIED_PORTFOLIO' as const,
location: { city: 'Basel', country: 'CH' as const },
address: { street: 'Teststr.', houseNumber: '1', postalCode: '4000', city: 'Basel', country: 'CH' },
areaSqm: 500,
rentPricePerSqm: 120,
availabilityDate: '2025-01-01',
availabilityStatus: 'AVAILABLE_NOW' as const,
sourceType: 'DIRECT',
confidenceScore: 0.8,
dataQuality: { score: 0.8, missingCriticalFields: [], missingOptionalFields: [], freshness: 'FRESH' as const, warnings: [] },
organizationId: 'org-test',
}
const result = await propertyService.create(input)
@@ -73,8 +73,8 @@ describe('MockAIService.generateFollowUpQuestions', () => {
assetType: 'OFFICE',
areaRange: { min: 300, max: 600 },
preferredLocations: ['Zürich'],
budgetRange: { maxPerSqm: 400 },
timing: { earliestMoveIn: '2025-09-01' },
budgetRange: { maxPerSqm: 400, currency: 'CHF' },
timing: { earliestMoveIn: '2025-09-01', latestMoveIn: '2026-09-01', flexibleTiming: false },
mustHaveCriteria: ['ÖV-Anbindung'],
}
const result = await run(MockAIService.generateFollowUpQuestions(criteria))
@@ -116,7 +116,7 @@ describe('MockAIService provenance', () => {
describe('MockAIService.summarizeTradeOffs', () => {
it('returns LOW risk when no HIGH severity trade-offs', async () => {
const tradeoffs: TradeOffInput[] = [
{ concern: 'Lage', severity: 'LOW', mitigation: 'Umgebung gut erschlossen' },
{ criterion: 'Lage', concern: 'Lage', severity: 'LOW', mitigation: 'Umgebung gut erschlossen' },
]
const result = await run(MockAIService.summarizeTradeOffs(tradeoffs))
expect(result.data.overallRisk).toBe('LOW')
@@ -124,7 +124,7 @@ describe('MockAIService.summarizeTradeOffs', () => {
it('returns MEDIUM risk when exactly 1 HIGH severity trade-off', async () => {
const tradeoffs: TradeOffInput[] = [
{ concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' },
{ criterion: 'Fläche', concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' },
]
const result = await run(MockAIService.summarizeTradeOffs(tradeoffs))
expect(result.data.overallRisk).toBe('MEDIUM')
@@ -132,8 +132,8 @@ describe('MockAIService.summarizeTradeOffs', () => {
it('returns HIGH risk when 2+ HIGH severity trade-offs', async () => {
const tradeoffs: TradeOffInput[] = [
{ concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' },
{ concern: 'Budget weit überschritten', severity: 'HIGH', mitigation: '' },
{ criterion: 'Fläche', concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' },
{ criterion: 'Budget', concern: 'Budget weit überschritten', severity: 'HIGH', mitigation: '' },
]
const result = await run(MockAIService.summarizeTradeOffs(tradeoffs))
expect(result.data.overallRisk).toBe('HIGH')
@@ -155,6 +155,7 @@ describe('MockAIService.generateMatchExplanation', () => {
propertyCity: 'Zürich',
positiveFactors: [{ criterion: 'Lage', explanation: 'Zentrale Lage', weight: 0.3 }],
negativeFactors: [{ criterion: 'Preis', explanation: 'Etwas über Budget', weight: 0.2 }],
needSummary: 'Bürofläche 200400 m² in Zürich',
}
it('generates STARK headline for score >= 78', async () => {
-5
View File
@@ -4,16 +4,11 @@ import type { UnifiedMatchResult } from '../../../domain/unifiedResult'
import type {
IAIService,
AIResponse,
DecisionBrief,
ComparisonSummary,
CriteriaExtractionResult,
OfferEmailPayload,
MatchExplanationInput,
MatchExplanation,
TradeOffInput,
TradeOffSummary,
DataQualityInput,
DataQualitySummary,
MarketSignalClassification,
} from '../IAIService'
import { mockProvenance } from '../IAIService'
@@ -316,7 +316,7 @@ export const OpenRouterAIService: IAIService = {
preferredLocations: ai.preferredLocations,
budgetRange: ai.budgetRange ?? undefined,
timing: ai.timing
? { ...ai.timing, latestMoveIn: ai.timing.latestMoveIn ?? undefined }
? { ...ai.timing, earliestMoveIn: ai.timing.earliestMoveIn ?? '', flexibleTiming: ai.timing.flexibleTiming ?? false }
: undefined,
mustHaveCriteria: ai.mustHaveCriteria,
}
+1 -1
View File
@@ -219,7 +219,7 @@ export const aiTraceStore = new AITraceStore()
// window.__aiTraces.stats() → summary statistics
// window.__aiTraces.clear() → clear all
if (import.meta.env.DEV && typeof window !== 'undefined') {
;(window as Record<string, unknown>).__aiTraces = aiTraceStore
;(window as unknown as Record<string, unknown>).__aiTraces = aiTraceStore
}
// ── Helpers ───────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -175,8 +175,8 @@ export const matchService = {
rentPricePerSqm: p.rentPricePerSqm,
matchScore: score,
imageUrl: p.images?.[0],
reasons: (match?.positiveFactors ?? []).slice(0, 3).map(f => f.explanation ?? f.label ?? '').filter(Boolean),
topUncertainty: (match?.tradeoffs ?? match?.tradeOffs ?? [])[0]?.description
reasons: (match?.positiveFactors ?? []).slice(0, 3).map(f => f.explanation ?? f.criterion ?? '').filter(Boolean),
topUncertainty: (match?.tradeoffs ?? match?.tradeOffs ?? [])[0]?.concern
?? (match?.negativeFactors ?? [])[0]?.explanation
?? undefined,
}))
+5
View File
@@ -6,22 +6,27 @@ const PROFILES: Record<string, WeightProfile> = {
OFFICE: {
area: 0.20, location: 0.25, budget: 0.20, timing: 0.15,
prestige: 0.10, accessibility: 0.05, expansionPotential: 0.03, flexibility: 0.02,
visibility: 0, talentAccess: 0, footfall: 0, esg: 0, taxEnvironment: 0,
},
LOGISTICS: {
area: 0.30, location: 0.20, budget: 0.20, timing: 0.15,
prestige: 0.02, accessibility: 0.08, expansionPotential: 0.03, flexibility: 0.02,
visibility: 0, talentAccess: 0, footfall: 0, esg: 0, taxEnvironment: 0,
},
RETAIL: {
area: 0.15, location: 0.30, budget: 0.20, timing: 0.10,
prestige: 0.12, accessibility: 0.08, expansionPotential: 0.03, flexibility: 0.02,
visibility: 0, talentAccess: 0, footfall: 0, esg: 0, taxEnvironment: 0,
},
PRODUCTION: {
area: 0.30, location: 0.20, budget: 0.20, timing: 0.15,
prestige: 0.02, accessibility: 0.07, expansionPotential: 0.04, flexibility: 0.02,
visibility: 0, talentAccess: 0, footfall: 0, esg: 0, taxEnvironment: 0,
},
DEFAULT: {
area: 0.25, location: 0.25, budget: 0.20, timing: 0.15,
prestige: 0.07, accessibility: 0.05, expansionPotential: 0.02, flexibility: 0.01,
visibility: 0, talentAccess: 0, footfall: 0, esg: 0, taxEnvironment: 0,
},
}
+6
View File
@@ -0,0 +1,6 @@
{
"buildCommand": "npx vite build",
"outputDirectory": "dist",
"framework": null,
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}