feat: Grundriss-Feature, Listenansicht mit Bildern, Gewerbe-Label, Image-Pool

- Grundriss (FloorPlanSection): PropertyUnit.floorPlanUrl?, Property.floorPlanUrl?;
  FloorPlanSection in MatchDetail + PropertyDetail; FloorPlanUrlSection in NewListing-Formular
- Listenansicht (MatchCardCompact): horizontales Layout mit 120px Bildstreifen,
  Score-Badge, Asset-Label-Overlay, alle 3 grünen Punkte, Anfrage-Button
- Light Industrial → "Gewerbe" überall (NeedInput, NeedCardPreview, CriteriaReviewPanel,
  newListingConstants, MyListings, propertyHelpers)
- "Zum Originalinserat"-Button nur bei Maison-Work-Objekten
- Image-Pool: propertyImageResolver mit sequentiellem Pool-Index (keine doppelten Bilder),
  nur Innenaufnahmen, rotate()-Trick für Sub-Pools
- Overlay-Labels (Objekttyp + Stadtteil) in LocationPreview + IntelligenceMatchCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 22:12:43 +02:00
parent 3ab6eeccf2
commit e95490eb72
39 changed files with 685 additions and 147 deletions
+3 -1
View File
@@ -14,6 +14,7 @@ import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection'
import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble'
import { AnfragenInquiryItem } from '../../components/demand/AnfragenInquiryItem'
import { INQUIRY_STATUS_META, DS_COLORS, DS_TEXT, DS_BG, DS_BORDER, DS_SURFACE } from '../../lib/ds'
import { useSessionStore } from '../../stores/sessionStore'
// ── Config ────────────────────────────────────────────────────────────────────
@@ -35,6 +36,7 @@ export default function Anfragen() {
const preselectedId = searchParams.get('inquiry')
const { currentUser } = useSessionStore()
const storeInquiries = useInquiryStore(s => s.sentInquiries)
const [inquiries, setInquiries] = useState(mockDemandInquiries)
const allInquiries = [...storeInquiries, ...inquiries]
@@ -293,7 +295,7 @@ export default function Anfragen() {
{/* Thread */}
<Box ref={threadRef} sx={{ flex: 1, overflowY: 'auto', px: { xs: 2, md: 3 }, py: 2.5, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{selected.thread.map(msg => (
<AnfragenMessageBubble key={msg.id} msg={msg} />
<AnfragenMessageBubble key={msg.id} msg={msg} currentUserName={currentUser?.name ?? undefined} />
))}
</Box>
+2 -1
View File
@@ -66,7 +66,8 @@ export default function Results() {
}
}, [activeNeedIdFromNav, queryClient])
const { data: results = [], isLoading, error } = useUnifiedResults(effectiveNeedId)
const needsLoading = allNeeds.length === 0 && !activeNeedIdFromNav
const { data: results = [], isLoading, error } = useUnifiedResults(needsLoading ? undefined : effectiveNeedId)
const isStaff = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN'
+2 -2
View File
@@ -1,11 +1,11 @@
import type { PipelineStage } from '../../domain/pipeline'
export const STAGE_ORDER: PipelineStage[] = [
'SAVED', 'DISCOVERED', 'QUALIFIED', 'VISITED', 'NEGOTIATION', 'CLOSED_WON', 'CLOSED_LOST',
'SAVED', 'CONTACTED', 'VISITED', 'NEGOTIATION', 'CLOSED_WON', 'CLOSED_LOST',
]
export const STAGE_LABELS: Record<string, string> = {
SAVED: 'Gemerkt', DISCOVERED: 'Entdeckt', QUALIFIED: 'Qualifiziert',
SAVED: 'Interessiert', CONTACTED: 'Angefragt',
VISITED: 'Besichtigt', NEGOTIATION: 'Verhandlung', CLOSED_WON: 'Gewonnen', CLOSED_LOST: 'Abgelehnt',
}
+1 -1
View File
@@ -22,7 +22,7 @@ import type { Property } from '../../domain/property'
const ASSET_LABELS: Record<string, string> = {
OFFICE: 'Büro',
RETAIL: 'Einzelhandel',
LIGHT_INDUSTRIAL: 'Leichtindustrie',
LIGHT_INDUSTRIAL: 'Gewerbe',
LOGISTICS: 'Logistik',
PRODUCTION: 'Produktion',
MIXED: 'Gemischt',
+6
View File
@@ -9,6 +9,7 @@ import {
SoftFactorsSection,
TechnicalDetailsSection,
ImageUrlSection,
FloorPlanUrlSection,
ContactSection,
CreatedScreen,
} from '../../components/new-listing'
@@ -92,6 +93,11 @@ export default function NewListing() {
onRemove={form.removeImage}
/>
<FloorPlanUrlSection
floorPlanUrl={form.floorPlanUrl}
onFloorPlanUrlChange={form.setFloorPlanUrl}
/>
<ContactSection
name={form.contactName} onNameChange={form.setContactName}
email={form.contactEmail} onEmailChange={form.setContactEmail}
+1 -1
View File
@@ -1,7 +1,7 @@
export const ASSET_TYPE_LABELS: Record<string, string> = {
OFFICE: 'Büro',
RETAIL: 'Einzelhandel',
LIGHT_INDUSTRIAL: 'Leichtindustrie',
LIGHT_INDUSTRIAL: 'Gewerbe',
LOGISTICS: 'Logistik',
PRODUCTION: 'Produktion',
MIXED: 'Gemischt',
+3 -1
View File
@@ -18,11 +18,12 @@ export function buildCreatePropertyInput(fields: {
parking: string
ceilingHeight: string
images: string[]
floorPlanUrl: string
}): CreatePropertyInput {
const {
assetType, street, houseNumber, postalCode, city,
areaSqm, rentPerSqm, availableFrom, description,
softLevels, floor, fitOut, parking, ceilingHeight, images,
softLevels, floor, fitOut, parking, ceilingHeight, images, floorPlanUrl,
} = fields
const sf = {
@@ -66,6 +67,7 @@ export function buildCreatePropertyInput(fields: {
softFactors: sf,
hardFacts: hf,
images: images.length > 0 ? images : undefined,
floorPlanUrl: floorPlanUrl.trim() || undefined,
dataQuality: {
score: 1.0,
missingCriticalFields: [],