feat: Reminder Manager + Schattenmarkt-Freigabe + mock data overhaul

- Add Reminder Manager page (/supply/reminder-manager) with KPI bar,
  filter bar, list/card feed, and detail drawer (7 sections incl.
  activity log, snooze, complete, dismiss actions)
- Add Schattenmarkt-Freigabe toggle on PropertyDetailView: Verwaltung
  can opt-in properties for early market exposure before contract expiry
- Auto-generate FutureSignal cards via useSchattenmarktSignals hook
  when leaseEndDate - leadTimeMonths <= MOCK_TODAY
- Fix useUnifiedResults: use property.resultType as fallback (was
  defaulting everything to VERIFIED_PORTFOLIO)
- Fix demand Results: hide VERIFIED_PORTFOLIO from non-manager users
  even when showOwnProperties toggle was previously enabled
- Fix AppShell: redirect to allowed workspace on user role switch
- Fix 3 wrong match scores (match-002: 93→62, match-009: 86→55,
  match-017: 87→52)
- Add 7 new match records (match-050–056) for need-001, need-002,
  need-011
- Add need-011 (Retail Bern Innenstadt)
- Add prop-031–036 (EXTERNAL_MARKET / MAISON_WORK / FUTURE_AVAILABILITY)
- Fix duplicate image URLs across all properties
- Add signal-011 (Bern Altstadt, Mode Boutique) + propertyId to
  signal-001

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 11:52:24 +02:00
parent 0663757cde
commit 9f391d17cb
36 changed files with 3011 additions and 71 deletions
+97
View File
@@ -0,0 +1,97 @@
import { Box, Typography } from '@mui/material'
import { useReminders } from '../../hooks/useReminders'
import { useReminderStore } from '../../stores/reminderStore'
import { ReminderListRow } from './ReminderListRow'
import { ReminderCard } from './ReminderCard'
import { ReminderSkeleton } from './ReminderSkeleton'
import { ReminderEmptyState } from './ReminderEmptyState'
import type { Reminder } from '../../domain/reminder'
const LIST_HEADER_COLS = '100px 130px 1fr 140px 90px 100px 90px'
function applyFilters(
reminders: Reminder[],
filterType: string,
filterStatus: string,
filterPriority: string,
searchQuery: string,
): Reminder[] {
return reminders.filter(r => {
if (filterType !== 'ALL' && r.type !== filterType) return false
if (filterStatus !== 'ALL' && r.status !== filterStatus) return false
if (filterPriority !== 'ALL' && r.priority !== filterPriority) return false
if (searchQuery) {
const q = searchQuery.toLowerCase()
const haystack = `${r.propertyTitle} ${r.propertyCity} ${r.tenantName}`.toLowerCase()
if (!haystack.includes(q)) return false
}
return true
})
}
export function ReminderFeed() {
const { data, isLoading } = useReminders()
const {
filterType, filterStatus, filterPriority, searchQuery, viewMode,
setFilterType, setFilterStatus, setFilterPriority, setSearchQuery,
} = useReminderStore()
if (isLoading) return <ReminderSkeleton />
const reminders = data ?? []
const filtered = applyFilters(reminders, filterType, filterStatus, filterPriority, searchQuery)
function resetFilters() {
setFilterType('ALL')
setFilterStatus('ALL')
setFilterPriority('ALL')
setSearchQuery('')
}
if (filtered.length === 0) {
return <ReminderEmptyState onReset={resetFilters} />
}
if (viewMode === 'card') {
return (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: 2,
}}
>
{filtered.map(r => (
<ReminderCard key={r.id} reminder={r} />
))}
</Box>
)
}
return (
<Box sx={{ border: '1px solid #e2e8f0', borderRadius: 1, overflow: 'hidden' }}>
{/* Table header */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: LIST_HEADER_COLS,
gap: 1,
px: 2,
py: 1,
bgcolor: '#f8fafc',
borderBottom: '1px solid #e2e8f0',
}}
>
{['Priorität', 'Typ', 'Objekt / Mieter', 'Fälligkeit', 'Fläche', 'Status', 'Aktionen'].map(h => (
<Typography key={h} variant="caption" sx={{ fontWeight: 600, color: '#64748b', fontSize: '0.7rem' }}>
{h}
</Typography>
))}
</Box>
{filtered.map(r => (
<ReminderListRow key={r.id} reminder={r} />
))}
</Box>
)
}