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>
This commit is contained in:
1 parent
d200d4e930
commit
9ed21e33a7
59 files changed
+2545
-1309
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { Alert, Box, Button, Chip, Divider, TextField, Typography } from '@mui/material'
|
||||
import { Building2, CheckCircle2, Copy, MapPin, Ruler, Search } from 'lucide-react'
|
||||
import { Building2, CheckCircle2, Copy, MapPin, Ruler } from 'lucide-react'
|
||||
import type { MarktHinweis } from '../../domain/marktHinweis'
|
||||
import type { Property } from '../../domain/property'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
@@ -127,76 +127,6 @@ const HinweisPropertyCard = memo(function HinweisPropertyCard({
|
||||
)
|
||||
})
|
||||
|
||||
// ── Netzwerk: HinweisListItem ────────────────────────────────────────────────
|
||||
|
||||
const HinweisListItem = memo(function HinweisListItem({
|
||||
hinweis, selected, onClick,
|
||||
}: {
|
||||
hinweis: MarktHinweis
|
||||
selected: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
const displayName = !hinweis.isAnonymized && hinweis.companyName
|
||||
? hinweis.companyName
|
||||
: `Anonym · ${ASSET_TYPE_LABELS[hinweis.assetType] ?? hinweis.assetType}`
|
||||
|
||||
const areaStr = useMemo(() => {
|
||||
if (hinweis.areaSqmMin != null && hinweis.areaSqmMax != null && hinweis.areaSqmMin !== hinweis.areaSqmMax) {
|
||||
return ` · ${hinweis.areaSqmMin}–${hinweis.areaSqmMax} m²`
|
||||
}
|
||||
if (hinweis.areaSqmMax != null) return ` · ${hinweis.areaSqmMax} m²`
|
||||
if (hinweis.areaSqmMin != null) return ` · ${hinweis.areaSqmMin} m²`
|
||||
return ''
|
||||
}, [hinweis.areaSqmMin, hinweis.areaSqmMax])
|
||||
|
||||
const visibilityBadge = useMemo(() => {
|
||||
if (hinweis.visibility === 'INTERN') {
|
||||
return <Chip label="Intern" size="small" sx={{ bgcolor: DS_ACCENT.indigo.bg, color: DS_ACCENT.indigo.strong, height: 18, fontSize: '0.6rem' }} />
|
||||
}
|
||||
if (hinweis.verwaltungId === OWN_VERWALTUNG_ID) {
|
||||
return <Chip label="Geteilt" size="small" sx={{ bgcolor: DS_ACCENT.violet.bg, color: DS_ACCENT.violet.main, height: 18, fontSize: '0.6rem' }} />
|
||||
}
|
||||
return <Chip label={hinweis.verwaltungName} size="small" sx={{ bgcolor: DS_SLATE[100], color: DS_SLATE[600], height: 18, fontSize: '0.6rem' }} />
|
||||
}, [hinweis.visibility, hinweis.verwaltungId, hinweis.verwaltungName])
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
px: 2, py: 1.5,
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
borderLeft: `3px solid ${selected ? '#152642' : 'transparent'}`,
|
||||
bgcolor: selected ? '#f0f9ff' : 'white',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: selected ? '#f0f9ff' : '#f8fafc' },
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75, mb: 0.25 }}>
|
||||
<Box sx={{ mt: 0.25, flexShrink: 0 }}>
|
||||
{hinweis.direction === 'SUCHE'
|
||||
? <Search size={14} color="#152642" />
|
||||
: <Building2 size={14} color="#7c3aed" />
|
||||
}
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: DS_SLATE[900], lineHeight: 1.3, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{displayName}
|
||||
</Typography>
|
||||
<Box sx={{ flexShrink: 0 }}>{visibilityBadge}</Box>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: DS_SLATE[400], fontSize: '0.65rem', display: 'block', mb: 0.5, pl: 2.75 }}>
|
||||
{hinweis.locationHint}{areaStr}
|
||||
</Typography>
|
||||
<Box sx={{ pl: 2.75 }}>
|
||||
{hinweis.direction === 'SUCHE'
|
||||
? <Chip label="Suche" size="small" sx={{ bgcolor: DS_ACCENT.blue.borderSoft, color: DS_ACCENT.blue.main, height: 16, fontSize: '0.6rem' }} />
|
||||
: <Chip label="Wird frei" size="small" sx={{ bgcolor: DS_ACCENT.success.bgAlt, color: DS_ACCENT.success.bright, height: 16, fontSize: '0.6rem' }} />
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
// ── Netzwerk: HinweisDetail ──────────────────────────────────────────────────
|
||||
|
||||
function HinweisDetail({ hinweis }: { hinweis: MarktHinweis }) {
|
||||
@@ -298,4 +228,4 @@ function HinweisDetail({ hinweis }: { hinweis: MarktHinweis }) {
|
||||
}
|
||||
|
||||
|
||||
export { HinweisPropertyCard, HinweisListItem, HinweisDetail }
|
||||
export { HinweisPropertyCard, HinweisDetail }
|
||||
Reference in new issue
Block a user