Files
property-match/src/components/market-leads/MarketLeadPanels.tsx
T
Benjamin SutterandClaude Opus 5 9ed21e33a7 feat(runde-5): Startseite mit Agenten-Meetingraum, sticky Chat und Noras Kanäle
Hauptnavigation und Startseite (§2)
- Hauptseite «Übersicht» samt Route, Menüeintrag, Hook, Service und den nur
  dort verwendeten Widgets entfernt; /supply/dashboard leitet auf die
  Startseite um, damit Lesezeichen nicht ins Leere laufen.
- «Meine Objekte» heisst im Menü «Startseite»; die frühere Überschrift
  «Objektverwaltung» heisst jetzt «Meine Objekte».
- Die Startseite scrollt als Ganzes. Die Höhe des Meetingraums rechnet gegen
  die Viewport-Höhe, damit Titel und Balken «Filter & Übersicht» beim Einstieg
  auf jeder Monitorgrösse sichtbar bleiben (geprüft 1280–1920 px).

Agenten-Meetingraum (§3)
- Zentralperspektive aus clip-path-Flächen, ohne 3D-Engine und ohne
  Animationsbibliothek: rechts geschlossene Wand, links und gegenüber
  boden- bis deckenhohe Glasfronten mit Blick auf Rhein, Basler Münster,
  Altstadt und Wettsteinbrücke.
- Sitzordnung, Zielrouten, Grössen und Plakettenversatz liegen in einer
  zentralen Konfiguration (meetingRoomSeats.ts).
- Neuer Agent Giorgio, Personalverwalter, sitzt vis-à-vis und führt in
  «Meine Agenten». Er steht bewusst nicht in AGENT_WORKSPACES: er hat keine
  Arbeitsseite, sondern ist die Adminfläche selbst.
- Auf schmalen Schirmen tritt eine vereinfachte Kachelanordnung an die Stelle
  der Perspektive; ausgeblendet wird kein Agent.

Sticky Chat bei allen Agenten (§5, §9–§11)
- Scrollt der Chatkopf unter den globalen Balken, übernimmt dort eine kompakte
  Fassung mit Porträt, Name, Frage und Eingabefeld.
- Ein gemeinsamer Zustand (agentChatStore) statt zweier Chat-Instanzen:
  Eingabetext und Fokus überstehen den Wechsel in beide Richtungen.
- Noras Seite scrollte bisher gar nicht — Chatkopf ausserhalb des
  Scrollbereichs, zwei getrennt scrollende Spalten darunter. Behoben über den
  Seitenaufbau, nicht über eine Nora-Sonderlösung.

Nora: CRM-Kanal und gemeinsame Lead-Liste (§12, §13)
- Dritter Kanal «CRM» mit eigenem Domänentyp, Mockdaten, Provider, Service und
  Hook nach dem bestehenden Schichtenmuster.
- Die Reiter «KI-Signale»/«Netzwerk» sind einer gemeinsamen Liste gewichen.
  Jede Zeile trägt ihre Kanalherkunft als Badge; die Herkunft steht im
  Datenmodell (LeadChannel) und wird nicht aus der Darstellung geraten.
- Der frühere Reiterbalken ist der Filter «Kanaltyp» mit Alle/KI Signal/
  Netzwerk/CRM, Standard «Alle», mit Empty State und Rücksetzung.

Produkttour und Profilmenü (§8)
- Tour auf die neue Struktur umgeschrieben: Startseite, Meetingraum, Agenten,
  sticky Chat, Leadkanäle. «Übersicht» kommt nicht mehr vor, «Meine Objekte»
  bezeichnet nur noch den Objektbereich.
- Bereich «Demo-Modus» mit «Verwaltung» und «Bürosuche» entfernt.

Geprüft: 413 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925).
Im Browser: Navigation, Agentenklicks aller sechs Plätze, Kanalfilter und
Detailschubladen, Sticky Chat bei vier Agenten, 1280–1920 px sowie
Tablet- und Mobilbreite, keine 404 für Bilder oder Routen.

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

117 lines
4.6 KiB
TypeScript

import { Box, Checkbox, Chip, Typography } from '@mui/material'
import { Building2, ExternalLink } from 'lucide-react'
import type { Property } from '../../domain/property'
import type { ExtractedContact } from '../../domain/futureSignal'
import { ObjectDeepLink } from '../team'
import { CONTACT_ICONS } from './marketLeadHelpers'
import { DS_BG, DS_BRAND, DS_SLATE, DS_TEXT } from '../../lib/ds'
/**
* Bausteine der Signal-Detailansicht auf der Nora-Seite.
*
* Ausgezogen aus `MarketIntelligence.tsx` — die Seite hält jetzt nur noch
* Reiter, Auswahl und Rahmen.
*/
// ── Left pane list item ──────────────────────────────────────────────────────
// ── Contact row ──────────────────────────────────────────────────────────────
/**
* Extrahierte Kontaktdaten (Runde 4, §7.3).
*
* Ohne Konfidenzstufe und ohne Farbpunkt: gezeigt wird nur noch, was klar
* belegbar ist — siehe `confirmableContacts`. Ein Wert mit dem Hinweis
* «spekulativ» hilft niemandem; entweder man kann ihn anschreiben oder nicht.
*/
function ContactRow({ contact }: { contact: ExtractedContact }) {
const icon = CONTACT_ICONS[contact.type]
const isClickable = contact.type === 'WEBSITE' || contact.type === 'LINKEDIN' || contact.type === 'EMAIL'
const href = contact.type === 'EMAIL' ? `mailto:${contact.value}` : contact.type === 'WEBSITE' || contact.type === 'LINKEDIN' ? `https://${contact.value.replace(/^https?:\/\//, '')}` : undefined
return (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.25, py: 0.75, borderBottom: '1px solid #f8fafc' }}>
<Box sx={{ color: DS_SLATE[500], mt: 0.125, flexShrink: 0 }}>{icon}</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
{isClickable && href ? (
<Box
component="a"
href={href}
target="_blank"
rel="noopener noreferrer"
sx={{ color: DS_BRAND.main, fontWeight: 500, fontSize: '0.8125rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' }, display: 'flex', alignItems: 'center', gap: 0.5 }}
>
{contact.value}
<ExternalLink size={11} />
</Box>
) : (
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '0.8125rem', color: DS_SLATE[900] }}>
{contact.value}
</Typography>
)}
{contact.label && (
<Typography variant="caption" sx={{ color: DS_SLATE[400], fontSize: '0.7rem' }}>
{contact.label}
</Typography>
)}
</Box>
</Box>
)
}
// ── Objektzeile mit Mehrfachauswahl ──────────────────────────────────────────
/**
* Ein passendes Portfolioobjekt zu einem Signal.
*
* Der Anschreiben-Entwurf und die Reminder-Aktion sind entfallen (Runde 4, §7.3):
* Nora erkennt die Nachfrage, das Dokument macht Livia. Geblieben ist die
* Auswahl — mehrere Objekte lassen sich gemeinsam für dasselbe Signal
* weiterreichen, statt für jedes einzeln denselben Weg zu gehen.
*/
function SignalPropertyRow({
p, fit, selected, onToggle,
}: {
p: Property
fit: number | null
selected: boolean
onToggle: () => void
}) {
return (
<Box
sx={{
display: 'flex', alignItems: 'center', gap: 1,
px: 1.5, py: 1.25, mb: 1,
border: `1px solid ${selected ? '#152642' : '#e2e8f0'}`,
borderRadius: 1.5,
bgcolor: selected ? '#f8fafc' : 'white',
}}
>
<Checkbox
size="small"
checked={selected}
onChange={onToggle}
slotProps={{ input: { 'aria-label': `${p.title} auswählen` } }}
sx={{ p: 0.5 }}
/>
<Building2 size={14} color="#152642" style={{ flexShrink: 0 }} />
<Box sx={{ flex: 1, minWidth: 0 }}>
<ObjectDeepLink propertyId={p.id} label={p.title} fontWeight={600} />
<Typography variant="caption" sx={{ color: DS_SLATE[500] }}>
{p.location.city} · {p.areaSqm.toLocaleString('de-CH')} m² · CHF {p.rentPricePerSqm}/m²/J
</Typography>
</Box>
{fit !== null && (
<Chip
label={`${fit}% Flächenmatch`}
size="small"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 600, flexShrink: 0, bgcolor: DS_BG.subtle, color: DS_TEXT.secondary }}
/>
)}
</Box>
)
}
export { ContactRow, SignalPropertyRow }