diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2ae1f03..8ba92fe 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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/**)" ] } } diff --git a/.vercel/README.txt b/.vercel/README.txt new file mode 100644 index 0000000..525d8ce --- /dev/null +++ b/.vercel/README.txt @@ -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. diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..25a7cf2 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_0UhtBM4Jg83OXPDkzhOM6x0gYfEY","orgId":"team_Quh2pScdkVSiGHkdwh6ntKZ5","projectName":"property-match"} \ No newline at end of file diff --git a/src/components/anfragencenter/InquiryReplyComposer.tsx b/src/components/anfragencenter/InquiryReplyComposer.tsx index 9eb6ed3..27f9f8c 100644 --- a/src/components/anfragencenter/InquiryReplyComposer.tsx +++ b/src/components/anfragencenter/InquiryReplyComposer.tsx @@ -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, diff --git a/src/components/anfragencenter/OfferCreationWizard.tsx b/src/components/anfragencenter/OfferCreationWizard.tsx index b9d8760..31db3e0 100644 --- a/src/components/anfragencenter/OfferCreationWizard.tsx +++ b/src/components/anfragencenter/OfferCreationWizard.tsx @@ -104,7 +104,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose } return ( - + Angebot erstellen @@ -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 }} /> showToast('PDF wird heruntergeladen…', 'info')} onAttach={() => { const label = `Angebot_${property?.title ?? 'Objekt'}.pdf` diff --git a/src/components/anfragencenter/OfferWizard.tsx b/src/components/anfragencenter/OfferWizard.tsx index 846852d..74c8869 100644 --- a/src/components/anfragencenter/OfferWizard.tsx +++ b/src/components/anfragencenter/OfferWizard.tsx @@ -48,12 +48,14 @@ export function OfferWizard() { fullScreen={fullScreen} maxWidth="lg" fullWidth - PaperProps={{ - sx: { - height: fullScreen ? '100vh' : '85vh', - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', + slotProps={{ + paper: { + sx: { + height: fullScreen ? '100vh' : '85vh', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }, }, }} > diff --git a/src/components/anfragencenter/PreparationWizard.tsx b/src/components/anfragencenter/PreparationWizard.tsx index dd7b538..9e1975e 100644 --- a/src/components/anfragencenter/PreparationWizard.tsx +++ b/src/components/anfragencenter/PreparationWizard.tsx @@ -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 ( - + Vorbereitung starten diff --git a/src/components/anfragencenter/ReportObjectFieldSelector.tsx b/src/components/anfragencenter/ReportObjectFieldSelector.tsx index 5449bb7..44265ed 100644 --- a/src/components/anfragencenter/ReportObjectFieldSelector.tsx +++ b/src/components/anfragencenter/ReportObjectFieldSelector.tsx @@ -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 ( } sx={{ minHeight: 40, '& .MuiAccordionSummary-content': { my: 0.5 } }}> diff --git a/src/components/compare/CompareMetricRow.tsx b/src/components/compare/CompareMetricRow.tsx new file mode 100644 index 0000000..889caf4 --- /dev/null +++ b/src/components/compare/CompareMetricRow.tsx @@ -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 ( + + {label} + {cells.map((cell, i) => ( + {cell} + ))} + + ) +}) diff --git a/src/components/layout/appShellConfig.ts b/src/components/layout/appShellConfig.ts index ce136bb..37f7d88 100644 --- a/src/components/layout/appShellConfig.ts +++ b/src/components/layout/appShellConfig.ts @@ -12,6 +12,7 @@ import { MessageSquare, Kanban, BellRing, + Settings, } from 'lucide-react' // --------------------------------------------------------------------------- @@ -68,6 +69,17 @@ export const WORKSPACE_CONFIG: Record = { { 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 diff --git a/src/components/match-card/FutureAvailabilityCard.tsx b/src/components/match-card/FutureAvailabilityCard.tsx index 8fdf7fb..bd2a3d5 100644 --- a/src/components/match-card/FutureAvailabilityCard.tsx +++ b/src/components/match-card/FutureAvailabilityCard.tsx @@ -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 }) { ) -} +}) diff --git a/src/components/match-card/MatchCardCompact.tsx b/src/components/match-card/MatchCardCompact.tsx index 01ee8d7..57c76c9 100644 --- a/src/components/match-card/MatchCardCompact.tsx +++ b/src/components/match-card/MatchCardCompact.tsx @@ -182,7 +182,7 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o - {vm.tradeoffs[0].description} + {vm.tradeoffs[0].concern} )} diff --git a/src/components/match-card/index.ts b/src/components/match-card/index.ts index cc6f354..6e7a581 100644 --- a/src/components/match-card/index.ts +++ b/src/components/match-card/index.ts @@ -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, diff --git a/src/components/match-detail/MatchDetailPropertySections.tsx b/src/components/match-detail/MatchDetailPropertySections.tsx index 1cb3a50..1a9bc6e 100644 --- a/src/components/match-detail/MatchDetailPropertySections.tsx +++ b/src/components/match-detail/MatchDetailPropertySections.tsx @@ -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['data']> diff --git a/src/components/match-detail/MatchDetailScoreBreakdown.tsx b/src/components/match-detail/MatchDetailScoreBreakdown.tsx new file mode 100644 index 0000000..83e7af2 --- /dev/null +++ b/src/components/match-detail/MatchDetailScoreBreakdown.tsx @@ -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['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 ── */} + + + {/* ── Full analysis (hidden by default) ── */} + {showFullAnalysis && ( + <> + + + {!isFuture && property && } + + {!isFuture && property?.images?.[0] && property?.location?.coordinates && ( + + + Standort + + {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} + + + + + )} + + + + {isFuture && } + + + )} + + ) +}) diff --git a/src/components/match-detail/PropertyDetailPublicSections.tsx b/src/components/match-detail/PropertyDetailPublicSections.tsx index 8fd0d5c..6fc4804 100644 --- a/src/components/match-detail/PropertyDetailPublicSections.tsx +++ b/src/components/match-detail/PropertyDetailPublicSections.tsx @@ -11,7 +11,6 @@ import { ExternalLink, Info, Layers, - ShieldCheck, Tag, Train, TrendingUp, diff --git a/src/components/match-detail/index.ts b/src/components/match-detail/index.ts index 932bde9..11aadff 100644 --- a/src/components/match-detail/index.ts +++ b/src/components/match-detail/index.ts @@ -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' diff --git a/src/components/new-listing/AreaDetailsSection.tsx b/src/components/new-listing/AreaDetailsSection.tsx index 7c8ef55..1ea4058 100644 --- a/src/components/new-listing/AreaDetailsSection.tsx +++ b/src/components/new-listing/AreaDetailsSection.tsx @@ -37,13 +37,13 @@ export function AreaDetailsSection({ onAreaSqmChange(e.target.value)} - size="small" type="number" inputProps={{ min: 1 }} fullWidth + size="small" type="number" slotProps={{ htmlInput: { min: 1 } }} fullWidth /> onRentPerSqmChange(e.target.value)} - size="small" type="number" inputProps={{ min: 1 }} fullWidth + size="small" type="number" slotProps={{ htmlInput: { min: 1 } }} fullWidth /> onParkingChange(e.target.value)} - size="small" type="number" inputProps={{ min: 0 }} fullWidth + size="small" type="number" slotProps={{ htmlInput: { min: 0 } }} fullWidth /> 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 /> diff --git a/src/components/onboarding/WelcomeDialog.tsx b/src/components/onboarding/WelcomeDialog.tsx index 12a1d0e..b1f0276 100644 --- a/src/components/onboarding/WelcomeDialog.tsx +++ b/src/components/onboarding/WelcomeDialog.tsx @@ -146,7 +146,6 @@ export function WelcomeDialog() { open={open} maxWidth="sm" fullWidth - disableEscapeKeyDown > {SLIDES[activeStep]} diff --git a/src/components/pipeline/index.ts b/src/components/pipeline/index.ts new file mode 100644 index 0000000..ab751b0 --- /dev/null +++ b/src/components/pipeline/index.ts @@ -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' diff --git a/src/components/supply/BerichtDialog.tsx b/src/components/supply/BerichtDialog.tsx index 9e24b3a..bb59842 100644 --- a/src/components/supply/BerichtDialog.tsx +++ b/src/components/supply/BerichtDialog.tsx @@ -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 */} diff --git a/src/components/supply/PropertyDetailOverview.tsx b/src/components/supply/PropertyDetailOverview.tsx index d2fafbb..859f3a4 100644 --- a/src/components/supply/PropertyDetailOverview.tsx +++ b/src/components/supply/PropertyDetailOverview.tsx @@ -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' diff --git a/src/components/supply/ReminderDetailDrawer.tsx b/src/components/supply/ReminderDetailDrawer.tsx index 0da2c8c..6f87fa2 100644 --- a/src/components/supply/ReminderDetailDrawer.tsx +++ b/src/components/supply/ReminderDetailDrawer.tsx @@ -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() diff --git a/src/components/supply/ReminderFeed.tsx b/src/components/supply/ReminderFeed.tsx index 9170b3d..de57ed1 100644 --- a/src/components/supply/ReminderFeed.tsx +++ b/src/components/supply/ReminderFeed.tsx @@ -95,7 +95,7 @@ export function ReminderFeed() { if (viewMode === 'card') { return ( - {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 ( diff --git a/src/domain/property.ts b/src/domain/property.ts index 6aacc62..6eacb9b 100644 --- a/src/domain/property.ts +++ b/src/domain/property.ts @@ -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 diff --git a/src/domain/unifiedResult.ts b/src/domain/unifiedResult.ts index 6e546e8..ea3cf3a 100644 --- a/src/domain/unifiedResult.ts +++ b/src/domain/unifiedResult.ts @@ -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 diff --git a/src/features/matching/__tests__/matchCardAdapter.test.ts b/src/features/matching/__tests__/matchCardAdapter.test.ts index 1f46662..f7f87b9 100644 --- a/src/features/matching/__tests__/matchCardAdapter.test.ts +++ b/src/features/matching/__tests__/matchCardAdapter.test.ts @@ -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) }) diff --git a/src/hooks/__tests__/useNeeds.test.ts b/src/hooks/__tests__/useNeeds.test.ts index 21ee014..3bbfdda 100644 --- a/src/hooks/__tests__/useNeeds.test.ts +++ b/src/hooks/__tests__/useNeeds.test.ts @@ -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') diff --git a/src/hooks/__tests__/useProperties.test.ts b/src/hooks/__tests__/useProperties.test.ts index 2cb40cb..ed8a349 100644 --- a/src/hooks/__tests__/useProperties.test.ts +++ b/src/hooks/__tests__/useProperties.test.ts @@ -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)) diff --git a/src/hooks/useMatchDetailData.ts b/src/hooks/useMatchDetailData.ts index ba61d9f..1481d9f 100644 --- a/src/hooks/useMatchDetailData.ts +++ b/src/hooks/useMatchDetailData.ts @@ -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({ + const { data: property = null } = useQuery, 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({ + const { data: need = null } = useQuery, Error, Need | null>({ queryKey: ['need', match?.needId], queryFn: () => needService.getById(match!.needId), enabled: !!match?.needId, diff --git a/src/hooks/useProperties.ts b/src/hooks/useProperties.ts index a2b088d..c46d3db 100644 --- a/src/hooks/useProperties.ts +++ b/src/hooks/useProperties.ts @@ -13,6 +13,7 @@ interface PropertyFilter { minAreaSqm?: number maxRentPerSqm?: number organizationId?: string + sourceType?: string } export function useProperties(filter?: PropertyFilter) { diff --git a/src/hooks/useSchattenmarktSignals.ts b/src/hooks/useSchattenmarktSignals.ts index 7b1bca2..9abb7f0 100644 --- a/src/hooks/useSchattenmarktSignals.ts +++ b/src/hooks/useSchattenmarktSignals.ts @@ -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)) } diff --git a/src/hooks/useUnifiedResults.ts b/src/hooks/useUnifiedResults.ts index 4d6b7c2..2d13738 100644 --- a/src/hooks/useUnifiedResults.ts +++ b/src/hooks/useUnifiedResults.ts @@ -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) { diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts index f44a452..c35b793 100644 --- a/src/lib/permissions.ts +++ b/src/lib/permissions.ts @@ -67,6 +67,10 @@ const WORKSPACE_ROLES: Record = { UserRole.PROPERTY_MANAGER, UserRole.DEMAND_USER, ], + [WorkspaceType.OPERATIONS]: [ + UserRole.SUPER_ADMIN, + UserRole.ORGANIZATION_ADMIN, + ], } // ── Core Functions ──────────────────────────────────────────────────────────── diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts index 4eacb63..ba1fb5c 100644 --- a/src/mock-data/properties.ts +++ b/src/mock-data/properties.ts @@ -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, diff --git a/src/pages/demand/Anfragen.tsx b/src/pages/demand/Anfragen.tsx index 9256b51..33aca59 100644 --- a/src/pages/demand/Anfragen.tsx +++ b/src/pages/demand/Anfragen.tsx @@ -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() { setSearch(e.target.value)} - InputProps={{ startAdornment: }} + slotProps={{ input: { startAdornment: } }} sx={{ mb: 1.25 }} /> diff --git a/src/pages/demand/Compare.tsx b/src/pages/demand/Compare.tsx index 2c0fc2d..fc847f4 100644 --- a/src/pages/demand/Compare.tsx +++ b/src/pages/demand/Compare.tsx @@ -41,7 +41,6 @@ export default function Compare() { overallWinnerIdx, bestScoreIdx, worstConfIdx, - dqScores, worstDQIdx, missingCriticalCounts, maxMissingCritical, diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx index 662638a..2727366 100644 --- a/src/pages/demand/MatchDetail.tsx +++ b/src/pages/demand/MatchDetail.tsx @@ -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 ── */} - - - {/* ── Full analysis (hidden by default) ── */} - {showFullAnalysis && ( - <> - - - {!isFuture && property && } - - {!isFuture && property?.images?.[0] && property?.location?.coordinates && ( - - - Standort - - {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} - - - - - )} - - - - {isFuture && } - - - )} + {/* ── Full analysis toggle + expanded panels ── */} + ) diff --git a/src/pages/supply/MarketIntelligence.tsx b/src/pages/supply/MarketIntelligence.tsx index bdbe14a..f00102c 100644 --- a/src/pages/supply/MarketIntelligence.tsx +++ b/src/pages/supply/MarketIntelligence.tsx @@ -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, diff --git a/src/pages/supply/SupplyDashboard.tsx b/src/pages/supply/SupplyDashboard.tsx index abd1f6c..bfb81f4 100644 --- a/src/pages/supply/SupplyDashboard.tsx +++ b/src/pages/supply/SupplyDashboard.tsx @@ -53,7 +53,7 @@ export default function SupplyDashboard() { {/* KPI strip */} - + {/* Starke Matches */} 0 ? DS_SURFACE.success.bg : 'white' }}> diff --git a/src/services/__tests__/futureSignalService.test.ts b/src/services/__tests__/futureSignalService.test.ts index 69c6675..22233ee 100644 --- a/src/services/__tests__/futureSignalService.test.ts +++ b/src/services/__tests__/futureSignalService.test.ts @@ -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 () => { diff --git a/src/services/__tests__/matchService.test.ts b/src/services/__tests__/matchService.test.ts index 793284e..12f9fb1 100644 --- a/src/services/__tests__/matchService.test.ts +++ b/src/services/__tests__/matchService.test.ts @@ -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 () => { diff --git a/src/services/__tests__/needService.test.ts b/src/services/__tests__/needService.test.ts index 0e653f4..d044849 100644 --- a/src/services/__tests__/needService.test.ts +++ b/src/services/__tests__/needService.test.ts @@ -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) diff --git a/src/services/__tests__/propertyService.test.ts b/src/services/__tests__/propertyService.test.ts index a632ce1..bccdf37 100644 --- a/src/services/__tests__/propertyService.test.ts +++ b/src/services/__tests__/propertyService.test.ts @@ -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) diff --git a/src/services/ai/__tests__/mockAIService.test.ts b/src/services/ai/__tests__/mockAIService.test.ts index 5b5cd5c..db2c09a 100644 --- a/src/services/ai/__tests__/mockAIService.test.ts +++ b/src/services/ai/__tests__/mockAIService.test.ts @@ -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 200–400 m² in Zürich', } it('generates STARK headline for score >= 78', async () => { diff --git a/src/services/ai/mock/MockAIService.ts b/src/services/ai/mock/MockAIService.ts index cb0ec7e..4bf678c 100644 --- a/src/services/ai/mock/MockAIService.ts +++ b/src/services/ai/mock/MockAIService.ts @@ -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' diff --git a/src/services/ai/openrouter/OpenRouterAIService.ts b/src/services/ai/openrouter/OpenRouterAIService.ts index c7c09b3..1686c8b 100644 --- a/src/services/ai/openrouter/OpenRouterAIService.ts +++ b/src/services/ai/openrouter/OpenRouterAIService.ts @@ -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, } diff --git a/src/services/ai/tracing.ts b/src/services/ai/tracing.ts index a418385..380942e 100644 --- a/src/services/ai/tracing.ts +++ b/src/services/ai/tracing.ts @@ -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).__aiTraces = aiTraceStore + ;(window as unknown as Record).__aiTraces = aiTraceStore } // ── Helpers ─────────────────────────────────────────────────────────────────── diff --git a/src/services/matchService.ts b/src/services/matchService.ts index 508997c..1b0c1a6 100644 --- a/src/services/matchService.ts +++ b/src/services/matchService.ts @@ -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, })) diff --git a/src/services/weightingService.ts b/src/services/weightingService.ts index 1b6ca24..787b607 100644 --- a/src/services/weightingService.ts +++ b/src/services/weightingService.ts @@ -6,22 +6,27 @@ const PROFILES: Record = { 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, }, } diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..3f6464c --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "buildCommand": "npx vite build", + "outputDirectory": "dist", + "framework": null, + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +}