Files
property-match/archive/livia/src/pages/supply/MyListings.tsx
T
Benjamin SutterandClaude Opus 5 2037e5a463 docs(agenten): Fallakte Livia und Agenten-Briefing sichern
`LIVIA_PROFIL_UND_PROMPT.md` hält Livia als Exposé Master fest, wie sie vor
Runde 8 war: Personalblatt, Aufgabenkatalog, Kanäle und Systeme, das
Exposé-Dossier mit elf Bereichen und neun Pflichtfeldern, die
`missingFacts`-Regel der Textmaschine — dazu ein System-Prompt, mit dem sich
diese Fassung ausserhalb des Repositories rekonstruieren lässt.

`archive/livia/` ergänzt die Beschreibung um die Wiederherstellung: die 23
Dateien der Exposé-Funktion als wörtliche Kopie im Original-Pfadlayout sowie
ein Manifest jeder Stelle, an der Livia in geteilten Dateien verdrahtet war.

Beides ist mit Runde 8 nicht überholt, sondern der Referenzstand: die
Exposé-Funktion wurde nicht entfernt, sondern nachgeordnet. Das Manifest
beschreibt allerdings den Rückbau eines vollständigen Entfernens und damit ein
Szenario, das so nicht eingetreten ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:35:57 +02:00

114 lines
4.6 KiB
TypeScript

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<string | null>(null)
const scrollRef = useRef<HTMLDivElement>(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 <LoadingPage />
if (isError) return <ErrorState message="Die Leads konnten nicht geladen werden." onRetry={() => refetch()} />
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box ref={scrollRef} sx={{ flex: 1, overflowY: 'auto' }}>
<AgentWorkspaceHero agentId={LIVIA.id} name={LIVIA.name} role={LIVIA.role} />
<Box sx={{ px: 3, pb: 5 }}>
<Tabs
value={tab}
onChange={(_, v: ExposeLeadTab) => handleTabChange(v)}
sx={{ mb: 2, '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }}
>
{Object.values(EXPOSE_LEAD_TABS).map(value => (
<Tab key={value} value={value} label={EXPOSE_LEAD_TAB_LABELS[value]} />
))}
</Tabs>
<LeadTable
leads={visible}
properties={properties}
selectedId={selectedId}
onSelect={handleSelect}
withWorkspace={tab === EXPOSE_LEAD_TABS.ACTIVE}
emptyText={
tab === EXPOSE_LEAD_TABS.ACTIVE
? 'Keine aktiven Leads. Sobald Nora ein Signal erkennt und es weitergeleitet wird, erscheint es hier.'
: 'Noch keine archivierten Leads.'
}
/>
{/* Weissraum unter der Liste, damit auch die letzte Zeile an den
oberen Rand gleiten kann. */}
<Box sx={{ height: '60vh' }} />
</Box>
</Box>
</Box>
)
}