diff --git a/src/components/demand/NeedInput.tsx b/src/components/demand/NeedInput.tsx index de28635..840fdad 100644 --- a/src/components/demand/NeedInput.tsx +++ b/src/components/demand/NeedInput.tsx @@ -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' @@ -242,31 +242,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) { )} - {/* Anonymous search */} - - - - Anonyme Suche - - - Firmenname wird nicht an Vermieter übermittelt - - - set({ ...c, isAnonymous: e.target.checked })} - sx={{ '& .MuiSwitch-thumb': { bgcolor: c.isAnonymous ? '#7c3aed' : undefined }, '& .Mui-checked + .MuiSwitch-track': { bgcolor: '#7c3aed' } }} - /> - } - label="" - sx={{ m: 0 }} - /> - - - + ) } diff --git a/src/components/match-card/MatchCardViewModel.ts b/src/components/match-card/MatchCardViewModel.ts index 40fa3a4..c01f434 100644 --- a/src/components/match-card/MatchCardViewModel.ts +++ b/src/components/match-card/MatchCardViewModel.ts @@ -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 diff --git a/src/components/results/UnifiedResultCard.tsx b/src/components/results/UnifiedResultCard.tsx index 84e634a..8ae3934 100644 --- a/src/components/results/UnifiedResultCard.tsx +++ b/src/components/results/UnifiedResultCard.tsx @@ -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 + return } diff --git a/src/components/results/UnifiedResultFeed.tsx b/src/components/results/UnifiedResultFeed.tsx index dbb1689..7be7de6 100644 --- a/src/components/results/UnifiedResultFeed.tsx +++ b/src/components/results/UnifiedResultFeed.tsx @@ -86,7 +86,7 @@ export function UnifiedResultFeed({ results, view = 'list' }: Props) { ))} ) : ( - + {items.map(result => ( ))} diff --git a/src/components/supply/PropertyIntelligenceCard.tsx b/src/components/supply/PropertyIntelligenceCard.tsx index ca14334..489923d 100644 --- a/src/components/supply/PropertyIntelligenceCard.tsx +++ b/src/components/supply/PropertyIntelligenceCard.tsx @@ -43,19 +43,26 @@ export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ onSelect(p.id)} sx={{ - borderRadius: 1, + borderRadius: 2, overflow: 'hidden', border: `1px solid ${DS_BORDER.default}`, bgcolor: '#ffffff', display: 'flex', flexDirection: 'column', cursor: 'pointer', - transition: 'border-color 0.15s', - '&:hover': { borderColor: '#c8c7c4' }, + position: 'relative', + zIndex: 0, + transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.15s', + '&:hover': { + borderColor: '#b0aead', + transform: 'scale(1.025)', + boxShadow: '0 10px 32px rgba(0,0,0,0.13)', + zIndex: 1, + }, }} > {/* ── Image — clean, no text overlay ── */} - + {hasImage ? ( @@ -78,7 +85,7 @@ export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ {/* ── Body ── */} - + {/* Title + location + status inline */} @@ -130,36 +137,32 @@ export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ - {/* Property spec chips */} - - {p.hardFacts?.fitOut && (() => { - const meta = FIT_OUT_META[p.hardFacts!.fitOut!] ?? { label: p.hardFacts!.fitOut!, tip: '' } - return ( - - - - ) - })()} - {p.contractDurationMonths && ( - - )} - - - {/* Data completeness */} - - - - Datenvollständigkeit - - = 0.75 ? '#b8975a' : DS_TEXT.muted }}> + {/* Chips + Datenvollständigkeit in einer Zeile */} + + + {p.hardFacts?.fitOut && (() => { + const meta = FIT_OUT_META[p.hardFacts!.fitOut!] ?? { label: p.hardFacts!.fitOut!, tip: '' } + return ( + + + + ) + })()} + {p.contractDurationMonths && ( + + )} + + + Daten + + = 0.75 ? '#b8975a' : DS_TEXT.muted }}> {confPct}% - diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index 15c2617..6ff1eca 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -1,11 +1,10 @@ import { useState, useMemo, useEffect } from 'react' -import { Box, Button, Card, Chip, Typography } from '@mui/material' +import { Box, Button, Typography } from '@mui/material' import { Zap } from 'lucide-react' import { useNavigate, useLocation } from 'react-router' import { useQueryClient } from '@tanstack/react-query' import { useUnifiedResults } from '../../hooks/useUnifiedResults' import { useNeeds } from '../../hooks/useNeeds' -import { DecisionContextPanel } from '../../components/ui' import { FeedEmptyState, FeedSkeleton, @@ -72,16 +71,14 @@ export default function Results() { const isStaff = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN' // Counts over the full result set — single pass, stable across filter changes - const { platformCount, maisonWorkCount, futureCount, missingDataCount } = useMemo(() => { - let platform = 0, maison = 0, future = 0, missing = 0 + const { platformCount, maisonWorkCount, futureCount } = useMemo(() => { + let platform = 0, maison = 0, future = 0 for (const r of results) { if (r.resultType === 'VERIFIED_PORTFOLIO') platform++ else if (r.resultType === 'MAISON_WORK') maison++ else if (r.resultType === 'FUTURE_AVAILABILITY') future++ - if ('match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) && - ((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0) missing++ } - return { platformCount: platform, maisonWorkCount: maison, futureCount: future, missingDataCount: missing } + return { platformCount: platform, maisonWorkCount: maison, futureCount: future } }, [results]) // Filter + sort in one memo — only reruns when inputs change @@ -98,7 +95,6 @@ export default function Results() { return sortResults(filtered, sortBy) }, [results, filterSource, sortBy, showFutureAvailability, showOwnProperties]) - const strongCount = useMemo(() => sorted.filter(r => r.matchScore >= 80).length, [sorted]) return ( @@ -113,103 +109,27 @@ export default function Results() { onViewChange={v => { setView(v); localStorage.setItem('view-results', v) }} /> - {!isLoading && results.length > 0 && ( - 0 ? [{ label: 'starke Treffer (≥80)', value: strongCount, severity: 'positive' as const }] : []), - ...(maisonWorkCount > 0 ? [{ label: 'Maison Work', value: maisonWorkCount, severity: 'neutral' as const }] : []), - ...(futureCount > 0 ? [{ label: 'Zukunftssignale', value: futureCount, severity: 'neutral' as const }] : []), - ...(missingDataCount > 0 ? [{ label: 'mit Datenlücken', value: missingDataCount, severity: 'warning' as const }] : []), - ]} - risks={[ - ...(missingDataCount > 0 ? [`${missingDataCount} Treffer mit fehlenden Daten — Einschätzung eingeschränkt`] : []), - ]} - actions={[ - { label: 'Vergleich öffnen', onClick: () => navigate('/demand/compare') }, - { label: 'Suche anpassen', onClick: () => navigate('/demand/ai-search') }, - ]} - /> - )} - + + {/* Aktive Suche */} {activeNeed && ( - - - - - Aktive Suche: {activeNeed.companyName} - - - - Typ: {activeNeed.assetType} - - - Fläche: {activeNeed.requiredArea.min}–{activeNeed.requiredArea.max} m² - - - Standort: {activeNeed.preferredLocations.join(', ')} - - {activeNeed.budgetRange && activeNeed.budgetRange.maxPerSqm > 0 && ( - - Budget: max. CHF {activeNeed.budgetRange.maxPerSqm}/m²/Jahr - - )} - {activeNeed.timing?.earliestMoveIn && !isNaN(new Date(activeNeed.timing.earliestMoveIn).getTime()) && ( - - Bezug ab: {new Date(activeNeed.timing.earliestMoveIn).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })} - - )} - {activeNeed.searchRadius && ( - - Radius: {activeNeed.searchRadius} km - - )} - {activeNeed.requiresDivisibility && activeNeed.minDivisibleUnit && ( - - Teilbar ab: {activeNeed.minDivisibleUnit} m² - - )} - {activeNeed.requiredFitOut && ( - - Ausbaustandard: min. { - activeNeed.requiredFitOut === 'BASIC' ? 'Basisausbau' : - activeNeed.requiredFitOut === 'FULL' ? 'Vollausbau' : 'Premiumausbau' - } - - )} - {activeNeed.fitOutBudgetMaxPerSqm && ( - - Ausbaubudget: max. CHF {activeNeed.fitOutBudgetMaxPerSqm}/m² - - )} - {activeNeed.mustCriteriaText && activeNeed.mustCriteriaText.length > 0 && ( - - Must-haves: {activeNeed.mustCriteriaText.slice(0, 3).join(' · ')} - - )} - - - - {activeNeed.isAnonymous && ( - - )} - - + + + + {activeNeed.companyName} + + {activeNeed.assetType} + {activeNeed.requiredArea.min}–{activeNeed.requiredArea.max} m² + {activeNeed.preferredLocations.join(', ')} + {activeNeed.budgetRange?.maxPerSqm > 0 && ( + max. CHF {activeNeed.budgetRange.maxPerSqm}/m²/J + )} - + + )} (() => (localStorage.getItem('view-properties') as 'list' | 'grid') ?? 'list' ) + const [columns, setColumns] = useState<3 | 5 | 10>(() => + (Number(localStorage.getItem('view-properties-cols')) as 3 | 5 | 10) || 3 + ) const [headerOpen, setHeaderOpen] = useState(() => localStorage.getItem('props-header-open') !== 'false' ) @@ -109,10 +112,31 @@ export default function Properties() { title="Objektverwaltung" subtitle={`${filtered.length} von ${properties.length} Objekten`} secondaryActions={ - { setView(v); localStorage.setItem('view-properties', v) }} - /> + + {view === 'grid' && ( + + {([3, 5, 10] as const).map(n => ( + { setColumns(n); localStorage.setItem('view-properties-cols', String(n)) }} + sx={{ + width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', fontSize: '0.72rem', fontWeight: 600, + bgcolor: columns === n ? '#152642' : 'transparent', + color: columns === n ? 'white' : '#64748b', + '&:hover': { bgcolor: columns === n ? '#162d4a' : '#f1f5f9' }, + }} + > + {n} + + ))} + + )} + { setView(v); localStorage.setItem('view-properties', v) }} + /> + } /> @@ -201,7 +225,7 @@ export default function Properties() { {view === 'grid' ? ( - + {filtered.map(p => ( ))}