Setzt das Umsetzungsbriefing Runde 4 im bestehenden Frontend um. Informationsarchitektur - «Teamübersicht» heisst «Meine Agenten»; ihr bisheriger Inhalt (Kreisgrafik, Auswertung, Verbindungszusammenfassung) ist entfallen. - Personalverwaltung, Bearbeitungsverlauf und Kanäle & Systeme sind jetzt Reiter derselben Seite (?section=), gestaltet wie die Auswahl im Bearbeitungsverlauf. Es wird nur der gewählte Bereich gerendert. - Die alten Unterseitenpfade leiten um, damit Lesezeichen nicht brechen. Agentenbestand - Reto und Lea vollständig entfernt — aus Navigation, Dossiers, Protokoll, Verbindungen, Vorgängen und Porträtbestand. - Retos Aufgaben liegen bei Bruno: Nachbereitung, WhatsApp-Anruf, Protokoll und Kundennotiz, Ablage im CRM. - Livia ist «Exposé Master»: Lageberichte, Inserate, Angebotsbroschüren. - Sidebar führt Ferdi, Bruno, Livia, Nora, Sina mit Porträt und Funktion. Agentenseiten - Ein gemeinsamer AgentWorkspaceHero auf allen fünf Seiten. - Ferdi: Auswertungskarten, Priorität und Typfarben entfallen; Fälligkeit nur bei fünf Tagen oder weniger rot; Objektlinks nach «Meine Objekte»; neu die Terminplanung im verbundenen Kalender mit typgerechtem PDF-Ausschnitt. - Sina: reduzierte, filter- und sortierbare Objektübersicht; Detailansicht direkt editierbar, leere Pflichtfelder rot umrandet. - Nora: Signale ohne Prozentsätze und Konfidenzstufen, nur belegbare Angaben; Mehrfachauswahl leitet Objekte an Livia weiter. - Livia: aktive und archivierte Leads, Arbeitsbereich gleitet an den oberen Rand; dreistufiger Exposé-Prozess Hochladen → Exposé → Export. - Bruno: neue Seite mit Auftragsliste und Vor-/Nachbereitungs-Drawer; Glocke warnt bei Besichtigung unter 24 Stunden ohne Bericht. Datenschicht - Neu: Kalender, Exposé-Leads, Exposé-Entwürfe, Besichtigungsaufträge — je Domain, Provider, Service und Hook. - IAIService um generateExposeText erweitert; der Entwurf nutzt ausschliesslich erfasste Objektdaten und meldet Lücken, statt sie zu füllen. - Alle Objektverweise zeigen auf reale Einträge aus «Meine Objekte»; neue Detailroute /supply/properties/:propertyId. Offen: Chat, Kalender, CRM und DMS sind Frontend-Simulation ohne Anbindung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
260 lines
9.0 KiB
TypeScript
260 lines
9.0 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
Box, Button, Dialog, DialogActions, DialogContent, DialogTitle,
|
|
FormControl, FormControlLabel, InputLabel, MenuItem, Select as MuiSelect,
|
|
Switch, TextField, ToggleButton, ToggleButtonGroup, Typography,
|
|
} from '@mui/material'
|
|
import { Building2, Globe, Lock, Search } from 'lucide-react'
|
|
import type {
|
|
MarktHinweis, HinweisVisibility, HinweisDirection, HinweisQuelle, CreateMarktHinweisInput,
|
|
} from '../../domain/marktHinweis'
|
|
import { useCreateMarktHinweis } from '../../hooks/useMarktHinweise'
|
|
import { QUELLE_LABELS } from './hinweisHelpers'
|
|
import { DS_BRAND, DS_SLATE } from '../../lib/ds'
|
|
|
|
/**
|
|
* Erfassung eines Netzwerk-Hinweises.
|
|
*
|
|
* Eigene Datei, weil der Dialog mit über 230 Zeilen für sich steht: er teilt
|
|
* mit den Panels nur die Beschriftungstabelle, nicht den Zustand.
|
|
*/
|
|
|
|
// ── Netzwerk: HinweisErfassenDialog ─────────────────────────────────────────
|
|
|
|
function HinweisErfassenDialog({
|
|
open,
|
|
onClose,
|
|
onCreated,
|
|
}: {
|
|
open: boolean
|
|
onClose: () => void
|
|
onCreated: (id: string) => void
|
|
}) {
|
|
const mutation = useCreateMarktHinweis()
|
|
|
|
const [direction, setDirection] = useState<HinweisDirection>('SUCHE')
|
|
const [assetType, setAssetType] = useState<string>('')
|
|
const [locationHint, setLocationHint] = useState<string>('')
|
|
const [areaSqmMin, setAreaSqmMin] = useState<string>('')
|
|
const [areaSqmMax, setAreaSqmMax] = useState<string>('')
|
|
const [companyName, setCompanyName] = useState<string>('')
|
|
const [isAnonymized, setIsAnonymized] = useState<boolean>(false)
|
|
const [quelle, setQuelle] = useState<HinweisQuelle>('NETZWERKEVENT')
|
|
const [note, setNote] = useState<string>('')
|
|
const [visibility, setVisibility] = useState<HinweisVisibility>('INTERN')
|
|
|
|
const isValid = assetType.trim() !== '' && locationHint.trim() !== ''
|
|
|
|
function resetForm() {
|
|
setDirection('SUCHE')
|
|
setAssetType('')
|
|
setLocationHint('')
|
|
setAreaSqmMin('')
|
|
setAreaSqmMax('')
|
|
setCompanyName('')
|
|
setIsAnonymized(false)
|
|
setQuelle('NETZWERKEVENT')
|
|
setNote('')
|
|
setVisibility('INTERN')
|
|
}
|
|
|
|
function handleSave() {
|
|
if (!isValid) return
|
|
const input: CreateMarktHinweisInput = {
|
|
direction,
|
|
assetType: assetType as MarktHinweis['assetType'],
|
|
locationHint,
|
|
areaSqmMin: areaSqmMin ? Number(areaSqmMin) : undefined,
|
|
areaSqmMax: areaSqmMax ? Number(areaSqmMax) : undefined,
|
|
companyName: companyName.trim() || undefined,
|
|
isAnonymized,
|
|
quelle,
|
|
note: note.trim() || undefined,
|
|
visibility,
|
|
status: 'OFFEN',
|
|
}
|
|
mutation.mutate(input, {
|
|
onSuccess: (result) => {
|
|
onCreated(result.id)
|
|
resetForm()
|
|
},
|
|
})
|
|
}
|
|
|
|
function handleClose() {
|
|
resetForm()
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
|
<DialogTitle>Hinweis erfassen</DialogTitle>
|
|
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
|
|
{/* Richtung */}
|
|
<Box>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_SLATE[600], display: 'block', mb: 0.75 }}>
|
|
Richtung
|
|
</Typography>
|
|
<ToggleButtonGroup
|
|
value={direction}
|
|
exclusive
|
|
onChange={(_, v) => { if (v) setDirection(v as HinweisDirection) }}
|
|
size="small"
|
|
sx={{ '& .MuiToggleButton-root': { textTransform: 'none', fontSize: '0.8rem' } }}
|
|
>
|
|
<ToggleButton value="SUCHE">
|
|
<Search size={14} style={{ marginRight: 6 }} /> Jemand sucht
|
|
</ToggleButton>
|
|
<ToggleButton value="VERFUEGBARKEIT">
|
|
<Building2 size={14} style={{ marginRight: 6 }} /> Wird verfügbar
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Box>
|
|
|
|
{/* Asset-Typ */}
|
|
<FormControl fullWidth required>
|
|
<InputLabel>Asset-Typ *</InputLabel>
|
|
<MuiSelect
|
|
value={assetType}
|
|
label="Asset-Typ *"
|
|
onChange={e => setAssetType(e.target.value)}
|
|
>
|
|
<MenuItem value="OFFICE">Büro</MenuItem>
|
|
<MenuItem value="RETAIL">Einzelhandel</MenuItem>
|
|
<MenuItem value="LIGHT_INDUSTRIAL">Gewerbe</MenuItem>
|
|
<MenuItem value="LOGISTICS">Logistik</MenuItem>
|
|
<MenuItem value="PRODUCTION">Produktion</MenuItem>
|
|
</MuiSelect>
|
|
</FormControl>
|
|
|
|
{/* Stadt / Region */}
|
|
<TextField
|
|
fullWidth
|
|
required
|
|
label="Stadt / Region"
|
|
value={locationHint}
|
|
onChange={e => setLocationHint(e.target.value)}
|
|
/>
|
|
|
|
{/* Fläche */}
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
<TextField
|
|
label="Von (m²)"
|
|
type="number"
|
|
value={areaSqmMin}
|
|
onChange={e => setAreaSqmMin(e.target.value)}
|
|
sx={{ flex: 1 }}
|
|
/>
|
|
<TextField
|
|
label="Bis (m²)"
|
|
type="number"
|
|
value={areaSqmMax}
|
|
onChange={e => setAreaSqmMax(e.target.value)}
|
|
sx={{ flex: 1 }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Firma / Name */}
|
|
<TextField
|
|
fullWidth
|
|
label="Firma / Name (optional)"
|
|
value={companyName}
|
|
onChange={e => setCompanyName(e.target.value)}
|
|
/>
|
|
|
|
{/* Quelle */}
|
|
<FormControl fullWidth>
|
|
<InputLabel>Quelle</InputLabel>
|
|
<MuiSelect
|
|
value={quelle}
|
|
label="Quelle"
|
|
onChange={e => setQuelle(e.target.value as HinweisQuelle)}
|
|
>
|
|
{Object.entries(QUELLE_LABELS).map(([k, v]) => (
|
|
<MenuItem key={k} value={k}>{v}</MenuItem>
|
|
))}
|
|
</MuiSelect>
|
|
</FormControl>
|
|
|
|
{/* Notiz */}
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
minRows={3}
|
|
label="Notiz (optional)"
|
|
value={note}
|
|
onChange={e => setNote(e.target.value)}
|
|
/>
|
|
|
|
{/* Sichtbarkeit */}
|
|
<Box>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_SLATE[600], display: 'block', mb: 0.75 }}>
|
|
Sichtbarkeit
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
<Box
|
|
onClick={() => setVisibility('INTERN')}
|
|
sx={{
|
|
flex: 1, borderRadius: 1.5, p: 1.5, cursor: 'pointer',
|
|
display: 'flex', flexDirection: 'column', gap: 0.5,
|
|
border: visibility === 'INTERN' ? '2px solid #4338ca' : '1px solid #e2e8f0',
|
|
bgcolor: visibility === 'INTERN' ? '#eef2ff' : 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<Lock size={14} color={visibility === 'INTERN' ? '#4338ca' : '#64748b'} />
|
|
<Typography variant="caption" sx={{ fontWeight: 700, color: visibility === 'INTERN' ? '#4338ca' : '#0f172a' }}>Intern</Typography>
|
|
</Box>
|
|
<Typography variant="caption" sx={{ color: DS_SLATE[500], fontSize: '0.7rem' }}>Nur für Ihr Team sichtbar</Typography>
|
|
</Box>
|
|
<Box
|
|
onClick={() => setVisibility('PLATTFORM')}
|
|
sx={{
|
|
flex: 1, borderRadius: 1.5, p: 1.5, cursor: 'pointer',
|
|
display: 'flex', flexDirection: 'column', gap: 0.5,
|
|
border: visibility === 'PLATTFORM' ? '2px solid #7c3aed' : '1px solid #e2e8f0',
|
|
bgcolor: visibility === 'PLATTFORM' ? '#faf5ff' : 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<Globe size={14} color={visibility === 'PLATTFORM' ? '#7c3aed' : '#64748b'} />
|
|
<Typography variant="caption" sx={{ fontWeight: 700, color: visibility === 'PLATTFORM' ? '#7c3aed' : '#0f172a' }}>Plattform</Typography>
|
|
</Box>
|
|
<Typography variant="caption" sx={{ color: DS_SLATE[500], fontSize: '0.7rem' }}>Für alle Verwaltungen auf Property Match sichtbar</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Anonymisieren (only for PLATTFORM) */}
|
|
{visibility === 'PLATTFORM' && (
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={isAnonymized}
|
|
onChange={e => setIsAnonymized(e.target.checked)}
|
|
size="small"
|
|
/>
|
|
}
|
|
label={<Typography variant="body2">Firmenname anonymisieren</Typography>}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
|
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
<Button onClick={handleClose} sx={{ textTransform: 'none' }}>Abbrechen</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleSave}
|
|
disabled={mutation.isPending || !isValid}
|
|
sx={{ textTransform: 'none', bgcolor: DS_BRAND.main, '&:hover': { bgcolor: DS_BRAND.hoverAlt } }}
|
|
>
|
|
Speichern
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
|
|
export { HinweisErfassenDialog }
|