import { useCallback, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router' import { Box, Tab, Tabs } from '@mui/material' import { AgentWorkspaceHero } from '../../components/team' import { LeadTable } from '../../components/expose' import { LoadingPage, ErrorState } from '../../components/ui' import { useExposeLeads } from '../../hooks/useExposeLeads' import { useProperties } from '../../hooks/useProperties' import { ExposeLeadStatus } from '../../domain/exposeLead' import { EXPOSE_LEAD_TABS, EXPOSE_LEAD_TAB_LABELS } from '../../lib/constants' import type { ExposeLeadTab } from '../../lib/constants' import { agentWorkspaceById } from '../../lib/agentWorkspaces' const LIVIA = agentWorkspaceById('livia')! const PARAM_TAB = 'tab' function resolveTab(raw: string | null): ExposeLeadTab { return raw === EXPOSE_LEAD_TABS.ARCHIVED ? EXPOSE_LEAD_TABS.ARCHIVED : EXPOSE_LEAD_TABS.ACTIVE } /** * Livia — Exposé Master (Runde 4, §8). * * Die frühere Inserate-Seite ist vollständig ersetzt: Titel, Beschreibung, der * Knopf «Neues Inserat» und die Inserate-Tabelle sind entfallen. Ziel der Seite * ist die schnelle Verarbeitung von Leads, die Nora erkannt und ein * Bewirtschafter zur Exposé-Erstellung weitergeleitet hat. */ export default function MyListings() { const [params, setParams] = useSearchParams() const tab = resolveTab(params.get(PARAM_TAB)) const [selectedId, setSelectedId] = useState(null) const scrollRef = useRef(null) const { data: leads = [], isLoading, isError, refetch } = useExposeLeads() const { data: properties = [] } = useProperties() const visible = useMemo(() => { const wanted = tab === EXPOSE_LEAD_TABS.ARCHIVED ? ExposeLeadStatus.ARCHIVED : ExposeLeadStatus.ACTIVE return leads .filter(l => l.status === wanted) .sort((a, b) => b.receivedAt.localeCompare(a.receivedAt)) }, [leads, tab]) /** * Beim Öffnen gleitet die gewählte Zeile an den oberen Rand des sichtbaren * Inhaltsbereichs; die nachfolgenden Leads rücken nach unten und dürfen aus * dem Blickfeld verschwinden (Runde 4, §8.4). Umgesetzt mit `scrollTo` auf dem * bestehenden Scrollcontainer — dafür braucht es keine Animationsbibliothek. * * Bei `prefers-reduced-motion` wird ohne Bewegung direkt gesprungen. */ const handleSelect = useCallback((id: string, row: HTMLElement | null) => { const next = id === selectedId ? null : id setSelectedId(next) if (!next || !row || !scrollRef.current) return const container = scrollRef.current const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches const top = container.scrollTop + row.getBoundingClientRect().top - container.getBoundingClientRect().top // Nach dem Zustandswechsel scrollen, sonst misst der Browser die alte Höhe. requestAnimationFrame(() => { container.scrollTo({ top, behavior: reduced ? 'auto' : 'smooth' }) }) }, [selectedId]) const handleTabChange = useCallback((next: ExposeLeadTab) => { setSelectedId(null) setParams({ [PARAM_TAB]: next }) }, [setParams]) if (isLoading) return if (isError) return refetch()} /> return ( handleTabChange(v)} sx={{ mb: 2, '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }} > {Object.values(EXPOSE_LEAD_TABS).map(value => ( ))} {/* Weissraum unter der Liste, damit auch die letzte Zeile an den oberen Rand gleiten kann. */} ) }