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:
@@ -1,13 +1,14 @@
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Box, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'
|
||||
import { Box, Button, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { ExternalLink, MapPin, MessageSquare } from 'lucide-react'
|
||||
import { ExternalLink, MapPin, MessageSquare, Send, MessagesSquare } from 'lucide-react'
|
||||
import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay'
|
||||
import { HeatBadge } from '../shared'
|
||||
import type { PipelineItem } from '../../domain/pipeline'
|
||||
import { STAGES, RESULT_TYPE_META } from './pipelineConstants'
|
||||
import { detailPath } from './pipelineUtils'
|
||||
import { useInquiryStore } from '../../stores/inquiryStore'
|
||||
|
||||
// ── DraggableCard ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,8 +26,9 @@ export function DraggableCard({
|
||||
onChatClick?: (e: React.MouseEvent) => void
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: item.id })
|
||||
const stageConfig = STAGES.find(s => s.key === item.stage)!
|
||||
const stageConfig = STAGES.find(s => s.key === item.stage) ?? STAGES[0]
|
||||
const path = detailPath(item)
|
||||
|
||||
const style = !isDragOverlay ? {
|
||||
@@ -87,7 +89,7 @@ export function DraggableCard({
|
||||
</Tooltip>
|
||||
)}
|
||||
{path && (
|
||||
<Tooltip title="Objekt öffnen">
|
||||
<Tooltip title="Match öffnen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); navigate(path) }}
|
||||
@@ -135,6 +137,57 @@ export function DraggableCard({
|
||||
{item.notes}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Row 6: Anfrage / Konversation */}
|
||||
{!isDragOverlay && (
|
||||
<Box sx={{ mt: 1, pt: 0.75, borderTop: '1px solid #f1f5f9' }}>
|
||||
{item.stage !== 'SAVED' || item.inquiryId ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<MessagesSquare size={11} />}
|
||||
fullWidth
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
navigate(`/demand/anfragen?inquiry=${item.inquiryId}`)
|
||||
}}
|
||||
sx={{
|
||||
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
||||
borderColor: '#1e3a5f', color: '#1e3a5f',
|
||||
'&:hover': { bgcolor: '#eff6ff', borderColor: '#1e3a5f' },
|
||||
}}
|
||||
>
|
||||
Zur Konversation
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<Send size={11} />}
|
||||
fullWidth
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openInquiryDialog({
|
||||
propertyTitle: item.title,
|
||||
location: item.propertyAddress ?? item.location,
|
||||
areaLabel: item.areaLabel,
|
||||
rentLabel: item.rentLabel,
|
||||
matchScore: item.matchScore,
|
||||
pipelineItemId: item.id,
|
||||
})
|
||||
}}
|
||||
sx={{
|
||||
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
||||
bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' },
|
||||
}}
|
||||
>
|
||||
Anfrage senden
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,19 +4,17 @@ export { RESULT_TYPE_META } from '../../lib/ds'
|
||||
// ── Stage config ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const STAGES = [
|
||||
{ key: 'SAVED' as PipelineStage, label: 'Gemerkt', color: '#475569', bgColor: '#f8fafc' },
|
||||
{ key: 'DISCOVERED' as PipelineStage, label: 'Entdeckt', color: '#0369a1', bgColor: '#f0f9ff' },
|
||||
{ key: 'QUALIFIED' as PipelineStage, label: 'Qualifiziert', color: '#1e3a5f', bgColor: '#eff6ff' },
|
||||
{ key: 'VISITED' as PipelineStage, label: 'Besichtigt', color: '#d97706', bgColor: '#fffbeb' },
|
||||
{ key: 'NEGOTIATION' as PipelineStage, label: 'Verhandlung', color: '#7c3aed', bgColor: '#faf5ff' },
|
||||
{ key: 'CLOSED_WON' as PipelineStage, label: 'Gewonnen', color: '#1a7a4a', bgColor: '#f0fdf4' },
|
||||
{ key: 'CLOSED_LOST' as PipelineStage, label: 'Abgelehnt', color: '#c0392b', bgColor: '#fef2f2' },
|
||||
{ key: 'SAVED' as PipelineStage, label: 'Interessiert', color: '#475569', bgColor: '#f8fafc' },
|
||||
{ key: 'CONTACTED' as PipelineStage, label: 'Angefragt', color: '#0369a1', bgColor: '#f0f9ff' },
|
||||
{ key: 'VISITED' as PipelineStage, label: 'Besichtigt', color: '#d97706', bgColor: '#fffbeb' },
|
||||
{ key: 'NEGOTIATION' as PipelineStage, label: 'Verhandlung', color: '#7c3aed', bgColor: '#faf5ff' },
|
||||
{ key: 'CLOSED_WON' as PipelineStage, label: 'Gewonnen', color: '#1a7a4a', bgColor: '#f0fdf4' },
|
||||
{ key: 'CLOSED_LOST' as PipelineStage, label: 'Abgelehnt', color: '#c0392b', bgColor: '#fef2f2' },
|
||||
] as const
|
||||
|
||||
export const NEXT_STAGE: Partial<Record<PipelineStage, { key: PipelineStage; label: string }>> = {
|
||||
SAVED: { key: 'DISCOVERED', label: 'Als entdeckt markieren' },
|
||||
DISCOVERED: { key: 'QUALIFIED', label: 'Qualifizieren' },
|
||||
QUALIFIED: { key: 'VISITED', label: 'Besichtigung planen' },
|
||||
SAVED: { key: 'CONTACTED', label: 'Anfrage senden' },
|
||||
CONTACTED: { key: 'VISITED', label: 'Besichtigung planen' },
|
||||
VISITED: { key: 'NEGOTIATION', label: 'Verhandlung starten' },
|
||||
NEGOTIATION: { key: 'CLOSED_WON', label: 'Als gewonnen markieren' },
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { PipelineItem } from '../../domain/pipeline'
|
||||
export { matchScoreHex as scoreColor } from '../../lib/utils'
|
||||
|
||||
export function detailPath(item: PipelineItem): string | null {
|
||||
// propertyId is always stable across sessions — prefer it
|
||||
// Both 'match-XXX' (static mock) and 'm__...' (deterministic dynamic) IDs are stable across sessions
|
||||
const isStableMatchId = item.matchId?.startsWith('match-') || item.matchId?.startsWith('m__')
|
||||
if (isStableMatchId) return `/demand/results/${item.matchId}`
|
||||
if (item.propertyId) return `/demand/property/${item.propertyId}`
|
||||
// matchId / UUID only works in the same session (matchStore is ephemeral)
|
||||
if (item.matchId) return `/demand/results/${item.matchId}`
|
||||
if (item.id.startsWith('match-')) return `/demand/results/${item.id}`
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user