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>
117 lines
4.6 KiB
TypeScript
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 }
|