feat: F015 shortlist management

3-panel Shortlists page with left list, center detail, right decision brief panel. AddToShortlistDialog wired into result feed cards and match detail. Full DRAFT→REVIEW_READY→FINALIZED workflow, AI-generated decision brief draft, duplicate prevention, and compare-view integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-16 21:10:06 +02:00
parent 2753d1717c
commit 65130efca7
22 changed files with 1079 additions and 195 deletions
+45 -1
View File
@@ -5,6 +5,45 @@ import type { AssetType } from '../domain/enums'
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../domain/needBuilder'
import type { UnifiedMatchResult } from '../domain/unifiedResult'
// ── Decision Brief ────────────────────────────────────────────────────────────
export interface DecisionBrief {
id: string
shortlistId: string
summary: string
sections: { title: string; body: string }[]
generatedAt: string
isDraft: true
}
function buildMockDecisionBrief(shortlistId: string): DecisionBrief {
return {
id: crypto.randomUUID(),
shortlistId,
summary: 'Die Shortlist enthält qualitativ hochwertige Matches mit starker Standortübereinstimmung. Die verfügbaren Flächen decken den Bedarf gut ab. Zwei Objekte eignen sich als Erstbesichtigungen.',
sections: [
{
title: 'Zusammenfassung der Objekte',
body: 'Die Shortlist umfasst mehrere Objekte aus dem verifizierten Portfolio. Die Matchscores liegen zwischen 74 und 88, was auf eine gute bis sehr gute Übereinstimmung mit den Suchkriterien hinweist.',
},
{
title: 'Standortbewertung',
body: 'Die Mehrheit der Objekte befindet sich in bevorzugten Lagen. Die ÖV-Anbindung ist bei allen Objekten als gut bis sehr gut einzustufen.',
},
{
title: 'Budgetanalyse',
body: 'Die Mietpreise liegen im budgetkonformen Bereich. Keine der Optionen überschreitet das maximale Budget pro m².',
},
{
title: 'Empfohlene nächste Schritte',
body: '1. Besichtigung der Top-2-Objekte vereinbaren. 2. Detaillierte Flächenpläne anfordern. 3. Vertragskonditionen prüfen lassen.',
},
],
generatedAt: new Date().toISOString(),
isDraft: true,
}
}
// ── Compare Summary ───────────────────────────────────────────────────────────
export interface ComparisonSummary {
@@ -284,7 +323,7 @@ function mockParseNeed(input: string): ParseNeedResult {
timing,
mustHaveCriteria,
infrastructureRequirements: [],
accessibilityRequirements: mustHaveCriteria.includes('Gute ÖV-Anbindung') ? ['ÖV-Anbindung'] : [],
accessibilityRequirements: [],
prestigeImportance,
flexibilityNeed: lower.includes('flexibel') ? 'HIGH' : 'MEDIUM',
expansionPotential: lower.includes('wachstum') || lower.includes('expansion'),
@@ -377,4 +416,9 @@ export const aiService = {
const result = mockParseNeed(JSON.stringify(criteria))
return { data: result.followUpQuestionCandidates }
},
async generateDecisionBrief(shortlistId: string): Promise<ItemResponse<DecisionBrief>> {
await new Promise(r => setTimeout(r, 1800))
return { data: buildMockDecisionBrief(shortlistId) }
},
}
+5 -5
View File
@@ -1,6 +1,6 @@
import { MockupShortlistProvider } from '../provider/MockupShortlistProvider'
import type { ShortlistFilters } from '../provider/IShortlistProvider'
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput } from '../domain/shortlist'
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist'
import type { ListResponse, ItemResponse } from './types'
const provider = MockupShortlistProvider
@@ -22,12 +22,12 @@ export const shortlistService = {
const data = await provider.update(id, input)
return { data }
},
async addItem(id: string, propertyId: string, note?: string): Promise<ItemResponse<Shortlist>> {
const data = await provider.addItem(id, propertyId, note)
async addItem(id: string, item: ShortlistItemInput): Promise<ItemResponse<Shortlist>> {
const data = await provider.addItem(id, item)
return { data }
},
async removeItem(id: string, propertyId: string): Promise<ItemResponse<Shortlist>> {
const data = await provider.removeItem(id, propertyId)
async removeItem(id: string, resultId: string): Promise<ItemResponse<Shortlist>> {
const data = await provider.removeItem(id, resultId)
return { data }
},
async remove(id: string): Promise<ItemResponse<void>> {