Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5259579f85 | |||
| beecf754d5 | |||
| 6e67c698ce | |||
| 3dcac2dcb6 | |||
| 5035445d2e | |||
| 017835b083 | |||
| d945fb1ab6 | |||
| f783cf6881 | |||
| cf9089b5a2 | |||
| b0f675c319 | |||
| da3b5b55cc | |||
| 76e52dae1d | |||
| 4c79d9f969 | |||
| ec5d567dae | |||
| e64ae15d17 | |||
| ee229f8f4f | |||
| d771029633 | |||
| 20dcc6b245 | |||
| bd9fd98d78 | |||
| bae0cd31d5 | |||
| 5c886193d5 | |||
| e6c20958a5 | |||
| 884bb468b8 | |||
| 29ee98a300 | |||
| 161b2d609a | |||
| da50f3b5ea |
+17
-1
@@ -13,7 +13,23 @@
|
||||
"Bash(git pull *)",
|
||||
"Bash(node scripts/check-tokens.js)",
|
||||
"Bash(echo \"EXIT:$?\")",
|
||||
"Bash(echo \"EXIT_CODE:$?\")"
|
||||
"Bash(echo \"EXIT_CODE:$?\")",
|
||||
"Bash(Get-ChildItem \"c:\\\\Users\\\\beni_\\\\AppData\\\\Local\\\\Temp\\\\docx-gen\\\\node_modules\\\\docx\" -Recurse -Name)",
|
||||
"Bash(start \"C:\\\\Users\\\\beni_\\\\OneDrive\\\\Desktop\\\\property-match\\\\Aenderungsprotokoll_Meeting_26052026.docx\")",
|
||||
"Bash(start http://localhost:5176)",
|
||||
"Bash(start http://localhost:5176/demand/ai-search)",
|
||||
"Bash(node -e ' *)",
|
||||
"Skill(run)",
|
||||
"Skill(run:*)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5173)",
|
||||
"Bash(powershell.exe -Command \"Start-Process 'http://localhost:5173'\")",
|
||||
"Bash(findstr \"LISTENING\")",
|
||||
"Bash(findstr \":517\")",
|
||||
"Bash(findstr \"LISTEN\")",
|
||||
"Bash(findstr \":5\")",
|
||||
"Bash(echo \"Exit: $?\")",
|
||||
"Bash(npx vercel *)",
|
||||
"Bash(Start-Process \"http://localhost:5180\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,23 @@
|
||||
"Bash(git commit *)",
|
||||
"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/**)"
|
||||
"Read(//c/Users/beni_/.claude/projects/c--Users-beni--OneDrive-Desktop-property-match/8e388ddd-e02e-47fe-8bb9-cdb8164c9fc3/tool-results/**)",
|
||||
"Bash(pandoc --version)",
|
||||
"Bash(npm list *)",
|
||||
"Read(//c/Program Files/LibreOffice/program/**)",
|
||||
"Read(//c/Program Files/Microsoft Office/**)",
|
||||
"Read(//c/Program Files \\(x86\\)/**)",
|
||||
"Bash(mkdir -p /tmp/docx-gen)",
|
||||
"Read(//tmp/**)",
|
||||
"Bash(npm init *)",
|
||||
"Bash(node generate.js)",
|
||||
"Bash(timeout 8 bash -c \"until curl -s http://localhost:5173 > /dev/null; do sleep 1; done\")",
|
||||
"Bash(dir \"c:\\\\Users\\\\beni_\\\\OneDrive\\\\Desktop\\\\property-match\\\\src\" -Depth 2)",
|
||||
"Bash(start \"\" \"http://localhost:5178/demand/results\")",
|
||||
"Bash(start \"\" \"http://localhost:5178/supply/properties\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"c:\\Users\\beni_\\AppData\\Local\\Temp\\docx-gen"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const TYPE_LABELS: Record<AIOutputType, string> = {
|
||||
}
|
||||
|
||||
const TYPE_COLORS: Record<AIOutputType, string> = {
|
||||
NEED_PARSE: '#1e3a5f',
|
||||
NEED_PARSE: '#152642',
|
||||
FOLLOW_UP_QUESTIONS: '#0891b2',
|
||||
MATCH_EXPLANATION: '#4f46e5',
|
||||
COMPARE_SUMMARY: '#1a7a4a',
|
||||
|
||||
@@ -61,7 +61,7 @@ export function AIReviewActionToolbar({ output, onUpdateStatus, onCopyJson, isSu
|
||||
variant="outlined"
|
||||
onClick={onCopyJson}
|
||||
startIcon={<Copy size={13} />}
|
||||
sx={{ textTransform: 'none', fontSize: '0.75rem', color: '#64748b', borderColor: '#e2e8f0', '&:hover': { bgcolor: '#f8fafc' } }}
|
||||
sx={{ textTransform: 'none', fontSize: '0.75rem', color: '#64748b', borderColor: '#e8e7e4', '&:hover': { bgcolor: '#f8fafc' } }}
|
||||
>
|
||||
JSON
|
||||
</Button>
|
||||
|
||||
@@ -93,18 +93,18 @@ export function ActiveInquiriesTab() {
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a' }}>
|
||||
Anfragen
|
||||
</Typography>
|
||||
<Box sx={{ px: 1, py: 0.125, borderRadius: 1, bgcolor: '#1e3a5f', color: 'white', fontWeight: 600, fontSize: '0.7rem' }}>
|
||||
<Box sx={{ px: 1, py: 0.125, borderRadius: 1, bgcolor: '#152642', color: 'white', fontWeight: 600, fontSize: '0.7rem' }}>
|
||||
{inquiries.length}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title="Listenansicht">
|
||||
<IconButton size="small" onClick={() => setView('list')} sx={{ bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', color: view === 'list' ? '#1e3a5f' : '#64748b' }}>
|
||||
<IconButton size="small" onClick={() => setView('list')} sx={{ bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', color: view === 'list' ? '#152642' : '#64748b' }}>
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Kartenansicht">
|
||||
<IconButton size="small" onClick={() => setView('grid')} sx={{ bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', color: view === 'grid' ? '#1e3a5f' : '#64748b' }}>
|
||||
<IconButton size="small" onClick={() => setView('grid')} sx={{ bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', color: view === 'grid' ? '#152642' : '#64748b' }}>
|
||||
<LayoutGrid size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -20,12 +20,12 @@ export function InquiryCard({ inquiry, selected, onClick }: InquiryCardProps) {
|
||||
p: 2,
|
||||
borderRadius: 1.5,
|
||||
border: '1px solid',
|
||||
borderColor: selected ? '#1e3a5f' : '#e2e8f0',
|
||||
borderColor: selected ? '#152642' : '#e8e7e4',
|
||||
bgcolor: selected ? '#f1f5f9' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
'&:hover': {
|
||||
borderColor: selected ? '#1e3a5f' : '#94a3b8',
|
||||
borderColor: selected ? '#152642' : '#94a3b8',
|
||||
boxShadow: '0 2px 6px rgba(15,23,42,0.06)',
|
||||
},
|
||||
display: 'flex',
|
||||
@@ -96,19 +96,9 @@ export function InquiryCard({ inquiry, selected, onClick }: InquiryCardProps) {
|
||||
{formatInquiryDate(inquiry.createdAt)}
|
||||
</Typography>
|
||||
{inquiry.matchScore !== undefined && (
|
||||
<Box
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#e0e7ff',
|
||||
color: '#3730a3',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
Match {inquiry.matchScore}%
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 700, color: '#152642' }}>
|
||||
{inquiry.matchScore}%
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function InquiryChat({
|
||||
fontSize: '0.75rem',
|
||||
borderColor: '#cbd5e1',
|
||||
color: '#475569',
|
||||
'&:hover': { borderColor: '#1e3a5f', color: '#1e3a5f', bgcolor: '#f0f4f8' },
|
||||
'&:hover': { borderColor: '#152642', color: '#152642', bgcolor: '#f0f4f8' },
|
||||
}}
|
||||
>
|
||||
Angebot erstellen
|
||||
@@ -56,6 +56,7 @@ export function InquiryChat({
|
||||
<InquiryReplyComposer
|
||||
inquiryId={inquiry.id}
|
||||
defaultSubject={inquiry.subject}
|
||||
tenantName={inquiry.tenantName}
|
||||
pendingAttachment={pendingAttachment}
|
||||
onPendingAttachmentConsumed={onPendingAttachmentConsumed}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Typography } from '@mui/material'
|
||||
import { ClipboardList } from 'lucide-react'
|
||||
import { ArrowRight, Building2, ClipboardList, MapPin, Ruler } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { useInquiryById, useMarkThreadAsRead } from '../../hooks/useInquiries'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { InquiryChat } from './InquiryChat'
|
||||
import { RelatedPropertyCardPanel } from './RelatedPropertyCardPanel'
|
||||
import type { Attachment } from '../../domain/inquiry'
|
||||
@@ -12,7 +14,9 @@ interface InquiryDetailPanelProps {
|
||||
|
||||
export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) {
|
||||
const { data: inquiry, isLoading } = useInquiryById(inquiryId)
|
||||
const { data: property } = usePropertyById(inquiry?.propertyId ?? '')
|
||||
const markRead = useMarkThreadAsRead()
|
||||
const navigate = useNavigate()
|
||||
const [preparationOpen, setPreparationOpen] = useState(false)
|
||||
const [offerWizardOpen, setOfferWizardOpen] = useState(false)
|
||||
const [pendingAttachment, setPendingAttachment] = useState<Attachment | null>(null)
|
||||
@@ -43,10 +47,13 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) {
|
||||
}
|
||||
|
||||
const isLatentInquiry = !!inquiry.needId
|
||||
const propertyImage = property?.images?.[0]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
|
||||
{/* Header: tenant + subject + action */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2.5,
|
||||
@@ -85,26 +92,92 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) {
|
||||
variant="outlined"
|
||||
startIcon={<ClipboardList size={14} />}
|
||||
onClick={() => setPreparationOpen(true)}
|
||||
sx={{ textTransform: 'none', whiteSpace: 'nowrap', borderColor: '#1e3a5f', color: '#1e3a5f', '&:hover': { bgcolor: '#f0f4f8' } }}
|
||||
sx={{ textTransform: 'none', whiteSpace: 'nowrap', borderColor: '#152642', color: '#152642', '&:hover': { bgcolor: '#f0f4f8' } }}
|
||||
>
|
||||
Vorbereitung starten
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, overflow: 'hidden' }}>
|
||||
{/* Property context bar */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
bgcolor: '#f8fafc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
bgcolor: '#e8e7e4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{propertyImage ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={propertyImage}
|
||||
alt={property?.title}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<Building2 size={18} color="#94a3b8" />
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.85rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: 1.3 }}
|
||||
>
|
||||
{property?.title ?? '—'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#64748b' }}>
|
||||
<MapPin size={11} style={{ flexShrink: 0 }} />
|
||||
<Typography variant="caption" sx={{ fontSize: '0.72rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{property?.location.city}
|
||||
{property?.areaSqm ? ` · ${property.areaSqm.toLocaleString('de-CH')} m²` : ''}
|
||||
{property?.rentPricePerSqm ? ` · CHF ${property.rentPricePerSqm}/m²/J.` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowRight size={12} />}
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{ textTransform: 'none', fontSize: '0.75rem', color: '#152642', whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||
>
|
||||
Objekt ansehen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Chat (flex: 1) */}
|
||||
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
||||
<InquiryChat
|
||||
inquiry={inquiry}
|
||||
onCreateOffer={() => setOfferWizardOpen(true)}
|
||||
pendingAttachment={pendingAttachment}
|
||||
onPendingAttachmentConsumed={() => setPendingAttachment(null)}
|
||||
/>
|
||||
<Box sx={{ width: { xs: '100%', md: 300 }, flexShrink: 0, borderTop: { xs: '1px solid #e2e8f0', md: 'none' }, maxHeight: { xs: 280, md: 'none' }, overflowY: 'auto' }}>
|
||||
<RelatedPropertyCardPanel
|
||||
propertyId={inquiry.propertyId}
|
||||
inquiryId={inquiry.id}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Bottom: weitere passende Objekte */}
|
||||
<Box sx={{ flexShrink: 0, borderTop: '1px solid #e2e8f0' }}>
|
||||
<RelatedPropertyCardPanel
|
||||
propertyId={inquiry.propertyId}
|
||||
inquiryId={inquiry.id}
|
||||
compact
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -17,11 +17,10 @@ export function InquiryListRow({ inquiry, selected, onClick }: InquiryListRowPro
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1.5,
|
||||
py: 1,
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: selected ? '#1e3a5f' : hasUnread ? '#2563eb' : 'transparent',
|
||||
bgcolor: selected ? '#f1f5f9' : 'white',
|
||||
borderLeft: selected ? '4px solid #2563eb' : `3px solid ${hasUnread ? '#2563eb' : 'transparent'}`,
|
||||
bgcolor: selected ? '#eff6ff' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.15s, border-color 0.15s',
|
||||
'&:hover': { bgcolor: selected ? '#f1f5f9' : '#f8fafc' },
|
||||
@@ -30,7 +29,7 @@ export function InquiryListRow({ inquiry, selected, onClick }: InquiryListRowPro
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: hasUnread ? 700 : 600, color: '#0f172a', fontSize: '0.85rem' }}
|
||||
sx={{ fontWeight: selected || hasUnread ? 700 : 600, color: selected ? '#1d4ed8' : '#0f172a', fontSize: '0.85rem' }}
|
||||
>
|
||||
{inquiry.tenantName}
|
||||
{inquiry.tenantCompany ? ` · ${inquiry.tenantCompany}` : ''}
|
||||
@@ -76,20 +75,9 @@ export function InquiryListRow({ inquiry, selected, onClick }: InquiryListRowPro
|
||||
{propertyLabelFromId(inquiry.propertyId)}
|
||||
</Typography>
|
||||
{inquiry.matchScore !== undefined && (
|
||||
<Box
|
||||
sx={{
|
||||
ml: 'auto',
|
||||
px: 0.75,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#e0e7ff',
|
||||
color: '#3730a3',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ ml: 'auto', fontSize: '0.75rem', fontWeight: 700, color: '#152642' }}>
|
||||
{inquiry.matchScore}%
|
||||
</Box>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.7rem', mt: 0.5, display: 'block' }}>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function InquiryMessageBubble({ message }: InquiryMessageBubbleProps) {
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: '75%',
|
||||
bgcolor: isTenant ? '#f1f5f9' : '#1e3a5f',
|
||||
bgcolor: isTenant ? '#f1f5f9' : '#152642',
|
||||
color: isTenant ? '#0f172a' : 'white',
|
||||
px: 1.75,
|
||||
py: 1.25,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Send, Paperclip, X } from 'lucide-react'
|
||||
import { Paperclip, Send, Sparkles, X } from 'lucide-react'
|
||||
import { useSendInquiryReply } from '../../hooks/useInquiries'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import type { Attachment } from '../../domain/inquiry'
|
||||
@@ -16,6 +16,7 @@ import { formatFileSize } from './inquiryUtils'
|
||||
interface InquiryReplyComposerProps {
|
||||
inquiryId: string
|
||||
defaultSubject: string
|
||||
tenantName?: string
|
||||
onSent?: () => void
|
||||
pendingAttachment?: Attachment | null
|
||||
onPendingAttachmentConsumed?: () => void
|
||||
@@ -24,6 +25,7 @@ interface InquiryReplyComposerProps {
|
||||
export function InquiryReplyComposer({
|
||||
inquiryId,
|
||||
defaultSubject,
|
||||
tenantName,
|
||||
onSent,
|
||||
pendingAttachment,
|
||||
onPendingAttachmentConsumed,
|
||||
@@ -33,6 +35,7 @@ export function InquiryReplyComposer({
|
||||
)
|
||||
const [body, setBody] = useState('')
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([])
|
||||
const [isAIDraft, setIsAIDraft] = useState(false)
|
||||
|
||||
const sendReply = useSendInquiryReply()
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
@@ -49,6 +52,14 @@ export function InquiryReplyComposer({
|
||||
|
||||
const sending = sendReply.isPending
|
||||
|
||||
function handleAIDraft() {
|
||||
const salutation = tenantName ? `Sehr geehrte/r ${tenantName}` : 'Sehr geehrte Damen und Herren'
|
||||
setBody(
|
||||
`${salutation},\n\nVielen Dank für Ihre Anfrage. Gerne bestätigen wir, dass das Objekt zum gewünschten Zeitpunkt verfügbar ist.\n\nWir würden Ihnen gerne einen Besichtigungstermin vorschlagen — bitte teilen Sie uns Ihre Verfügbarkeit mit, damit wir einen passenden Termin finden können.\n\nFür Rückfragen stehen wir Ihnen jederzeit gerne zur Verfügung.\n\nMit freundlichen Grüssen`
|
||||
)
|
||||
setIsAIDraft(true)
|
||||
}
|
||||
|
||||
const handleAddMockAttachment = () => {
|
||||
const name = `Anhang_${attachments.length + 1}.pdf`
|
||||
setAttachments(prev => [
|
||||
@@ -82,6 +93,7 @@ export function InquiryReplyComposer({
|
||||
showToast('Antwort gesendet', 'success')
|
||||
setBody('')
|
||||
setAttachments([])
|
||||
setIsAIDraft(false)
|
||||
onSent?.()
|
||||
}
|
||||
|
||||
@@ -103,15 +115,26 @@ export function InquiryReplyComposer({
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{isAIDraft && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Sparkles size={12} color="#7c3aed" />
|
||||
<Typography variant="caption" sx={{ color: '#7c3aed', fontSize: '0.7rem', fontWeight: 500 }}>
|
||||
KI-Entwurf — bitte prüfen und anpassen
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label="Nachricht"
|
||||
value={body}
|
||||
onChange={e => setBody(e.target.value)}
|
||||
onChange={e => { setBody(e.target.value); setIsAIDraft(false) }}
|
||||
multiline
|
||||
rows={4}
|
||||
rows={7}
|
||||
placeholder="Antwort verfassen..."
|
||||
fullWidth
|
||||
sx={isAIDraft ? { '& .MuiOutlinedInput-root': { borderColor: '#ddd6fe' }, '& fieldset': { borderColor: '#ddd6fe !important' } } : {}}
|
||||
/>
|
||||
|
||||
{attachments.length > 0 && (
|
||||
@@ -149,15 +172,32 @@ export function InquiryReplyComposer({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Paperclip size={14} />}
|
||||
onClick={handleAddMockAttachment}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Anhang
|
||||
</Button>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 0.75 }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Sparkles size={13} />}
|
||||
onClick={handleAIDraft}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
color: '#7c3aed',
|
||||
borderColor: '#ddd6fe',
|
||||
'&:hover': { bgcolor: '#f5f3ff', borderColor: '#c4b5fd' },
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
KI-Entwurf
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Paperclip size={13} />}
|
||||
onClick={handleAddMockAttachment}
|
||||
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
||||
>
|
||||
Anhang
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
@@ -166,7 +206,7 @@ export function InquiryReplyComposer({
|
||||
}
|
||||
onClick={handleSend}
|
||||
disabled={sending || !body.trim()}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Antwort senden
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function LatentInquiriesTab() {
|
||||
<Box sx={{ px: 2, py: 1.5, borderTop: '1px solid #e2e8f0', flexShrink: 0 }}>
|
||||
<Box
|
||||
onClick={() => setMobileView('properties')}
|
||||
sx={{ p: 1.25, bgcolor: '#1e3a5f', color: 'white', borderRadius: 1, textAlign: 'center', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600 }}
|
||||
sx={{ p: 1.25, bgcolor: '#152642', color: 'white', borderRadius: 1, textAlign: 'center', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600 }}
|
||||
>
|
||||
Eigene Objekte anzeigen →
|
||||
</Box>
|
||||
|
||||
@@ -164,11 +164,11 @@ export function MockPdfPreview({ fields, fallbackFields, fieldSelections }: Mock
|
||||
{hasPropertyPage && (
|
||||
<>
|
||||
<Box sx={{ my: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Divider sx={{ flex: 1, borderColor: '#1e3a5f', borderWidth: 1 }} />
|
||||
<Divider sx={{ flex: 1, borderColor: '#152642', borderWidth: 1 }} />
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', whiteSpace: 'nowrap', letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||||
Seite 2 — Objektdetails
|
||||
</Typography>
|
||||
<Divider sx={{ flex: 1, borderColor: '#1e3a5f', borderWidth: 1 }} />
|
||||
<Divider sx={{ flex: 1, borderColor: '#152642', borderWidth: 1 }} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
|
||||
@@ -116,7 +116,7 @@ export function OfferChatComposer() {
|
||||
py: 0.75,
|
||||
}}
|
||||
>
|
||||
<FileText size={14} color="#1e3a5f" />
|
||||
<FileText size={14} color="#152642" />
|
||||
<Typography variant="caption" sx={{ fontSize: '0.8rem', fontWeight: 500 }}>
|
||||
{a.fileName}
|
||||
</Typography>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function OfferCreationPanel({ selectedCount, needId, needTitle }: OfferCr
|
||||
onClick={() => open(needId, needTitle)}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
fontWeight: 600,
|
||||
'&:hover': { bgcolor: '#16304d' },
|
||||
}}
|
||||
|
||||
@@ -255,7 +255,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
onClick={() => setStep(1)}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
@@ -264,7 +264,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setStep(2)}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
@@ -273,7 +273,7 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleGeneratePdf}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
PDF erstellen
|
||||
</Button>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function OfferFieldSelectionStep() {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setStep('pdf_review')}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
|
||||
@@ -179,7 +179,7 @@ export function OfferPdfReviewStep() {
|
||||
endIcon={markChecked.isPending ? <CircularProgress size={14} sx={{ color: 'white' }} /> : <Send size={14} />}
|
||||
onClick={handleNext}
|
||||
disabled={!pdfReady || markChecked.isPending}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
{markChecked.isPending ? 'Wird geprüft…' : 'Angebot senden →'}
|
||||
</Button>
|
||||
|
||||
@@ -141,7 +141,7 @@ export function OfferPropertySelectionStep() {
|
||||
endIcon={createDraft.isPending ? <CircularProgress size={14} sx={{ color: 'white' }} /> : <ArrowRight size={14} />}
|
||||
onClick={handleNext}
|
||||
disabled={selectedIds.length === 0 || createDraft.isPending}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
|
||||
@@ -18,8 +18,8 @@ export function OfferWizardPdfStep({ generating, progress, ready, draft, propert
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 3 }}>
|
||||
{generating ? (
|
||||
<>
|
||||
<CircularProgress size={48} sx={{ color: '#1e3a5f' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#1e3a5f' }}>
|
||||
<CircularProgress size={48} sx={{ color: '#152642' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#152642' }}>
|
||||
PDF wird generiert…
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
@@ -44,7 +44,7 @@ export function OfferWizardPdfStep({ generating, progress, ready, draft, propert
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ bgcolor: '#1e3a5f', px: 2, py: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{ bgcolor: '#152642', px: 2, py: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#ef4444' }} />
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#f59e0b' }} />
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#10b981' }} />
|
||||
@@ -53,7 +53,7 @@ export function OfferWizardPdfStep({ generating, progress, ready, draft, propert
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, p: 3 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#1e3a5f', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#152642', mb: 1 }}>
|
||||
Angebotsschreiben
|
||||
</Typography>
|
||||
{draft?.editableFields.slice(0, 3).map(f => (
|
||||
@@ -83,7 +83,7 @@ export function OfferWizardPdfStep({ generating, progress, ready, draft, propert
|
||||
variant="contained"
|
||||
startIcon={<Download size={16} />}
|
||||
onClick={onDownload}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Herunterladen
|
||||
</Button>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function OwnPropertyMatchList({ need }: OwnPropertyMatchListProps) {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Target size={14} color="#1e3a5f" />
|
||||
<Target size={14} color="#152642" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a' }}>
|
||||
Eigene Objekte
|
||||
</Typography>
|
||||
@@ -72,7 +72,7 @@ export function OwnPropertyMatchList({ need }: OwnPropertyMatchListProps) {
|
||||
px: 1,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
|
||||
@@ -159,8 +159,8 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 3 }}>
|
||||
{generating ? (
|
||||
<>
|
||||
<CircularProgress size={48} sx={{ color: '#1e3a5f' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#1e3a5f' }}>
|
||||
<CircularProgress size={48} sx={{ color: '#152642' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#152642' }}>
|
||||
Bericht wird erstellt…
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
@@ -234,7 +234,7 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
fullWidth
|
||||
startIcon={<Download size={16} />}
|
||||
onClick={() => showToast('PDF wird heruntergeladen…', 'info')}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Herunterladen
|
||||
</Button>
|
||||
@@ -271,7 +271,7 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
variant="contained"
|
||||
disabled={selectedIds.length === 0}
|
||||
onClick={handleGenerate}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
@@ -282,7 +282,7 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
disabled={finalizing}
|
||||
onClick={handleFinalize}
|
||||
startIcon={finalizing ? <CircularProgress size={14} sx={{ color: 'white' }} /> : undefined}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
|
||||
sx={{ textTransform: 'none', bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }}
|
||||
>
|
||||
Finalisieren
|
||||
</Button>
|
||||
|
||||
@@ -19,12 +19,12 @@ export function PublicNeedCard({ need, selected, onClick }: PublicNeedCardProps)
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
border: '1px solid',
|
||||
borderColor: selected ? '#1e3a5f' : '#e2e8f0',
|
||||
borderColor: selected ? '#152642' : '#e8e7e4',
|
||||
bgcolor: selected ? '#f1f5f9' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
'&:hover': {
|
||||
borderColor: selected ? '#1e3a5f' : '#94a3b8',
|
||||
borderColor: selected ? '#152642' : '#94a3b8',
|
||||
boxShadow: '0 2px 6px rgba(15,23,42,0.06)',
|
||||
},
|
||||
display: 'flex',
|
||||
|
||||
@@ -174,7 +174,7 @@ export function PublicNeedDetail({ need }: PublicNeedDetailProps) {
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#1e3a5f',
|
||||
color: '#152642',
|
||||
fontSize: '0.75rem',
|
||||
bgcolor: '#e0e7ff',
|
||||
px: 1,
|
||||
@@ -189,7 +189,7 @@ export function PublicNeedDetail({ need }: PublicNeedDetailProps) {
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
@@ -197,7 +197,7 @@ export function PublicNeedDetail({ need }: PublicNeedDetailProps) {
|
||||
sx={{
|
||||
width: `${pct}%`,
|
||||
height: '100%',
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
transition: 'width 0.3s',
|
||||
}}
|
||||
/>
|
||||
@@ -229,7 +229,7 @@ export function PublicNeedDetail({ need }: PublicNeedDetailProps) {
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
fontWeight: 600,
|
||||
'&:hover': { bgcolor: '#16304d' },
|
||||
}}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function PublicNeedList({ selectedNeedId, onSelect, fullWidth }: PublicNe
|
||||
px: 1,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Button, CircularProgress, Divider, Typography } from '@mui/material'
|
||||
import { ArrowRight, Building2, Calendar, MapPin, Ruler, Trophy } from 'lucide-react'
|
||||
import { ArrowRight, Building2, Calendar, MapPin, Ruler } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { useAdditionalMatchesForInquiry } from '../../hooks/useMatches'
|
||||
@@ -8,9 +8,10 @@ import type { AdditionalPropertyMatch } from '../../domain/additionalMatch'
|
||||
interface RelatedPropertyCardPanelProps {
|
||||
propertyId: string
|
||||
inquiryId: string
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPropertyCardPanelProps) {
|
||||
export function RelatedPropertyCardPanel({ propertyId, inquiryId, compact }: RelatedPropertyCardPanelProps) {
|
||||
const { data: property, isLoading } = usePropertyById(propertyId)
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
@@ -18,7 +19,7 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
isLoading: loadingMatches,
|
||||
} = useAdditionalMatchesForInquiry(inquiryId, { minScore: 80, excludePropertyId: propertyId })
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !compact) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
|
||||
<CircularProgress size={20} />
|
||||
@@ -26,6 +27,44 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
)
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Box sx={{ px: 1.5, pt: 1.25, pb: 1.5, bgcolor: 'white' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.65rem', display: 'block', mb: 1 }}
|
||||
>
|
||||
Weitere passende Objekte
|
||||
</Typography>
|
||||
|
||||
{loadingMatches ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 1.5 }}>
|
||||
<CircularProgress size={16} />
|
||||
</Box>
|
||||
) : additionalMatches.length === 0 ? (
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.72rem' }}>
|
||||
Keine weiteren Matches
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1.25,
|
||||
overflowX: 'auto',
|
||||
pb: 0.5,
|
||||
'&::-webkit-scrollbar': { height: 3 },
|
||||
'&::-webkit-scrollbar-thumb': { bgcolor: '#cbd5e1', borderRadius: 2 },
|
||||
}}
|
||||
>
|
||||
{additionalMatches.map(m => (
|
||||
<MatchSliderCard key={m.propertyId} match={m} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (!property) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
@@ -51,25 +90,22 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
Bezogenes Objekt
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
height: 140,
|
||||
aspectRatio: '3/2',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#e2e8f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundImage: image ? `url(${image})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
bgcolor: '#f4f3f0',
|
||||
}}
|
||||
>
|
||||
{!image && <Building2 size={32} color="#94a3b8" />}
|
||||
{image ? (
|
||||
<Box component="img" src={image} alt={property.title}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (
|
||||
<Box sx={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Building2 size={32} color="#94a3b8" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.95rem' }}>
|
||||
@@ -77,20 +113,20 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569', fontSize: '0.8125rem' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569' }}>
|
||||
<MapPin size={14} />
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>
|
||||
{property.location.city}
|
||||
{property.location.district ? `, ${property.location.district}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569', fontSize: '0.8125rem' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569' }}>
|
||||
<Ruler size={14} />
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>
|
||||
{property.areaSqm.toLocaleString('de-CH')} m² · CHF {property.rentPricePerSqm}/m²/Jahr
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569', fontSize: '0.8125rem' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#475569' }}>
|
||||
<Calendar size={14} />
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>
|
||||
Verfügbar ab {new Date(property.availabilityDate).toLocaleDateString('de-CH')}
|
||||
@@ -114,14 +150,10 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
Objekt ansehen
|
||||
</Button>
|
||||
|
||||
{/* Weitere Matches */}
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Trophy size={14} color="#d97706" />
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.68rem' }}>
|
||||
Weitere passende Objekte
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.68rem' }}>
|
||||
Weitere passende Objekte
|
||||
</Typography>
|
||||
|
||||
{loadingMatches ? (
|
||||
<CircularProgress size={16} sx={{ alignSelf: 'center' }} />
|
||||
@@ -140,74 +172,117 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope
|
||||
)
|
||||
}
|
||||
|
||||
function MatchSliderCard({ match }: { match: AdditionalPropertyMatch }) {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<Box
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
width: 168,
|
||||
border: '1px solid #e8e7e4',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
bgcolor: 'white',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
'&:hover': {
|
||||
borderColor: '#b0aead',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Image — 16:9 crop, compact supplementary card */}
|
||||
<Box sx={{ position: 'relative', height: 60, bgcolor: '#f4f3f0', overflow: 'hidden' }}>
|
||||
{match.imageUrl ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={match.imageUrl}
|
||||
alt={match.title}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Building2 size={16} color="#94a3b8" />
|
||||
</Box>
|
||||
)}
|
||||
{/* Score badge overlay */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
bgcolor: 'rgba(15,23,42,0.72)',
|
||||
color: 'white',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
borderRadius: '4px',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{match.matchScore}%
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ p: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mb: 0.2 }}>
|
||||
<Typography
|
||||
sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.75rem', lineHeight: 1.3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, mr: 0.5 }}
|
||||
>
|
||||
{match.title}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.7rem', fontWeight: 700, color: '#152642', flexShrink: 0 }}>
|
||||
{match.matchScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.68rem', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{match.location}
|
||||
</Typography>
|
||||
{match.reasons[0] && (
|
||||
<Typography variant="caption" sx={{ color: '#15803d', fontSize: '0.67rem', display: 'block', lineHeight: 1.4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', mt: 0.2 }}>
|
||||
+ {match.reasons[0]}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function AdditionalMatchCard({ match }: { match: AdditionalPropertyMatch }) {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'white',
|
||||
borderTop: '1px solid #f1f5f9',
|
||||
pt: 1,
|
||||
'&:first-of-type': { borderTop: 'none', pt: 0 },
|
||||
}}
|
||||
>
|
||||
{match.imageUrl && (
|
||||
<Box
|
||||
sx={{
|
||||
height: 70,
|
||||
backgroundImage: `url(${match.imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.75rem', lineHeight: 1.3 }}>
|
||||
{match.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
px: 0.75,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: match.matchScore >= 90 ? '#fef3c7' : '#e0e7ff',
|
||||
color: match.matchScore >= 90 ? '#92400e' : '#3730a3',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.65rem',
|
||||
flexShrink: 0,
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
{match.matchScore}%
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.7rem', display: 'block', mb: 0.5 }}>
|
||||
{match.location} · {match.areaSqm.toLocaleString('de-CH')} m² · CHF {match.rentPricePerSqm}/m²/Jahr
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mb: 0.25 }}>
|
||||
<Typography sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.8rem', lineHeight: 1.3, mr: 0.5 }}>
|
||||
{match.title}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 700, color: '#152642', flexShrink: 0 }}>
|
||||
{match.matchScore}%
|
||||
</Typography>
|
||||
{match.reasons.length > 0 && (
|
||||
<Box sx={{ mb: 0.5 }}>
|
||||
{match.reasons.map((r, i) => (
|
||||
<Typography key={i} variant="caption" sx={{ color: '#15803d', fontSize: '0.68rem', display: 'block', lineHeight: 1.4 }}>
|
||||
+ {r}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{match.topUncertainty && (
|
||||
<Typography variant="caption" sx={{ color: '#92400e', fontSize: '0.68rem', display: 'block', lineHeight: 1.4, mb: 0.25 }}>
|
||||
⚠ {match.topUncertainty}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowRight size={11} />}
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', mt: 0.5, p: 0, minWidth: 0, color: '#1e3a5f' }}
|
||||
>
|
||||
Ansehen
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.72rem', display: 'block', mb: 0.375 }}>
|
||||
{match.location} · {match.areaSqm.toLocaleString('de-CH')} m²
|
||||
</Typography>
|
||||
{match.reasons[0] && (
|
||||
<Typography variant="caption" sx={{ color: '#15803d', fontSize: '0.7rem', display: 'block', mb: 0.375, lineHeight: 1.4 }}>
|
||||
+ {match.reasons[0]}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowRight size={10} />}
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', p: 0, minWidth: 0, color: '#152642' }}
|
||||
>
|
||||
Ansehen
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@ export function SelectablePropertyMatchCard({
|
||||
p: 1.25,
|
||||
borderRadius: 1.5,
|
||||
border: '1px solid',
|
||||
borderColor: selected ? '#1e3a5f' : '#e2e8f0',
|
||||
borderColor: selected ? '#152642' : '#e8e7e4',
|
||||
bgcolor: selected ? '#eff6ff' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
'&:hover': {
|
||||
borderColor: selected ? '#1e3a5f' : '#94a3b8',
|
||||
borderColor: selected ? '#152642' : '#94a3b8',
|
||||
},
|
||||
display: 'flex',
|
||||
gap: 1.25,
|
||||
@@ -57,7 +57,7 @@ export function SelectablePropertyMatchCard({
|
||||
height: 40,
|
||||
flexShrink: 0,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -21,7 +21,7 @@ export function latentStatusBadge(status: 'public' | 'paused' | 'expired'): {
|
||||
} {
|
||||
if (status === 'public') return { label: 'Öffentlich', bg: '#dcfce7', fg: '#166534' }
|
||||
if (status === 'paused') return { label: 'Pausiert', bg: '#fed7aa', fg: '#9a3412' }
|
||||
return { label: 'Abgelaufen', bg: '#e2e8f0', fg: '#475569' }
|
||||
return { label: 'Abgelaufen', bg: '#e8e7e4', fg: '#475569' }
|
||||
}
|
||||
|
||||
// Deterministic pseudo-random match score from string IDs (range 60–96)
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ACTION_COLORS: Record<string, string> = {
|
||||
NAVIGATE: '#1e3a5f',
|
||||
NAVIGATE: '#152642',
|
||||
OPEN_REVIEW: '#7c3aed',
|
||||
ADD_TO_SHORTLIST: '#1a7a4a',
|
||||
REQUEST_DATA: '#d97706',
|
||||
|
||||
@@ -25,7 +25,7 @@ function MessageBubble({ message }: { message: AssistantMessage }) {
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
borderRadius: isUser ? '8px 8px 2px 8px' : '2px 8px 8px 8px',
|
||||
bgcolor: isUser ? '#1e3a5f' : '#f1f5f9',
|
||||
bgcolor: isUser ? '#152642' : '#f1f5f9',
|
||||
color: isUser ? 'white' : '#1e293b',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -237,7 +237,7 @@ export function GlobalAIAssistantDrawer() {
|
||||
color: 'white',
|
||||
flexShrink: 0,
|
||||
'&:hover': { bgcolor: '#4338ca' },
|
||||
'&:disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' },
|
||||
'&:disabled': { bgcolor: '#e8e7e4', color: '#94a3b8' },
|
||||
}}
|
||||
>
|
||||
<Send size={16} />
|
||||
|
||||
@@ -37,10 +37,10 @@ export function DemoRoleSwitcher() {
|
||||
sx={{
|
||||
fontSize: '0.7rem',
|
||||
height: 22,
|
||||
bgcolor: active ? '#1e3a5f' : 'transparent',
|
||||
bgcolor: active ? '#152642' : 'transparent',
|
||||
color: active ? '#fff' : 'text.secondary',
|
||||
border: '1px solid',
|
||||
borderColor: active ? '#1e3a5f' : 'divider',
|
||||
borderColor: active ? '#152642' : 'divider',
|
||||
'&:hover': { bgcolor: active ? '#162d4a' : 'rgba(0,0,0,0.04)' },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function SessionExpired() {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleRelogin}
|
||||
sx={{ mt: 1, bgcolor: '#1e3a5f', textTransform: 'none' }}
|
||||
sx={{ mt: 1, bgcolor: '#152642', textTransform: 'none' }}
|
||||
>
|
||||
Erneut anmelden
|
||||
</Button>
|
||||
|
||||
@@ -17,7 +17,7 @@ interface MetricCardProps {
|
||||
sx?: SxProps<Theme>
|
||||
}
|
||||
|
||||
export function MetricCard({ label, value, delta, icon, color = '#1e3a5f', sx }: MetricCardProps) {
|
||||
export function MetricCard({ label, value, delta, icon, color = '#152642', sx }: MetricCardProps) {
|
||||
return (
|
||||
<Card sx={{ p: 2.5, ...sx }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface SourceTypeBadgeProps {
|
||||
}
|
||||
|
||||
const CONFIG: Record<string, { bg: string; color: string; Icon: React.ElementType }> = {
|
||||
VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.1)', color: '#1e3a5f', Icon: ShieldCheck },
|
||||
VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.1)', color: '#152642', Icon: ShieldCheck },
|
||||
MAISON_WORK: { bg: 'rgba(3,105,161,0.1)', color: '#0369a1', Icon: Building2 },
|
||||
FUTURE_AVAILABILITY: { bg: 'rgba(124,58,237,0.1)', color: '#6d28d9', Icon: Sparkles },
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) {
|
||||
sx={{
|
||||
fontWeight: 700, lineHeight: 1.3, mb: 0.25,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { color: '#1e3a5f' },
|
||||
'&:hover': { color: '#152642' },
|
||||
}}
|
||||
onClick={() => navigate(`/demand/results/${item.matchId}`)}
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function CompareCriteriaCard({ compareItems, relevantCriteria, overallWin
|
||||
<TableCell sx={{ ...LABEL_SX, bgcolor: '#f8fafc', fontSize: 11 }}>Kriterium</TableCell>
|
||||
{compareItems.map(item => (
|
||||
<TableCell key={item.matchId} sx={{ ...DATA_SX, bgcolor: '#f8fafc' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#1e3a5f' }} noWrap>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#152642' }} noWrap>
|
||||
{getTitle(item)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function CompareEmptyState() {
|
||||
Fügen Sie 2–4 Ergebnisse aus dem Feed, Match Detail oder Match Center zum Vergleich hinzu.
|
||||
</Typography>
|
||||
<Button variant="contained" size="small" onClick={() => navigate('/demand/results')}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}>
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}>
|
||||
Zu den Suchergebnissen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -208,7 +208,7 @@ export function CompareTableBody({
|
||||
+ {fitOutMonthlyLabel} CHF/Monat (Ausbau ÷ {FITOUT_AMORTIZATION_YEARS} J.)
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#1e3a5f', borderTop: '1px solid #e2e8f0', pt: 0.5, mt: 0.25 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#152642', borderTop: '1px solid #e2e8f0', pt: 0.5, mt: 0.25 }}>
|
||||
= {totalMonthly.toLocaleString('de-CH')} CHF/Monat
|
||||
</Typography>
|
||||
{fitOutLabel && (
|
||||
|
||||
@@ -81,7 +81,7 @@ export function DataQualityPanel({ property }: DataQualityPanelProps) {
|
||||
Datenaktualisierung anfragen
|
||||
</Button>
|
||||
{q.missingCriticalFields.length > 0 && (
|
||||
<Button variant="contained" size="small" sx={{ textTransform: 'none', bgcolor: '#1e3a5f' }}>
|
||||
<Button variant="contained" size="small" sx={{ textTransform: 'none', bgcolor: '#152642' }}>
|
||||
Fehlende Felder ergänzen
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function DataQualityProgress({ property, compact = false }: DataQualityPr
|
||||
sx={{
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': { bgcolor: d.color },
|
||||
}}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ export function DataQualityProgress({ property, compact = false }: DataQualityPr
|
||||
sx={{
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': { bgcolor: d.color },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ProvenancePanel({ property: p }: ProvenancePanelProps) {
|
||||
sx={{
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': { bgcolor: color },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,7 @@ export function AISearchActionBar({ canProceed, isSearching, isSavingProfile, on
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
'&:hover': { bgcolor: '#162d4a' },
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
|
||||
@@ -48,7 +48,7 @@ export function AISearchSavePreview({
|
||||
<Box sx={{
|
||||
maxWidth: 720, mx: 'auto', mt: 2,
|
||||
p: 1.5, borderRadius: 1.5, border: '1px solid',
|
||||
borderColor: isAnonymous ? '#7c3aed' : '#e2e8f0',
|
||||
borderColor: isAnonymous ? '#7c3aed' : '#e8e7e4',
|
||||
bgcolor: isAnonymous ? '#f5f3ff' : '#f8fafc',
|
||||
transition: 'all 0.15s',
|
||||
}}>
|
||||
@@ -86,7 +86,7 @@ export function AISearchSavePreview({
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
endIcon={isSaving ? <CircularProgress size={16} color="inherit" /> : <Save size={16} />}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none' }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none' }}
|
||||
>
|
||||
{isSaving
|
||||
? 'Wird gespeichert…'
|
||||
|
||||
@@ -66,7 +66,7 @@ export function FollowUpPanel({ questions, answers, onAnswer, onContinue, onRepa
|
||||
disabled={requiredUnanswered.length > 0}
|
||||
onClick={onContinue}
|
||||
endIcon={<ArrowRight size={16} />}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Weiter zur Gewichtung
|
||||
</Button>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function NeedBuilderErrorState({ message, onRetry }: Props) {
|
||||
variant="contained"
|
||||
startIcon={<RotateCcw size={16} />}
|
||||
onClick={onRetry}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
|
||||
@@ -66,7 +66,7 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
{c.assetType && (
|
||||
<Chip label={ASSET_LABELS[c.assetType] ?? c.assetType} size="small" sx={{ bgcolor: '#1e3a5f', color: 'white' }} />
|
||||
<Chip label={ASSET_LABELS[c.assetType] ?? c.assetType} size="small" sx={{ bgcolor: '#152642', color: 'white' }} />
|
||||
)}
|
||||
<Chip label={isLowConfidence ? 'ENTWURF (needs_review)' : 'ENTWURF'} size="small" variant="outlined" />
|
||||
</Box>
|
||||
@@ -143,7 +143,7 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi
|
||||
variant="determinate"
|
||||
value={overallConfidence * 100}
|
||||
sx={{
|
||||
height: 6, borderRadius: 3, bgcolor: '#e2e8f0',
|
||||
height: 6, borderRadius: 3, bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
bgcolor: overallConfidence >= 0.7 ? '#1a7a4a' : overallConfidence >= 0.5 ? '#d97706' : '#c0392b',
|
||||
},
|
||||
@@ -171,7 +171,7 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={((weights[k] ?? 0) / maxWeight) * 100}
|
||||
sx={{ flex: 1, height: 6, borderRadius: 3, bgcolor: '#e2e8f0', '& .MuiLinearProgress-bar': { bgcolor: '#1e3a5f' } }}
|
||||
sx={{ flex: 1, height: 6, borderRadius: 3, bgcolor: '#e8e7e4', '& .MuiLinearProgress-bar': { bgcolor: '#152642' } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ width: 32, textAlign: 'right' }}>{pct}%</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Card, Chip, FormControlLabel, Slider, Stack, Switch, TextField, Typography } from '@mui/material'
|
||||
import { Box, Card, Chip, Slider, Stack, TextField, Typography } from '@mui/material'
|
||||
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
|
||||
import { AssetType } from '../../domain/enums'
|
||||
import { NeedExtendedRequirements } from './NeedExtendedRequirements'
|
||||
@@ -81,7 +81,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
clickable
|
||||
onClick={() => set({ ...c, assetType: c.assetType === opt.value ? undefined : opt.value })}
|
||||
sx={c.assetType === opt.value
|
||||
? { bgcolor: '#1e3a5f', color: 'white', '& .MuiChip-label': { color: 'white' } }
|
||||
? { bgcolor: '#152642', color: 'white', '& .MuiChip-label': { color: 'white' } }
|
||||
: {}}
|
||||
/>
|
||||
))}
|
||||
@@ -139,7 +139,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
})
|
||||
}}
|
||||
sx={c.preferredLocations?.includes(loc)
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#1e3a5f', color: 'white' }
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#152642', color: 'white' }
|
||||
: { fontSize: '0.68rem', height: 22, color: '#64748b', borderColor: '#cbd5e1' }
|
||||
}
|
||||
/>
|
||||
@@ -169,7 +169,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
<Box sx={{ mb: 2.5, px: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>Suchradius</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#1e3a5f' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#152642' }}>
|
||||
{c.searchRadius ?? 30} km
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -181,7 +181,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
step={1}
|
||||
onChange={(_, v) => set({ ...c, searchRadius: v as number })}
|
||||
size="small"
|
||||
sx={{ color: '#1e3a5f', '& .MuiSlider-markLabel': { fontSize: '0.62rem' } }}
|
||||
sx={{ color: '#152642', '& .MuiSlider-markLabel': { fontSize: '0.62rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -197,7 +197,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
clickable
|
||||
onClick={() => set({ ...c, budgetRange: { maxPerSqm: p.value, currency: 'CHF' } })}
|
||||
sx={c.budgetRange?.maxPerSqm === p.value
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#1e3a5f', color: 'white' }
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#152642', color: 'white' }
|
||||
: { fontSize: '0.68rem', height: 22, color: '#64748b', borderColor: '#cbd5e1' }
|
||||
}
|
||||
/>
|
||||
@@ -242,31 +242,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Anonymous search */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1, mt: 0.5, py: 1, px: 1.5, borderRadius: 1.5, bgcolor: c.isAnonymous ? 'rgba(124,58,237,0.07)' : '#f8fafc', border: `1px solid ${c.isAnonymous ? '#7c3aed' : '#e2e8f0'}` }}>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: c.isAnonymous ? '#7c3aed' : '#334155' }}>
|
||||
Anonyme Suche
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', fontSize: '0.65rem' }}>
|
||||
Firmenname wird nicht an Vermieter übermittelt
|
||||
</Typography>
|
||||
</Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={c.isAnonymous ?? false}
|
||||
onChange={e => set({ ...c, isAnonymous: e.target.checked })}
|
||||
sx={{ '& .MuiSwitch-thumb': { bgcolor: c.isAnonymous ? '#7c3aed' : undefined }, '& .Mui-checked + .MuiSwitch-track': { bgcolor: '#7c3aed' } }}
|
||||
/>
|
||||
}
|
||||
label=""
|
||||
sx={{ m: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<NeedExtendedRequirements criteria={c} onChange={set} />
|
||||
<NeedExtendedRequirements criteria={c} onChange={set} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function VoiceNeedInput({ text, onTextChange, onAiSubmit, isAnalyzing, is
|
||||
sx={{
|
||||
p: 3,
|
||||
border: '1px solid',
|
||||
borderColor: isRecording ? '#dc2626' : '#e2e8f0',
|
||||
borderColor: isRecording ? '#dc2626' : '#e8e7e4',
|
||||
transition: 'border-color 0.2s',
|
||||
bgcolor: isRecording ? '#fff5f5' : 'white',
|
||||
}}
|
||||
@@ -101,11 +101,11 @@ export function VoiceNeedInput({ text, onTextChange, onAiSubmit, isAnalyzing, is
|
||||
/>
|
||||
) : isAnalyzing ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircularProgress size={14} sx={{ color: '#1e3a5f' }} />
|
||||
<Typography variant="caption" sx={{ color: '#1e3a5f', fontWeight: 600 }}>Analysiert…</Typography>
|
||||
<CircularProgress size={14} sx={{ color: '#152642' }} />
|
||||
<Typography variant="caption" sx={{ color: '#152642', fontWeight: 600 }}>Analysiert…</Typography>
|
||||
</Box>
|
||||
) : isAutoGen && text ? (
|
||||
<Typography variant="caption" sx={{ color: '#1e3a5f', fontSize: 11 }}>⚡ auto-synchronisiert</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#152642', fontSize: 11 }}>⚡ auto-synchronisiert</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
@@ -154,7 +154,7 @@ export function VoiceNeedInput({ text, onTextChange, onAiSubmit, isAnalyzing, is
|
||||
size="small"
|
||||
disabled={isAnalyzing || !isSpeechSupported}
|
||||
title={isSpeechSupported ? 'Spracheingabe starten' : 'Spracheingabe nicht verfügbar'}
|
||||
sx={{ bgcolor: '#f1f5f9', color: '#475569', '&:hover': { bgcolor: '#e2e8f0' }, '&:disabled': { opacity: 0.35 } }}
|
||||
sx={{ bgcolor: '#f1f5f9', color: '#475569', '&:hover': { bgcolor: '#e8e7e4' }, '&:disabled': { opacity: 0.35 } }}
|
||||
>
|
||||
<Mic size={16} />
|
||||
</IconButton>
|
||||
|
||||
@@ -109,7 +109,7 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
||||
</Box>
|
||||
|
||||
<Card sx={{ p: 3 }}>
|
||||
<SliderGroup label="Harte Kriterien" keys={HARD_KEYS} raw={raw} onSlider={handleSlider} color="#1e3a5f" />
|
||||
<SliderGroup label="Harte Kriterien" keys={HARD_KEYS} raw={raw} onSlider={handleSlider} color="#152642" />
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<SliderGroup label="Weiche Kriterien" keys={SOFT_KEYS} raw={raw} onSlider={handleSlider} color="#7c3aed" />
|
||||
</Card>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function PriorityChipGroup({ label, options, value, onChange, exclusive =
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{options.map(opt => {
|
||||
const selected = value.includes(opt.value)
|
||||
const accent = opt.color ?? '#1e3a5f'
|
||||
const accent = opt.color ?? '#152642'
|
||||
return (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function FutureSignalFilterBar({ filters, onChange, totalCount, filteredC
|
||||
onClick={() => set({ signalType: filters.signalType === t ? '' : t })}
|
||||
sx={{
|
||||
fontWeight: filters.signalType === t ? 700 : 400,
|
||||
bgcolor: filters.signalType === t ? '#1e3a5f' : undefined,
|
||||
bgcolor: filters.signalType === t ? '#152642' : undefined,
|
||||
color: filters.signalType === t ? 'white' : undefined,
|
||||
}}
|
||||
/>
|
||||
@@ -131,7 +131,7 @@ export function FutureSignalFilterBar({ filters, onChange, totalCount, filteredC
|
||||
label={`${activeCount} Filter aktiv`}
|
||||
size="small"
|
||||
onDelete={() => onChange(DEFAULT_SIGNAL_FILTERS)}
|
||||
sx={{ bgcolor: '#eff6ff', color: '#1e3a5f' }}
|
||||
sx={{ bgcolor: '#eff6ff', color: '#152642' }}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
|
||||
@@ -13,7 +13,8 @@ export const SIDEBAR_BG = '#0f1923'
|
||||
export const DIVIDER_COLOR = 'rgba(255,255,255,0.08)'
|
||||
export const TEXT_MUTED = '#94a3b8'
|
||||
export const TEXT_WHITE = '#ffffff'
|
||||
export const NAV_ACTIVE_BG = 'rgba(255,255,255,0.12)'
|
||||
export const NAV_ACTIVE_BG = 'transparent'
|
||||
export const NAV_ACTIVE_ACCENT = '#b8975a'
|
||||
export const NAV_HOVER_BG = 'rgba(255,255,255,0.06)'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -188,16 +189,16 @@ export function Sidebar({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.25,
|
||||
px: collapsed ? 0 : 1.5,
|
||||
pl: collapsed ? 0 : isActive ? 1.25 : 1.5,
|
||||
pr: collapsed ? 0 : 1.5,
|
||||
py: 0.75,
|
||||
mx: 0.5,
|
||||
borderRadius: 1,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
backgroundColor: isActive ? NAV_ACTIVE_BG : 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: isActive ? NAV_ACTIVE_BG : NAV_HOVER_BG,
|
||||
},
|
||||
transition: 'background-color 0.15s ease',
|
||||
borderLeft: collapsed ? 'none' : `3px solid ${isActive ? NAV_ACTIVE_ACCENT : 'transparent'}`,
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': { backgroundColor: NAV_HOVER_BG },
|
||||
transition: 'border-color 0.15s ease',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -208,8 +209,9 @@ export function Sidebar({
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: isActive ? TEXT_WHITE : TEXT_MUTED,
|
||||
fontWeight: isActive ? 500 : 400,
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
fontSize: '0.875rem',
|
||||
letterSpacing: '-0.01em',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
@@ -263,7 +265,7 @@ export function Sidebar({
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: '0.75rem',
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#b8975a',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -31,10 +31,10 @@ export function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: Top
|
||||
<Box
|
||||
component="header"
|
||||
sx={{
|
||||
height: 56,
|
||||
height: 64,
|
||||
flexShrink: 0,
|
||||
backgroundColor: '#ffffff',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
borderBottom: '1px solid #e8e7e4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
@@ -51,12 +51,13 @@ export function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: Top
|
||||
<Chip
|
||||
label={config.label}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
backgroundColor: config.chipColor,
|
||||
color: '#ffffff',
|
||||
color: '#152642',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
height: 22,
|
||||
border: '1px solid #e8e7e4',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
|
||||
@@ -4,7 +4,7 @@ import { X } from 'lucide-react'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
|
||||
const TYPE_DOT: Record<string, string> = {
|
||||
VERIFIED_PORTFOLIO: '#1e3a5f',
|
||||
VERIFIED_PORTFOLIO: '#152642',
|
||||
MAISON_WORK: '#0369a1',
|
||||
FUTURE_AVAILABILITY: '#7c3aed',
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export function CompareTray() {
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/demand/compare')}
|
||||
sx={{ bgcolor: '#1e3a5f', textTransform: 'none', flexShrink: 0, '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', textTransform: 'none', flexShrink: 0, '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Vergleich starten
|
||||
</Button>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function OrganizationContextBadge() {
|
||||
variant="outlined"
|
||||
icon={<Building2 size={12} color="#64748b" />}
|
||||
label={currentUser.organizationName}
|
||||
sx={{ fontSize: '0.7rem', height: 22, color: '#64748b', borderColor: '#e2e8f0' }}
|
||||
sx={{ fontSize: '0.7rem', height: 22, color: '#64748b', borderColor: '#e8e7e4' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function PageHeader({
|
||||
sx,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<Box sx={{ px: 3, py: 2.5, borderBottom: '1px solid #e2e8f0', ...sx }}>
|
||||
<Box sx={{ px: 3, py: 2.5, borderBottom: '1px solid #e8e7e4', bgcolor: 'white', ...sx }}>
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<Breadcrumbs separator="/" sx={{ mb: 1 }}>
|
||||
{breadcrumbs.map((crumb) =>
|
||||
@@ -52,7 +52,7 @@ export function PageHeader({
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3 }}>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '1.125rem', lineHeight: 1.3, color: '#0f172a' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{badge}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function UserMenu() {
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleOpen} size="small" sx={{ p: 0 }}>
|
||||
<Avatar sx={{ width: 32, height: 32, fontSize: '0.75rem', bgcolor: '#1e3a5f' }}>
|
||||
<Avatar sx={{ width: 32, height: 32, fontSize: '0.75rem', bgcolor: '#152642' }}>
|
||||
{initials}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const WORKSPACE_CONFIG: Record<WorkspaceType, WorkspaceConfig> = {
|
||||
abbreviation: 'VW',
|
||||
icon: Building2,
|
||||
firstPath: '/supply/dashboard',
|
||||
chipColor: '#1e3a5f',
|
||||
chipColor: '#152642',
|
||||
navItems: [
|
||||
{ path: '/supply/dashboard', label: 'Übersicht', icon: LayoutDashboard },
|
||||
{ path: '/supply/properties', label: 'Meine Objekte', icon: Building2 },
|
||||
|
||||
@@ -32,7 +32,6 @@ export const FutureAvailabilityCard = memo(function FutureAvailabilityCard({ vm
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${borderColor}`,
|
||||
borderLeft: `3px solid ${accentColor}`,
|
||||
background: 'white',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
boxShadow: '0 1px 8px rgba(0,0,0,0.06)',
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { Box, Button, Chip, Divider, Typography } from '@mui/material'
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react'
|
||||
import { Box, Button, Chip, Typography } from '@mui/material'
|
||||
import { Building2, CheckCircle2, MapPin } from 'lucide-react'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useInquiryStore } from '../../stores/inquiryStore'
|
||||
import { LocationPreview } from '../shared/LocationPreview'
|
||||
import { HeatBadge } from '../shared/HeatBadge'
|
||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||
import { RESULT_TYPE_META, DS_TEXT, DS_BORDER } from '../../lib/ds'
|
||||
import { confidenceHex } from '../../lib/utils'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
import { FutureAvailabilityCard } from './FutureAvailabilityCard'
|
||||
|
||||
// ── Main component ────────────────────────────────────────────────────────────
|
||||
|
||||
interface Props {
|
||||
vm: MatchCardViewModel
|
||||
imageUrl?: string
|
||||
@@ -30,52 +26,80 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
|
||||
const { currentUser } = useSessionStore()
|
||||
const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
|
||||
const isPortfolioOwner = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN'
|
||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
|
||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#9a9a9a' }
|
||||
const isFuture = vm.resultType === 'FUTURE_AVAILABILITY'
|
||||
const confPct = Math.round(vm.confidenceScore * 100)
|
||||
|
||||
// Future availability cards use the new design
|
||||
if (isFuture) {
|
||||
return <FutureAvailabilityCard vm={vm} />
|
||||
}
|
||||
|
||||
// secondary chips — max 1 visible (availability only), no overflow chip
|
||||
const secondaryChip = vm.availabilityLabel ?? vm.fitOutLabel ?? null
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
boxShadow: `0 2px 16px rgba(0,0,0,0.07), 0 0 0 1px ${theme.cardBorder}`,
|
||||
background: theme.cardBg,
|
||||
border: `1px solid ${DS_BORDER.default}`,
|
||||
bgcolor: '#ffffff',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'box-shadow 0.15s, transform 0.1s',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.15s',
|
||||
'&:hover': {
|
||||
boxShadow: `0 6px 28px rgba(0,0,0,0.12), 0 0 0 1px ${theme.cardBorder}`,
|
||||
transform: 'translateY(-1px)',
|
||||
borderColor: '#b0aead',
|
||||
transform: 'scale(1.025)',
|
||||
boxShadow: '0 10px 32px rgba(0,0,0,0.13)',
|
||||
zIndex: 1,
|
||||
},
|
||||
}}>
|
||||
|
||||
{/* ── Hero zone ── */}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={175} overlayTypeLabel={overlayTypeLabel} overlayLocationLabel={overlayLocationLabel} />
|
||||
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={200} overlayTypeLabel={overlayTypeLabel} overlayLocationLabel={overlayLocationLabel} />
|
||||
|
||||
{/* Score badge — bottom-left editorial */}
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 10, left: 10,
|
||||
background: theme.gradient, borderRadius: '10px',
|
||||
px: 1.5, py: 0.5,
|
||||
boxShadow: `0 4px 16px ${theme.glow}`,
|
||||
position: 'absolute', bottom: 10, left: 12,
|
||||
bgcolor: theme.gradient, borderRadius: '8px',
|
||||
px: 1.25, py: 0.375,
|
||||
border: `1px solid ${theme.border}`,
|
||||
minWidth: 52, textAlign: 'center',
|
||||
}}>
|
||||
<Typography sx={{ fontWeight: 900, fontSize: '1.5rem', color: theme.text, lineHeight: 1 }}>
|
||||
<Typography sx={{
|
||||
fontWeight: 700, fontSize: '1.625rem',
|
||||
color: '#ffffff', lineHeight: 1, letterSpacing: '-0.02em',
|
||||
}}>
|
||||
{vm.matchScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ position: 'absolute', top: 10, right: 44, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}>
|
||||
|
||||
{/* Location pill — Ginesta-style, top-left */}
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 10, left: 10,
|
||||
bgcolor: 'rgba(255,255,255,0.92)',
|
||||
borderRadius: '20px',
|
||||
px: 1, py: 0.35,
|
||||
display: 'flex', alignItems: 'center', gap: 0.4,
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}>
|
||||
<MapPin size={9} color="#64748b" />
|
||||
<Typography sx={{ fontSize: '0.65rem', fontWeight: 500, color: '#374151', lineHeight: 1 }}>
|
||||
{vm.locationLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Top-right badge strip — result type + owner + heat */}
|
||||
<Box sx={{ position: 'absolute', top: 10, right: 10, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}>
|
||||
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
|
||||
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
|
||||
<Chip
|
||||
icon={<Building2 size={9} color="#1e3a5f" />}
|
||||
icon={<Building2 size={9} color="#152642" />}
|
||||
label="Ihr Objekt"
|
||||
size="small"
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.85)', color: 'white', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }}
|
||||
sx={{ bgcolor: 'rgba(21,38,66,0.85)', color: 'white', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }}
|
||||
/>
|
||||
)}
|
||||
<HeatBadge propertyId={vm.propertyId} size="sm" />
|
||||
@@ -83,24 +107,24 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
|
||||
</Box>
|
||||
|
||||
{/* ── Card body ── */}
|
||||
<Box sx={{ p: 2, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{/* Unit-first title (floor + street) */}
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
|
||||
<Box sx={{ p: 2.25, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
|
||||
{/* Title */}
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary }} noWrap>
|
||||
{vm.title}
|
||||
</Typography>
|
||||
{/* Building name as subtitle when a unit or floor range is in the title */}
|
||||
{vm.propertySubtitle && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 0.125, color: '#475569', fontWeight: 500 }} noWrap>
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 0.125, color: DS_TEXT.secondary, fontWeight: 500 }} noWrap>
|
||||
{vm.propertySubtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25 }}>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block', mt: 0.25 }}>
|
||||
{[vm.locationLabel, vm.availabilityLabel].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
|
||||
{/* Unit area + rent when a specific unit is matched */}
|
||||
{/* Unit details */}
|
||||
{vm.preMarketUnit && (
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.68rem', mt: 0.25 }}>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.68rem', mt: 0.25 }}>
|
||||
{vm.preMarketUnit.areaSqm.toLocaleString('de-CH')} m²
|
||||
{vm.preMarketUnit.rentPricePerSqm
|
||||
? ` · CHF ${Math.round(vm.preMarketUnit.rentPricePerSqm / 12).toLocaleString('de-CH')}/m²/Mt.`
|
||||
@@ -108,43 +132,47 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Regular explainability summary */}
|
||||
{/* Confidence + one secondary chip max */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 1, alignItems: 'center' }}>
|
||||
<Chip label={`${confPct}% Konfidenz`} size="small"
|
||||
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
|
||||
{secondaryChip && (
|
||||
<Chip label={secondaryChip} size="small" variant="outlined"
|
||||
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.secondary }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Explainability */}
|
||||
{vm.explainabilitySummary && (
|
||||
<>
|
||||
<Divider sx={{ my: 1.25 }} />
|
||||
<Typography variant="body2" sx={{
|
||||
fontWeight: 500, color: '#1e293b', lineHeight: 1.5, flex: 1,
|
||||
overflow: 'hidden', display: '-webkit-box',
|
||||
WebkitLineClamp: 3, WebkitBoxOrient: 'vertical',
|
||||
}}>
|
||||
{vm.explainabilitySummary}
|
||||
</Typography>
|
||||
</>
|
||||
<Typography variant="body2" sx={{
|
||||
fontWeight: 500, color: DS_TEXT.secondary, lineHeight: 1.5, flex: 1, mt: 1,
|
||||
overflow: 'hidden', display: '-webkit-box',
|
||||
WebkitLineClamp: 3, WebkitBoxOrient: 'vertical',
|
||||
}}>
|
||||
{vm.explainabilitySummary}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{vm.reasons.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ my: 1.25 }} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
|
||||
{vm.reasons.slice(0, 3).map((r, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
|
||||
<CheckCircle2 size={13} color="#1a7a4a" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 600, color: '#1e293b', lineHeight: 1.3 }}>
|
||||
{r.label}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: '#64748b', lineHeight: 1.3 }}>
|
||||
{r.explanation}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 1 }}>
|
||||
{vm.reasons.slice(0, 2).map((r, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
|
||||
<CheckCircle2 size={13} color="#1d6342" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 600, color: DS_TEXT.primary, lineHeight: 1.3 }}>
|
||||
{r.label}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: DS_TEXT.muted, lineHeight: 1.3 }}>
|
||||
{r.explanation}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: `1px solid ${DS_BORDER.muted}` }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
@@ -158,7 +186,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
|
||||
propertyId: vm.propertyId,
|
||||
})
|
||||
}}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, bgcolor: '#152642', '&:hover': { bgcolor: '#0e1c30' } }}
|
||||
>
|
||||
Anfrage
|
||||
</Button>
|
||||
@@ -169,9 +197,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
|
||||
variant={a.variant === 'primary' ? 'contained' : 'outlined'}
|
||||
onClick={a.onClick}
|
||||
disabled={a.disabled}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||
}}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function MatchActionToolbar({ actions }: Props) {
|
||||
onClick={action.onClick}
|
||||
sx={
|
||||
action.variant === 'primary'
|
||||
? { bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }
|
||||
? { bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }
|
||||
: action.variant === 'danger'
|
||||
? { color: '#c0392b', borderColor: '#c0392b', '&:hover': { borderColor: '#c0392b' } }
|
||||
: {}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { memo } from 'react'
|
||||
import { Alert, Box, Button, Chip, Divider, Typography } from '@mui/material'
|
||||
import { MapPin, Building2, CheckCircle2, AlertTriangle } from 'lucide-react'
|
||||
import { Alert, Box, Button, Chip, Typography } from '@mui/material'
|
||||
import { MapPin, AlertTriangle } from 'lucide-react'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useInquiryStore } from '../../stores/inquiryStore'
|
||||
import { HeatBadge } from '../shared/HeatBadge'
|
||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||
import { RESULT_TYPE_META } from '../../lib/ds'
|
||||
import { RESULT_TYPE_META, DS_TEXT, DS_BORDER } from '../../lib/ds'
|
||||
import { confidenceHex } from '../../lib/utils'
|
||||
import { ScoreInlineBreakdown } from './ScoreInlineBreakdown'
|
||||
import { MatchCardRestrictedState } from './MatchCardRestrictedState'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
|
||||
interface Props {
|
||||
vm: MatchCardViewModel
|
||||
imageUrl?: string
|
||||
overlayTypeLabel?: string
|
||||
}
|
||||
|
||||
export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, overlayTypeLabel }: Props) {
|
||||
export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl }: Props) {
|
||||
if (vm.isRestricted) return <MatchCardRestrictedState />
|
||||
|
||||
const { currentUser } = useSessionStore()
|
||||
@@ -25,29 +23,47 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
|
||||
const isPortfolioOwner = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN'
|
||||
|
||||
const tier = getScoreTier(vm.matchScore)
|
||||
const theme = SCORE_THEME[tier]
|
||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
|
||||
const scoreBadgeBg = SCORE_THEME[tier].gradient
|
||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#9a9a9a' }
|
||||
const confPct = Math.round(vm.confidenceScore * 100)
|
||||
const topRisk = vm.risks.find(r => r.level === 'CRITICAL' || r.level === 'HIGH')
|
||||
|
||||
const borderColor = vm.isCompareSelected ? '#7c3aed' : vm.isSelected ? '#1e3a5f' : 'transparent'
|
||||
const leftAccent = vm.isCompareSelected ? '#5b3f8a' : vm.isSelected ? '#152642' : 'transparent'
|
||||
|
||||
// Compact metric line — area · rent · availability
|
||||
const metricParts: string[] = []
|
||||
if (vm.areaSqm) metricParts.push(`${vm.areaSqm.toLocaleString('de-CH')} m²`)
|
||||
if (vm.rentPerSqm) metricParts.push(`CHF ${vm.rentPerSqm}/m²/J`)
|
||||
if (vm.availabilityLabel) metricParts.push(vm.availabilityLabel)
|
||||
const metricLine = metricParts.join(' · ')
|
||||
|
||||
// Detail action — prefer the "Details →" action if available, fallback to first action
|
||||
const detailAction = vm.actions.find(a => a.actionType === 'OPEN_DETAIL' && a.label.includes('Detail'))
|
||||
?? vm.actions.find(a => a.actionType === 'OPEN_DETAIL')
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
mb: 1.5,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: `2px solid ${borderColor}`,
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.07)',
|
||||
background: theme.cardBg,
|
||||
border: `1px solid ${DS_BORDER.default}`,
|
||||
borderLeft: `3px solid ${leftAccent}`,
|
||||
bgcolor: '#ffffff',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
opacity: vm.isStaleData ? 0.75 : 1,
|
||||
transition: 'box-shadow 0.15s',
|
||||
'&:hover': { boxShadow: '0 4px 20px rgba(0,0,0,0.11)' },
|
||||
transition: 'border-color 0.15s, transform 0.18s ease, box-shadow 0.18s ease',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
'&:hover': {
|
||||
borderColor: '#b0aead',
|
||||
transform: 'scale(1.012)',
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.10)',
|
||||
zIndex: 1,
|
||||
},
|
||||
}}>
|
||||
|
||||
{/* ── Image strip ── */}
|
||||
<Box sx={{ width: 120, flexShrink: 0, position: 'relative', bgcolor: '#dce8f2' }}>
|
||||
{/* ── Image strip — 180px, Ginesta proportion ── */}
|
||||
<Box sx={{ width: 180, flexShrink: 0, position: 'relative', bgcolor: '#f4f3f0' }}>
|
||||
{imageUrl ? (
|
||||
<img
|
||||
src={imageUrl}
|
||||
@@ -56,188 +72,129 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{
|
||||
width: '100%', height: '100%', minHeight: 120,
|
||||
width: '100%', height: '100%', minHeight: 140,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'linear-gradient(160deg,#dce8f2 0%,#b8cfe0 100%)',
|
||||
bgcolor: '#f4f3f0',
|
||||
}}>
|
||||
<MapPin size={24} color="#7ba3bf" />
|
||||
<MapPin size={24} color="#c4c4c4" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Score badge */}
|
||||
{/* Score badge — top left */}
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 8, left: 8,
|
||||
background: theme.gradient, borderRadius: '8px',
|
||||
px: 1, py: 0.25,
|
||||
border: `1px solid ${theme.border}`,
|
||||
boxShadow: `0 2px 8px ${theme.glow}`,
|
||||
bgcolor: scoreBadgeBg, borderRadius: '6px',
|
||||
px: 0.875, py: 0.25,
|
||||
border: `1px solid ${SCORE_THEME[tier].border}`,
|
||||
}}>
|
||||
<Typography sx={{ fontWeight: 900, fontSize: '1rem', color: theme.text, lineHeight: 1 }}>
|
||||
<Typography sx={{
|
||||
fontWeight: 700, fontSize: '1.125rem',
|
||||
color: '#ffffff', lineHeight: 1, letterSpacing: '-0.01em',
|
||||
}}>
|
||||
{vm.matchScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Asset type label */}
|
||||
{overlayTypeLabel && (
|
||||
<Box sx={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0,
|
||||
px: 0.75, py: 0.5,
|
||||
background: 'linear-gradient(to top, rgba(0,0,0,0.65) 0%, transparent 100%)',
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.6rem', fontWeight: 700, color: 'white', letterSpacing: 0.3, lineHeight: 1.3 }}>
|
||||
{overlayTypeLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{/* Location pill — Ginesta-style, top right */}
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 8, right: 8,
|
||||
bgcolor: 'rgba(255,255,255,0.92)',
|
||||
borderRadius: '20px',
|
||||
px: 1, py: 0.35,
|
||||
display: 'flex', alignItems: 'center', gap: 0.4,
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}>
|
||||
<MapPin size={9} color="#64748b" />
|
||||
<Typography sx={{ fontSize: '0.65rem', fontWeight: 500, color: '#374151', lineHeight: 1 }}>
|
||||
{vm.locationLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Content ── */}
|
||||
<Box sx={{ flex: 1, p: 1.75, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
<Box sx={{ flex: 1, px: 2, py: 2.5, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
|
||||
{/* Alerts */}
|
||||
{vm.disclaimer && (
|
||||
<Alert severity="warning" sx={{ mb: 1, py: 0.25, fontSize: '0.72rem' }}>{vm.disclaimer}</Alert>
|
||||
)}
|
||||
|
||||
|
||||
{/* Badges row */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.625, flexWrap: 'wrap', gap: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, alignItems: 'center' }}>
|
||||
<HeatBadge propertyId={vm.propertyId} size="sm" />
|
||||
<Chip label={rt.label} size="small"
|
||||
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
|
||||
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
|
||||
<Chip icon={<Building2 size={9} color="#1e3a5f" />} label="Ihr Objekt" size="small"
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.10)', color: '#1e3a5f', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }} />
|
||||
)}
|
||||
<Chip label={`${confPct}% Konfidenz`} size="small"
|
||||
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
|
||||
{vm.availabilityLabel && (
|
||||
<Chip label={vm.availabilityLabel} size="small" variant="outlined" sx={{ fontSize: 10, height: 20 }} />
|
||||
)}
|
||||
{vm.fitOutLabel && (
|
||||
<Chip
|
||||
label={vm.fitOutLabel}
|
||||
size="small" variant="outlined"
|
||||
sx={{
|
||||
fontSize: 10, height: 20,
|
||||
borderColor: vm.fitOutViable ? '#059669' : '#94a3b8',
|
||||
color: vm.fitOutViable ? '#059669' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{vm.fitOutInvestment && !vm.fitOutInvestment.isFullyCovered && (
|
||||
<Chip
|
||||
label={vm.fitOutInvestment.shortLabel}
|
||||
size="small" variant="outlined"
|
||||
sx={{
|
||||
fontSize: 10, height: 20,
|
||||
borderColor: vm.fitOutInvestment.netTotal.max > 100000 ? '#d97706' : '#94a3b8',
|
||||
color: vm.fitOutInvestment.netTotal.max > 100000 ? '#92400e' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{vm.isDivisible && vm.minDivisibleUnitSqm && (
|
||||
<Chip
|
||||
label={`Teilbar ab ${vm.minDivisibleUnitSqm} m²`}
|
||||
size="small" variant="outlined"
|
||||
sx={{ fontSize: 10, height: 20, borderColor: '#7c3aed', color: '#7c3aed' }}
|
||||
/>
|
||||
)}
|
||||
{topRisk && (
|
||||
<Chip
|
||||
label={topRisk.level === 'CRITICAL' ? 'Kritisch' : 'Hohes Risiko'}
|
||||
size="small" color="error" variant="outlined"
|
||||
sx={{ fontSize: 10, height: 20 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* Chips — result type + heat + confidence */}
|
||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', mb: 0.875 }}>
|
||||
<Chip label={rt.label} size="small"
|
||||
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
|
||||
<HeatBadge propertyId={vm.propertyId} size="sm" />
|
||||
<Chip label={`${confPct}% Konfidenz`} size="small"
|
||||
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
|
||||
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
|
||||
<Chip label="Ihr Objekt" size="small"
|
||||
sx={{ bgcolor: 'rgba(21,38,66,0.10)', color: '#152642', fontWeight: 700, fontSize: 9, height: 18 }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Title + location */}
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
|
||||
{/* Title */}
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary }} noWrap>
|
||||
{vm.title}
|
||||
</Typography>
|
||||
{vm.propertySubtitle && (
|
||||
<Typography variant="caption" sx={{ color: '#475569', fontWeight: 500, display: 'block', mt: 0.1 }} noWrap>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.secondary, fontWeight: 500, display: 'block', mt: 0.1 }} noWrap>
|
||||
{vm.propertySubtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.25 }}>
|
||||
<MapPin size={11} color="#64748b" />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{vm.locationLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Explainability summary */}
|
||||
{/* Metric line — Ginesta-style */}
|
||||
{metricLine && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8', mt: 0.5, letterSpacing: 0.1 }}>
|
||||
{metricLine}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Explainability summary — 1 line */}
|
||||
{vm.explainabilitySummary && (
|
||||
<Typography variant="body2" sx={{ color: '#374151', fontWeight: 500, mt: 0.5, lineHeight: 1.45,
|
||||
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
|
||||
<Typography variant="body2" sx={{
|
||||
color: DS_TEXT.secondary, fontWeight: 500, mt: 0.875, lineHeight: 1.5,
|
||||
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical',
|
||||
}}>
|
||||
{vm.explainabilitySummary}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
{/* Score breakdown */}
|
||||
{vm.scoreBreakdown && (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<ScoreInlineBreakdown scoreBreakdown={vm.scoreBreakdown} allFactors={vm.allFactors} compact />
|
||||
</Box>
|
||||
{/* Critical risk only */}
|
||||
{topRisk && topRisk.level === 'CRITICAL' && (
|
||||
<Alert icon={<AlertTriangle size={13} />} severity="error"
|
||||
sx={{ mt: 0.75, py: 0.25, fontSize: '0.72rem' }}>
|
||||
{topRisk.description}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Reasons — all 3, same style as grid card */}
|
||||
{vm.reasons.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{vm.reasons.slice(0, 3).map((r, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
|
||||
<CheckCircle2 size={12} color="#1a7a4a" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: '#1e293b', lineHeight: 1.3 }}>
|
||||
{r.label}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.69rem', color: '#64748b', lineHeight: 1.3 }}>
|
||||
{r.explanation}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Top tradeoff */}
|
||||
{vm.tradeoffs.length > 0 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5, mt: 0.5 }}>
|
||||
<AlertTriangle size={12} color="#d97706" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.69rem', color: '#92400e', lineHeight: 1.3 }}>
|
||||
{vm.tradeoffs[0].concern}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mt: 1.25, pt: 1, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<Button size="small" variant="contained" onClick={e => {
|
||||
e.stopPropagation()
|
||||
openInquiryDialog({
|
||||
propertyTitle: vm.title,
|
||||
location: vm.locationLabel ?? '',
|
||||
matchScore: vm.matchScore,
|
||||
matchId: vm.id,
|
||||
propertyId: vm.propertyId,
|
||||
})
|
||||
}}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}>
|
||||
{/* 2 Buttons — Anfrage + Details */}
|
||||
<Box sx={{ display: 'flex', gap: 0.625, mt: 'auto', pt: 1.25 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openInquiryDialog({
|
||||
propertyTitle: vm.title,
|
||||
location: vm.locationLabel ?? '',
|
||||
matchScore: vm.matchScore,
|
||||
matchId: vm.id,
|
||||
propertyId: vm.propertyId,
|
||||
})
|
||||
}}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, bgcolor: '#152642', '&:hover': { bgcolor: '#0e1c30' } }}
|
||||
>
|
||||
Anfrage
|
||||
</Button>
|
||||
{vm.actions.map(a => (
|
||||
<Button key={a.id} size="small" variant={a.variant === 'primary' ? 'contained' : 'outlined'}
|
||||
onClick={a.onClick} disabled={a.disabled}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }}>
|
||||
{a.label}
|
||||
{detailAction && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={detailAction.onClick}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }}
|
||||
>
|
||||
Details →
|
||||
</Button>
|
||||
))}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MatchScoreDisplay } from './MatchScoreDisplay'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
|
||||
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
VERIFIED_PORTFOLIO: { label: 'Verified', color: '#1e3a5f' },
|
||||
VERIFIED_PORTFOLIO: { label: 'Verified', color: '#152642' },
|
||||
MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
|
||||
FUTURE_AVAILABILITY: { label: 'Signal', color: '#7c3aed' },
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ export function MatchCardHeader({ vm, compact }: Props) {
|
||||
/>
|
||||
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
|
||||
<Chip
|
||||
icon={<Building2 size={10} color="#1e3a5f" />}
|
||||
icon={<Building2 size={10} color="#152642" />}
|
||||
label="Ihr Objekt"
|
||||
size="small"
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.12)', color: '#1e3a5f', fontWeight: 700, fontSize: 11, '& .MuiChip-icon': { ml: 0.5 } }}
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.12)', color: '#152642', fontWeight: 700, fontSize: 11, '& .MuiChip-icon': { ml: 0.5 } }}
|
||||
/>
|
||||
)}
|
||||
{vm.assetType && (
|
||||
|
||||
@@ -86,6 +86,10 @@ export interface MatchCardViewModel {
|
||||
preMarketUnit?: PropertyUnit // specific unit being released pre-market
|
||||
preMarketAllUnits?: PropertyUnit[] // all units of the backing property
|
||||
|
||||
// Key metrics for compact display (Ginesta-style metric line)
|
||||
areaSqm?: number
|
||||
rentPerSqm?: number
|
||||
|
||||
// Score transparency
|
||||
scoreBreakdown?: {
|
||||
hardMatchScore: number
|
||||
|
||||
@@ -25,7 +25,7 @@ const HARD_KEYS = new Set(['area', 'location', 'budget', 'timing'])
|
||||
|
||||
function importanceLabel(weight: number, maxWeight: number): { label: string; color: string } {
|
||||
const ratio = maxWeight > 0 ? weight / maxWeight : 0
|
||||
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#1e3a5f' }
|
||||
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#152642' }
|
||||
if (ratio >= 0.65) return { label: 'Sehr wichtig', color: '#1d4ed8' }
|
||||
if (ratio >= 0.40) return { label: 'Wichtig', color: '#475569' }
|
||||
if (ratio >= 0.20) return { label: 'Wenig wichtig',color: '#94a3b8' }
|
||||
@@ -93,15 +93,15 @@ export function ScoreInlineBreakdown({ scoreBreakdown: sb, allFactors, compact =
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#1e3a5f', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 1 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#152642', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 1 }}>
|
||||
Hart-Kriterien
|
||||
</Typography>
|
||||
{hardFactors.map((f, i) => <FactorRow key={i} factor={f} maxWeight={maxHardWeight} />)}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#eef2ff', px: 1.25, py: 0.6, borderRadius: 1, mb: softFactors.length > 0 ? 2 : 1.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e3a5f' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#152642' }}>
|
||||
{sb.hardMatchScore}/100 × 60%
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1e3a5f' }}>{hardContrib} Pkt</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: '#152642' }}>{hardContrib} Pkt</Typography>
|
||||
</Box>
|
||||
|
||||
{softFactors.length > 0 && (
|
||||
|
||||
@@ -10,7 +10,7 @@ export function SignalQualityDots({ quality }: { quality: 'HIGH' | 'MEDIUM' | 'L
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{c.dots.map((filled, i) => (
|
||||
<Box key={i} sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: filled ? c.color : '#e2e8f0' }} />
|
||||
<Box key={i} sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: filled ? c.color : '#e8e7e4' }} />
|
||||
))}
|
||||
<Typography sx={{ fontSize: '0.68rem', color: c.color, fontWeight: 600, ml: 0.25 }}>
|
||||
{c.label}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function MatchListCard({ match, property, need, onSelect, onApprove }: Pr
|
||||
size="small"
|
||||
sx={need.isAnonymous
|
||||
? { bgcolor: '#f5f3ff', color: '#6d28d9', fontSize: 11, height: 20, fontWeight: 600, border: '1px solid #ddd6fe' }
|
||||
: { bgcolor: '#eff6ff', color: '#1e3a5f', fontSize: 11, height: 20, fontWeight: 500 }
|
||||
: { bgcolor: '#eff6ff', color: '#152642', fontSize: 11, height: 20, fontWeight: 500 }
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ const STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
PENDING_REVIEW: { label: 'Ausstehend', color: '#d97706' },
|
||||
APPROVED: { label: 'Genehmigt', color: '#1a7a4a' },
|
||||
REJECTED: { label: 'Abgelehnt', color: '#c0392b' },
|
||||
SHORTLISTED: { label: 'Shortlist', color: '#1e3a5f' },
|
||||
SHORTLISTED: { label: 'Shortlist', color: '#152642' },
|
||||
}
|
||||
|
||||
export function MatchStatusBadge({ status }: { status?: MatchStatus }) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ExecutiveSummaryPanel({ match }: Props) {
|
||||
|
||||
const rows: { icon: React.ReactNode; label: string; text: string }[] = [
|
||||
{
|
||||
icon: <Target size={15} color="#1e3a5f" />,
|
||||
icon: <Target size={15} color="#152642" />,
|
||||
label: 'Gesamteignung',
|
||||
text: match.explainabilitySummary || `${match.matchStrength}-Match mit ${match.matchScore} Punkten`,
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ export function InquiryQuickDialog() {
|
||||
size="small"
|
||||
disabled={!message.trim()}
|
||||
startIcon={<Send size={14} />}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Anfrage senden
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Property } from '../../domain/property'
|
||||
import type { FutureSignal } from '../../domain/futureSignal'
|
||||
|
||||
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
|
||||
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#152642' },
|
||||
MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
|
||||
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function MatchDetailHeader({ match, property, signal, onBack, onCompare,
|
||||
size="small"
|
||||
startIcon={<Columns2 size={14} />}
|
||||
onClick={onCompare}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Vergleichen
|
||||
</Button>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function MatchDetailHero({
|
||||
{/* Hero: image first, map fallback */}
|
||||
{!isFuture && (
|
||||
property?.images?.[0] ? (
|
||||
<Box sx={{ width: '100%', height: 400, overflow: 'hidden', bgcolor: '#e2e8f0', flexShrink: 0 }}>
|
||||
<Box sx={{ width: '100%', height: 400, overflow: 'hidden', bgcolor: '#e8e7e4', flexShrink: 0 }}>
|
||||
<img
|
||||
src={property.images[0]}
|
||||
alt={property.title}
|
||||
@@ -105,7 +105,7 @@ export function MatchDetailHero({
|
||||
size="small"
|
||||
startIcon={<Columns2 size={14} />}
|
||||
onClick={onCompare}
|
||||
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none' }}
|
||||
sx={{ bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none' }}
|
||||
>
|
||||
Vergleichen
|
||||
</Button>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function NextActionsPanel({ match, onCompare, onPipeline, onInquire }: Pr
|
||||
variant="contained"
|
||||
startIcon={<MessageSquare size={14} />}
|
||||
onClick={onInquire}
|
||||
sx={{ justifyContent: 'flex-start', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ justifyContent: 'flex-start', bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
Anfrage senden
|
||||
</Button>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function factorLabel(criterion: string): string {
|
||||
// Convert normalised weight to 1-5 importance level relative to other factors in the same set
|
||||
export function importanceLabel(weight: number, maxWeight: number): { label: string; color: string } {
|
||||
const ratio = maxWeight > 0 ? weight / maxWeight : 0
|
||||
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#1e3a5f' }
|
||||
if (ratio >= 0.85) return { label: 'Entscheidend', color: '#152642' }
|
||||
if (ratio >= 0.65) return { label: 'Sehr wichtig', color: '#1d4ed8' }
|
||||
if (ratio >= 0.40) return { label: 'Wichtig', color: '#475569' }
|
||||
if (ratio >= 0.20) return { label: 'Wenig wichtig',color: '#94a3b8' }
|
||||
|
||||
@@ -41,7 +41,7 @@ function Slide0() {
|
||||
return (
|
||||
<Box className="flex flex-col items-center text-center px-2">
|
||||
<Box className="mb-4">
|
||||
<Building2 size={48} color="#1e3a5f" />
|
||||
<Building2 size={48} color="#152642" />
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||
Willkommen bei Property Match
|
||||
@@ -74,7 +74,7 @@ function Slide1() {
|
||||
return (
|
||||
<Box className="flex flex-col items-center text-center px-2">
|
||||
<Box className="mb-4">
|
||||
<Search size={48} color="#1e3a5f" />
|
||||
<Search size={48} color="#152642" />
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||
Bedarf beschreiben — KI findet Matches
|
||||
|
||||
@@ -18,7 +18,7 @@ export function DataCategoryBadgeList({ categories, max }: DataCategoryBadgeList
|
||||
label={cat}
|
||||
sx={{
|
||||
bgcolor: 'rgba(30,58,95,0.07)',
|
||||
color: '#1e3a5f',
|
||||
color: '#152642',
|
||||
border: 'none',
|
||||
fontSize: '0.68rem',
|
||||
}}
|
||||
|
||||
@@ -156,7 +156,7 @@ export function MarketSignalDetailPanel({ signal }: MarketSignalDetailPanelProps
|
||||
label={entity.value}
|
||||
sx={{
|
||||
bgcolor: 'rgba(30,58,95,0.08)',
|
||||
color: '#1e3a5f',
|
||||
color: '#152642',
|
||||
border: 'none',
|
||||
fontSize: '0.75rem',
|
||||
cursor: 'default',
|
||||
|
||||
@@ -48,10 +48,10 @@ export function MarketSignalFilterBar({ filters, onChange }: MarketSignalFilterB
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
height: 20,
|
||||
bgcolor: active ? '#1e3a5f' : 'transparent',
|
||||
bgcolor: active ? '#152642' : 'transparent',
|
||||
color: active ? '#fff' : 'text.secondary',
|
||||
border: '1px solid',
|
||||
borderColor: active ? '#1e3a5f' : 'divider',
|
||||
borderColor: active ? '#152642' : 'divider',
|
||||
'&:hover': { bgcolor: active ? '#162d4a' : 'rgba(0,0,0,0.04)' },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ReviewGatePanel({ gate }: ReviewGatePanelProps) {
|
||||
|
||||
{reviewerNotes.length > 0 && (
|
||||
<Box sx={{ mt: 1, p: 1, bgcolor: 'rgba(30,58,95,0.04)', borderRadius: 1, border: '1px solid rgba(30,58,95,0.1)' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e3a5f', display: 'block', mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#152642', display: 'block', mb: 0.5 }}>
|
||||
Reviewer-Informationen
|
||||
</Typography>
|
||||
{reviewerNotes.map((note, i) => (
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SignalConversionPanel({ signal }: SignalConversionPanelProps) {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<ArrowRight size={16} color="#1e3a5f" />
|
||||
<ArrowRight size={16} color="#152642" />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
In Future Availability überführen
|
||||
</Typography>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function SignalInbox({
|
||||
<Chip
|
||||
label={isLoading ? '…' : signals.length}
|
||||
size="small"
|
||||
sx={{ bgcolor: '#1e3a5f', color: '#fff', fontSize: '0.7rem', height: 20 }}
|
||||
sx={{ bgcolor: '#152642', color: '#fff', fontSize: '0.7rem', height: 20 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export function SignalPipelineStepper({ pipelineState }: SignalPipelineStepperPr
|
||||
sx={{
|
||||
mb: 1,
|
||||
'& .MuiStepIcon-root': { fontSize: '1.6rem', color: '#cbd5e1' },
|
||||
'& .MuiStepIcon-root.Mui-active': { color: '#1e3a5f' },
|
||||
'& .MuiStepIcon-root.Mui-active': { color: '#152642' },
|
||||
'& .MuiStepIcon-root.Mui-completed': { color: '#15803d' },
|
||||
'& .MuiStepLabel-label': { fontSize: '0.68rem', mt: 0.5, color: '#64748b' },
|
||||
'& .MuiStepLabel-label.Mui-active': { color: '#1e3a5f', fontWeight: 700 },
|
||||
'& .MuiStepLabel-label.Mui-active': { color: '#152642', fontWeight: 700 },
|
||||
'& .MuiStepLabel-label.Mui-completed': { color: '#15803d' },
|
||||
'& .MuiStepConnector-line': { borderTopWidth: 2, borderColor: '#e2e8f0' },
|
||||
'& .MuiStepConnector-line': { borderTopWidth: 2, borderColor: '#e8e7e4' },
|
||||
'& .MuiStepConnector-root.Mui-completed .MuiStepConnector-line': { borderColor: '#15803d' },
|
||||
'& .MuiStepConnector-root.Mui-active .MuiStepConnector-line': { borderColor: '#1e3a5f' },
|
||||
'& .MuiStepConnector-root.Mui-active .MuiStepConnector-line': { borderColor: '#152642' },
|
||||
}}
|
||||
>
|
||||
{PIPELINE_STAGE_ORDER.map((stage) => (
|
||||
|
||||
@@ -123,7 +123,7 @@ export function SignalPipelineView({ signal }: SignalPipelineViewProps) {
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
bgcolor: '#1e3a5f',
|
||||
bgcolor: '#152642',
|
||||
'&:hover': { bgcolor: '#162d4a' },
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ function getStageColor(entry: AuditTrailEntry): string {
|
||||
const idx = PIPELINE_STAGE_ORDER.indexOf(entry.stage)
|
||||
if (idx <= 0) return '#94a3b8'
|
||||
if (idx >= 5) return '#15803d'
|
||||
if (idx >= 3) return '#1e3a5f'
|
||||
if (idx >= 3) return '#152642'
|
||||
if (idx >= 2) return '#4f46e5'
|
||||
return '#2563eb'
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function SourceCard({ source, selected, onClick }: SourceCardProps) {
|
||||
mt: 0.25,
|
||||
}}
|
||||
>
|
||||
<Icon size={14} color="#1e3a5f" />
|
||||
<Icon size={14} color="#152642" />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
|
||||
@@ -86,7 +86,7 @@ export function SourceDetailPanel({ source }: SourceDetailPanelProps) {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<SourceIcon size={18} color="#1e3a5f" />
|
||||
<SourceIcon size={18} color="#152642" />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3, mb: 0.25 }}>
|
||||
@@ -166,7 +166,7 @@ export function SourceDetailPanel({ source }: SourceDetailPanelProps) {
|
||||
key={a}
|
||||
size="small"
|
||||
label={a}
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.06)', color: '#1e3a5f', border: 'none', fontSize: '0.72rem' }}
|
||||
sx={{ bgcolor: 'rgba(30,58,95,0.06)', color: '#152642', border: 'none', fontSize: '0.72rem' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -194,7 +194,7 @@ export function SourceDetailPanel({ source }: SourceDetailPanelProps) {
|
||||
startIcon={isRunning ? <CircularProgress size={12} sx={{ color: '#fff' }} /> : <Play size={14} />}
|
||||
disabled={isRunning}
|
||||
onClick={() => triggerRun(source.id)}
|
||||
sx={{ bgcolor: '#1e3a5f', textTransform: 'none', fontSize: '0.8rem', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
sx={{ bgcolor: '#152642', textTransform: 'none', fontSize: '0.8rem', '&:hover': { bgcolor: '#162d4a' } }}
|
||||
>
|
||||
{isRunning ? 'Läuft...' : 'Demo-Run starten'}
|
||||
</Button>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SourceList({
|
||||
<Chip
|
||||
label={isLoading ? '…' : sources.length}
|
||||
size="small"
|
||||
sx={{ bgcolor: '#1e3a5f', color: '#fff', fontSize: '0.7rem', height: 20 }}
|
||||
sx={{ bgcolor: '#152642', color: '#fff', fontSize: '0.7rem', height: 20 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function DraggableCard({
|
||||
boxShadow: isDragOverlay ? '0 8px 24px rgba(0,0,0,0.18)' : '0 1px 3px rgba(0,0,0,0.08)',
|
||||
'&:hover': isDragOverlay ? {} : {
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
borderColor: isSelected ? '#1e3a5f' : '#bfdbfe',
|
||||
borderColor: isSelected ? '#152642' : '#bfdbfe',
|
||||
},
|
||||
transition: isDragOverlay ? undefined : 'box-shadow 0.15s, border-color 0.15s',
|
||||
userSelect: 'none',
|
||||
@@ -83,7 +83,7 @@ export function DraggableCard({
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.125, flexShrink: 0 }}>
|
||||
{item.inquiryId && onChatClick && (
|
||||
<Tooltip title="Chat öffnen">
|
||||
<IconButton size="small" onClick={onChatClick} sx={{ p: 0.25, color: '#1e3a5f', '&:hover': { bgcolor: '#eff6ff' } }}>
|
||||
<IconButton size="small" onClick={onChatClick} sx={{ p: 0.25, color: '#152642', '&:hover': { bgcolor: '#eff6ff' } }}>
|
||||
<MessageSquare size={13} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -93,7 +93,7 @@ export function DraggableCard({
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); navigate(path) }}
|
||||
sx={{ p: 0.25, color: '#94a3b8', '&:hover': { color: '#1e3a5f', bgcolor: '#eff6ff' } }}
|
||||
sx={{ p: 0.25, color: '#94a3b8', '&:hover': { color: '#152642', bgcolor: '#eff6ff' } }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
</IconButton>
|
||||
@@ -154,8 +154,8 @@ export function DraggableCard({
|
||||
}}
|
||||
sx={{
|
||||
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
||||
borderColor: '#1e3a5f', color: '#1e3a5f',
|
||||
'&:hover': { bgcolor: '#eff6ff', borderColor: '#1e3a5f' },
|
||||
borderColor: '#152642', color: '#152642',
|
||||
'&:hover': { bgcolor: '#eff6ff', borderColor: '#152642' },
|
||||
}}
|
||||
>
|
||||
Zur Konversation
|
||||
@@ -180,7 +180,7 @@ export function DraggableCard({
|
||||
}}
|
||||
sx={{
|
||||
fontSize: '0.7rem', py: 0.5, textTransform: 'none', fontWeight: 600,
|
||||
bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' },
|
||||
bgcolor: '#152642', '&:hover': { bgcolor: '#162d4a' },
|
||||
}}
|
||||
>
|
||||
Anfrage senden
|
||||
|
||||
@@ -16,10 +16,10 @@ interface Props {
|
||||
isPropertyManager?: boolean
|
||||
}
|
||||
|
||||
const FILTER_OPTIONS: { value: FilterSource; label: string; color: string }[] = [
|
||||
{ value: 'ALL', label: 'Alle', color: '#1e3a5f' },
|
||||
{ value: 'PLATFORM', label: 'Plattform', color: '#1e3a5f' },
|
||||
{ value: 'MAISON_WORK', label: 'Maison Work', color: '#0369a1' },
|
||||
const FILTER_OPTIONS: { value: FilterSource; label: string }[] = [
|
||||
{ value: 'ALL', label: 'Alle' },
|
||||
{ value: 'PLATFORM', label: 'Plattform' },
|
||||
{ value: 'MAISON_WORK', label: 'Maison Work' },
|
||||
]
|
||||
|
||||
const SORT_OPTIONS: { value: SortBy; label: string }[] = [
|
||||
@@ -28,6 +28,9 @@ const SORT_OPTIONS: { value: SortBy; label: string }[] = [
|
||||
{ value: 'rent', label: 'Mietpreis' },
|
||||
]
|
||||
|
||||
const ACTIVE_CHIP = { bgcolor: '#152642', color: 'white', border: '1px solid #152642', fontWeight: 600 }
|
||||
const INACTIVE_CHIP = { bgcolor: 'transparent', color: 'text.secondary', border: '1px solid #e8e7e4', fontWeight: 400 }
|
||||
|
||||
export function ResultFilterBar({
|
||||
filterSource,
|
||||
onFilterChange,
|
||||
@@ -45,7 +48,7 @@ export function ResultFilterBar({
|
||||
{/* Source filter + toggles */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{FILTER_OPTIONS.map(({ value, label, color }) => {
|
||||
{FILTER_OPTIONS.map(({ value, label }) => {
|
||||
const active = filterSource === value
|
||||
return (
|
||||
<Chip
|
||||
@@ -54,12 +57,7 @@ export function ResultFilterBar({
|
||||
size="small"
|
||||
clickable
|
||||
onClick={() => onFilterChange(value)}
|
||||
sx={{
|
||||
bgcolor: active ? color : 'transparent',
|
||||
color: active ? 'white' : 'text.secondary',
|
||||
border: `1px solid ${active ? color : '#e2e8f0'}`,
|
||||
fontWeight: active ? 600 : 400,
|
||||
}}
|
||||
sx={active ? ACTIVE_CHIP : INACTIVE_CHIP}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -67,36 +65,28 @@ export function ResultFilterBar({
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||
|
||||
{/* Future Availability toggle */}
|
||||
<Chip
|
||||
size="small"
|
||||
clickable
|
||||
icon={<Zap size={11} color={showFutureAvailability ? '#7c3aed' : '#94a3b8'} />}
|
||||
icon={<Zap size={11} color={showFutureAvailability ? '#5b3f8a' : '#c4c4c4'} />}
|
||||
label="Future Availability"
|
||||
onClick={() => onShowFutureAvailabilityChange(!showFutureAvailability)}
|
||||
sx={{
|
||||
bgcolor: showFutureAvailability ? '#f3e8ff' : 'transparent',
|
||||
color: showFutureAvailability ? '#6d28d9' : 'text.disabled',
|
||||
border: `1px solid ${showFutureAvailability ? '#c4b5fd' : '#e2e8f0'}`,
|
||||
fontWeight: showFutureAvailability ? 600 : 400,
|
||||
textDecoration: showFutureAvailability ? 'none' : 'line-through',
|
||||
...(showFutureAvailability ? ACTIVE_CHIP : INACTIVE_CHIP),
|
||||
bgcolor: showFutureAvailability ? '#152642' : 'transparent',
|
||||
'& .MuiChip-icon': { ml: 0.75 },
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Eigene Objekte toggle — only for Property Managers */}
|
||||
{isPropertyManager && (
|
||||
<Chip
|
||||
size="small"
|
||||
clickable
|
||||
icon={<Building2 size={11} color={showOwnProperties ? '#1e3a5f' : '#94a3b8'} />}
|
||||
label="Eigene Objekte einblenden"
|
||||
icon={<Building2 size={11} color={showOwnProperties ? 'white' : '#c4c4c4'} />}
|
||||
label="Eigene Objekte"
|
||||
onClick={() => onShowOwnPropertiesChange?.(!showOwnProperties)}
|
||||
sx={{
|
||||
bgcolor: showOwnProperties ? 'rgba(30,58,95,0.10)' : 'transparent',
|
||||
color: showOwnProperties ? '#1e3a5f' : 'text.disabled',
|
||||
border: `1px solid ${showOwnProperties ? '#1e3a5f' : '#e2e8f0'}`,
|
||||
fontWeight: showOwnProperties ? 600 : 400,
|
||||
...(showOwnProperties ? ACTIVE_CHIP : INACTIVE_CHIP),
|
||||
'& .MuiChip-icon': { ml: 0.75 },
|
||||
}}
|
||||
/>
|
||||
@@ -113,11 +103,7 @@ export function ResultFilterBar({
|
||||
size="small"
|
||||
clickable
|
||||
onClick={() => onSortChange(value)}
|
||||
sx={{
|
||||
bgcolor: sortBy === value ? '#1e3a5f' : 'transparent',
|
||||
color: sortBy === value ? 'white' : 'text.secondary',
|
||||
border: `1px solid ${sortBy === value ? '#1e3a5f' : '#e2e8f0'}`,
|
||||
}}
|
||||
sx={sortBy === value ? ACTIVE_CHIP : INACTIVE_CHIP}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -125,16 +125,6 @@ export function UnifiedResultCard({ result, view = 'list' }: Props) {
|
||||
const listImageUrl = result.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? result.property.images?.[0]
|
||||
: undefined
|
||||
const listOverlay = result.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? resolveImageLabel({
|
||||
assetType: result.property.assetType,
|
||||
city: result.property.location.city,
|
||||
district: result.property.location.district,
|
||||
prestige: result.property.softFactors?.prestige,
|
||||
floorLevel: result.property.floorLevel,
|
||||
propertyId: result.property.id,
|
||||
})
|
||||
: null
|
||||
|
||||
return <MatchCardCompact vm={vm} imageUrl={listImageUrl} overlayTypeLabel={listOverlay?.type} />
|
||||
return <MatchCardCompact vm={vm} imageUrl={listImageUrl} />
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function UnifiedResultFeed({ results, view = 'list' }: Props) {
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{items.map(result => (
|
||||
<UnifiedResultCard key={result.matchId} result={result} view="list" />
|
||||
))}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function ReviewDetailPanel({
|
||||
sx={{
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
bgcolor: task.confidenceScore >= 0.7 ? '#1a7a4a' : task.confidenceScore >= 0.5 ? '#d97706' : '#c0392b',
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ReviewEntityType } from '../../domain/review'
|
||||
|
||||
const CONFIG: Record<ReviewEntityType, { label: string; color: string }> = {
|
||||
FUTURE_SIGNAL: { label: 'Zukunftssignal', color: '#7c3aed' },
|
||||
MATCH_EXPLANATION: { label: 'Match-Begründung', color: '#1e3a5f' },
|
||||
MATCH_EXPLANATION: { label: 'Match-Begründung', color: '#152642' },
|
||||
LOW_CONFIDENCE_MATCH: { label: 'Niedr. Konfidenz', color: '#ea580c' },
|
||||
CONTACT_RELEASE: { label: 'Kontaktfreigabe', color: '#0891b2' },
|
||||
AI_OUTPUT: { label: 'AI-Output', color: '#4f46e5' },
|
||||
|
||||
@@ -76,7 +76,7 @@ export function ReviewTaskCard({ task, isSelected, onSelect }: ReviewTaskCardPro
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: '#e8e7e4',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
bgcolor: task.confidenceScore >= 0.7 ? '#1a7a4a' : task.confidenceScore >= 0.5 ? '#d97706' : '#c0392b',
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ interface ScoreBarProps {
|
||||
}
|
||||
|
||||
export function ScoreBar({ label, value, weight, color, maxWidth = 200 }: ScoreBarProps) {
|
||||
const resolved = color ?? COLOR_MAP[matchScoreColor(value)] ?? '#1e3a5f'
|
||||
const resolved = color ?? COLOR_MAP[matchScoreColor(value)] ?? '#152642'
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
|
||||
@@ -6,26 +6,15 @@ export type BadgeSemanticVariant = keyof typeof BADGE_COLORS
|
||||
|
||||
export interface GenericBadgeProps {
|
||||
label: string
|
||||
/**
|
||||
* Semantic variant key from BADGE_COLORS (preferred).
|
||||
* Example: semanticVariant="confidenceHigh" instead of color="#1a7a4a"
|
||||
*/
|
||||
/** Semantic variant key from BADGE_COLORS (preferred). */
|
||||
semanticVariant?: BadgeSemanticVariant
|
||||
/**
|
||||
* Raw hex color — use only when no semantic variant fits.
|
||||
* Prefer semanticVariant to avoid hardcoded colors.
|
||||
*/
|
||||
/** Raw hex color — use only when no semantic variant fits. */
|
||||
color?: string
|
||||
/** transparent: semi-opaque bg (color + 18% opacity) | solid: filled bg, white text */
|
||||
variant?: 'transparent' | 'solid'
|
||||
/** Adds a 1px border at color 25% opacity (transparent variant only) */
|
||||
showBorder?: boolean
|
||||
/** fontWeight 700 instead of 600 */
|
||||
/** outlined: border + text, no bg | solid: filled bg, white text */
|
||||
variant?: 'outlined' | 'solid'
|
||||
bold?: boolean
|
||||
/** Optional lucide / MUI icon element — sized by the caller */
|
||||
icon?: ReactElement
|
||||
size?: 'small' | 'medium'
|
||||
/** Wraps the badge in a Tooltip */
|
||||
tooltip?: string
|
||||
ariaLabel?: string
|
||||
}
|
||||
@@ -34,15 +23,14 @@ export function GenericBadge({
|
||||
label,
|
||||
semanticVariant,
|
||||
color: colorProp,
|
||||
variant = 'transparent',
|
||||
showBorder = false,
|
||||
variant = 'outlined',
|
||||
bold = false,
|
||||
icon,
|
||||
size = 'small',
|
||||
tooltip,
|
||||
ariaLabel,
|
||||
}: GenericBadgeProps) {
|
||||
const color = semanticVariant ? BADGE_COLORS[semanticVariant] : (colorProp ?? '#64748b')
|
||||
const color = semanticVariant ? BADGE_COLORS[semanticVariant] : (colorProp ?? '#9a9a9a')
|
||||
|
||||
const chip = (
|
||||
<Chip
|
||||
@@ -51,11 +39,11 @@ export function GenericBadge({
|
||||
icon={icon}
|
||||
aria-label={ariaLabel}
|
||||
sx={{
|
||||
fontWeight: bold ? 700 : 600,
|
||||
fontSize: size === 'small' ? '0.7rem' : '0.8125rem',
|
||||
bgcolor: variant === 'solid' ? color : `${color}18`,
|
||||
fontWeight: bold ? 700 : 500,
|
||||
fontSize: size === 'small' ? '0.75rem' : '0.8125rem',
|
||||
bgcolor: variant === 'solid' ? color : 'transparent',
|
||||
color: variant === 'solid' ? 'white' : color,
|
||||
border: variant === 'transparent' && showBorder ? `1px solid ${color}40` : 'none',
|
||||
border: variant === 'outlined' ? `1px solid ${color}` : 'none',
|
||||
'& .MuiChip-icon': { ml: 0.5 },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -114,7 +114,7 @@ export function LocationPreview({ imageUrl, lat, lng, address, cityLabel, height
|
||||
'&:hover': { bgcolor: 'white' },
|
||||
}}
|
||||
>
|
||||
<Map size={14} color="#1e3a5f" />
|
||||
<Map size={14} color="#152642" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function PropertyMap({ lat, lng, label, height = 260, searchCenterLat, se
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#1e3a5f', flexShrink: 0 }} />
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#152642', flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.67rem', color: '#334155' }}>Objekt</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user