import { useCallback, useMemo, useState } from 'react' import { Alert, Box, Tab, Tabs } from '@mui/material' import { LeadTable } from './LeadTable' import { PanelLoadingState, ErrorState } from '../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' interface Props { /** Scrollcontainer der Seite — die geöffnete Zeile gleitet darin nach oben. */ scrollRef: React.RefObject } /** * Der Exposé-Bereich auf Livias Arbeitsplatz (Runde 4, §8; Runde 8, §1). * * Aus `MyListings.tsx` herausgelöst, als die Recherche mit Runde 8 zur * Hauptaufgabe wurde. Die Exposé-Funktion ist damit nachgeordnet, aber * unverändert vollständig — Leadliste, dreistufiger Assistent, Pflichtfelder * und Export arbeiten weiter wie zuvor. */ export function ExposeLeadSection({ scrollRef }: Props) { const [tab, setTab] = useState(EXPOSE_LEAD_TABS.ACTIVE) const [selectedId, setSelectedId] = useState(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, scrollRef]) const handleTabChange = useCallback((next: ExposeLeadTab) => { setSelectedId(null) setTab(next) }, []) if (isLoading) return if (isError) return refetch()} /> return ( Nachgeordnete Aufgabe. Livias Schwerpunkt liegt seit Runde 8 auf der Marktrecherche — Exposés entstehen weiterhin hier, wenn ein Objekt vermarktet werden soll. handleTabChange(v)} sx={{ mb: 2, '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }} > {Object.values(EXPOSE_LEAD_TABS).map(value => ( ))} ) }