From a521155bb220822f4a3c91b89b91401b33d77254 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Tue, 19 May 2026 22:36:27 +0200 Subject: [PATCH] feat: mix Schattenmarkt into result tiers, add filter toggle Co-Authored-By: Claude Sonnet 4.6 --- src/components/results/ResultFilterBar.tsx | 87 ++++++++++++++------ src/components/results/UnifiedResultFeed.tsx | 54 +----------- src/pages/demand/Results.tsx | 12 ++- 3 files changed, 72 insertions(+), 81 deletions(-) diff --git a/src/components/results/ResultFilterBar.tsx b/src/components/results/ResultFilterBar.tsx index 9e40933..6df071e 100644 --- a/src/components/results/ResultFilterBar.tsx +++ b/src/components/results/ResultFilterBar.tsx @@ -1,7 +1,8 @@ -import { Box, Card, Chip, Stack, Typography } from '@mui/material' +import { Box, Card, Chip, Divider, Stack, Typography } from '@mui/material' +import { Zap } from 'lucide-react' import type { ResultType } from '../../domain/enums' -type FilterSource = ResultType | 'ALL' +type FilterSource = Exclude | 'ALL' type SortBy = 'score' | 'rent' | 'area' interface Props { @@ -9,13 +10,14 @@ interface Props { onFilterChange: (v: FilterSource) => void sortBy: SortBy onSortChange: (v: SortBy) => void + showSchattenmarkt: boolean + onShowSchattenmarktChange: (v: boolean) => void } const FILTER_OPTIONS: { value: FilterSource; label: string; color: string }[] = [ - { value: 'ALL', label: 'Alle', color: '#1e3a5f' }, - { value: 'VERIFIED_PORTFOLIO', label: 'Verified Portfolio', color: '#1e3a5f' }, - { value: 'EXTERNAL_MARKET', label: 'Marktinserate', color: '#d97706' }, - { value: 'FUTURE_AVAILABILITY', label: 'Zukunftssignale', color: '#7c3aed' }, + { value: 'ALL', label: 'Alle', color: '#1e3a5f' }, + { value: 'VERIFIED_PORTFOLIO', label: 'Portfolio', color: '#1e3a5f' }, + { value: 'EXTERNAL_MARKET', label: 'Marktinserate', color: '#d97706' }, ] const SORT_OPTIONS: { value: SortBy; label: string }[] = [ @@ -24,30 +26,61 @@ const SORT_OPTIONS: { value: SortBy; label: string }[] = [ { value: 'rent', label: 'Mietpreis' }, ] -export function ResultFilterBar({ filterSource, onFilterChange, sortBy, onSortChange }: Props) { +export function ResultFilterBar({ + filterSource, + onFilterChange, + sortBy, + onSortChange, + showSchattenmarkt, + onShowSchattenmarktChange, +}: Props) { return ( - - {FILTER_OPTIONS.map(({ value, label, color }) => { - const active = filterSource === value - return ( - onFilterChange(value)} - sx={{ - bgcolor: active ? color : 'transparent', - color: active ? 'white' : 'text.secondary', - border: `1px solid ${active ? color : '#e2e8f0'}`, - fontWeight: active ? 600 : 400, - }} - /> - ) - })} - + {/* Source filter + Schattenmarkt toggle */} + + + {FILTER_OPTIONS.map(({ value, label, color }) => { + const active = filterSource === value + return ( + onFilterChange(value)} + sx={{ + bgcolor: active ? color : 'transparent', + color: active ? 'white' : 'text.secondary', + border: `1px solid ${active ? color : '#e2e8f0'}`, + fontWeight: active ? 600 : 400, + }} + /> + ) + })} + + + + + {/* Schattenmarkt toggle */} + } + label="Schattenmarkt" + onClick={() => onShowSchattenmarktChange(!showSchattenmarkt)} + sx={{ + bgcolor: showSchattenmarkt ? '#f3e8ff' : 'transparent', + color: showSchattenmarkt ? '#6d28d9' : 'text.disabled', + border: `1px solid ${showSchattenmarkt ? '#c4b5fd' : '#e2e8f0'}`, + fontWeight: showSchattenmarkt ? 600 : 400, + textDecoration: showSchattenmarkt ? 'none' : 'line-through', + '& .MuiChip-icon': { ml: 0.75 }, + }} + /> + + + {/* Sort */} Sortierung: {SORT_OPTIONS.map(({ value, label }) => ( diff --git a/src/components/results/UnifiedResultFeed.tsx b/src/components/results/UnifiedResultFeed.tsx index 64ce79c..dbb1689 100644 --- a/src/components/results/UnifiedResultFeed.tsx +++ b/src/components/results/UnifiedResultFeed.tsx @@ -1,5 +1,4 @@ -import { Alert, Box, Chip, Divider, Typography } from '@mui/material' -import { Zap } from 'lucide-react' +import { Box, Chip, Divider, Typography } from '@mui/material' import type { UnifiedMatchResult } from '../../domain/unifiedResult' import { UnifiedResultCard } from './UnifiedResultCard' import { SCORE_THEME } from '../shared/scoreTheme' @@ -73,46 +72,10 @@ function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProp ) } -function SchattenmarktSectionHeader({ count }: { count: number }) { - return ( - - - - - - - Schattenmarkt - - - - - - } - sx={{ - mb: 2, - bgcolor: '#faf5ff', - border: '1px solid #e9d5ff', - color: '#4c1d95', - '& .MuiAlert-icon': { color: '#7c3aed' }, - }} - > - Was sind Schattenmarkt-Signale? Die KI hat aus öffentlichen Quellen (Stelleninseraten, Pressemeldungen, Baubewilligungen etc.) Hinweise gefunden, dass in Ihrer Zielregion in absehbarer Zeit Flächen frei werden könnten. Dies sind keine bestätigten Angebote — aber frühzeitige Hinweise, die Sie im Auge behalten sollten. Klicken Sie auf Details für Quellen und die vollständige KI-Analyse. - - - ) -} - export function UnifiedResultFeed({ results, view = 'list' }: Props) { - const realResults = results.filter(r => r.resultType !== 'FUTURE_AVAILABILITY') - const signalResults = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY') - - const tiers = TIER_CONFIG.map(t => ({ ...t, items: realResults.filter(t.filter) })) + // All results — real properties and Schattenmarkt signals — are sorted into + // the same score tiers so they appear in a single unified ranked list. + const tiers = TIER_CONFIG.map(t => ({ ...t, items: results.filter(t.filter) })) .filter(t => t.items.length > 0) function renderCards(items: UnifiedMatchResult[]) { @@ -133,7 +96,6 @@ export function UnifiedResultFeed({ results, view = 'list' }: Props) { return ( - {/* Real properties — scored tiers */} {tiers.map((tier, idx) => ( ))} - - {/* Schattenmarkt signals — dedicated section */} - {signalResults.length > 0 && ( - - - {renderCards(signalResults)} - - )} ) } diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index b9a3b40..d13ad0a 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -16,7 +16,7 @@ import { AddToShortlistDialog } from '../../components/shortlist' import type { ResultType } from '../../domain/enums' import type { UnifiedMatchResult } from '../../domain/unifiedResult' -type FilterSource = ResultType | 'ALL' +type FilterSource = Exclude | 'ALL' type SortBy = 'score' | 'rent' | 'area' function sortResults(results: UnifiedMatchResult[], sortBy: SortBy): UnifiedMatchResult[] { @@ -36,6 +36,7 @@ export default function Results() { const queryClient = useQueryClient() const [filterSource, setFilterSource] = useState('ALL') const [sortBy, setSortBy] = useState('score') + const [showSchattenmarkt, setShowSchattenmarkt] = useState(true) const [view, setView] = useState<'list' | 'grid'>(() => (localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list' ) @@ -67,8 +68,9 @@ export default function Results() { const { data: results = [], isLoading } = useUnifiedResults(activeNeed?.id) - const filtered = - filterSource === 'ALL' ? results : results.filter(r => r.resultType === filterSource) + const filtered = results + .filter(r => filterSource === 'ALL' || r.resultType === filterSource) + .filter(r => showSchattenmarkt || r.resultType !== 'FUTURE_AVAILABILITY') const sorted = sortResults(filtered, sortBy) @@ -167,12 +169,14 @@ export default function Results() { onFilterChange={setFilterSource} sortBy={sortBy} onSortChange={setSortBy} + showSchattenmarkt={showSchattenmarkt} + onShowSchattenmarktChange={setShowSchattenmarkt} /> {isLoading ? ( ) : sorted.length === 0 ? ( - + ) : ( )}