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
@@ -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,
},
}