diff --git a/src/domain/marktHinweis.ts b/src/domain/marktHinweis.ts new file mode 100644 index 0000000..9afa632 --- /dev/null +++ b/src/domain/marktHinweis.ts @@ -0,0 +1,27 @@ +import type { AssetType } from './enums' + +export type HinweisVisibility = 'INTERN' | 'PLATTFORM' +export type HinweisDirection = 'SUCHE' | 'VERFUEGBARKEIT' +export type HinweisStatus = 'OFFEN' | 'IN_BEARBEITUNG' | 'ARCHIVIERT' +export type HinweisQuelle = 'NETZWERKEVENT' | 'TELEFONAT' | 'BESICHTIGUNG' | 'MESSE' | 'EMAIL' | 'SONSTIGES' + +export interface MarktHinweis { + id: string + createdAt: string + createdBy: string + verwaltungId: string + verwaltungName: string + direction: HinweisDirection + assetType: AssetType + locationHint: string + areaSqmMin?: number + areaSqmMax?: number + companyName?: string + isAnonymized: boolean + quelle: HinweisQuelle + note?: string + visibility: HinweisVisibility + status: HinweisStatus +} + +export type CreateMarktHinweisInput = Omit diff --git a/src/hooks/useMarktHinweise.ts b/src/hooks/useMarktHinweise.ts new file mode 100644 index 0000000..5c3c9de --- /dev/null +++ b/src/hooks/useMarktHinweise.ts @@ -0,0 +1,21 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { marktHinweisService } from '../services/marktHinweisService' +import type { CreateMarktHinweisInput } from '../domain/marktHinweis' + +export function useMarktHinweise() { + return useQuery({ + queryKey: ['marktHinweise'], + queryFn: () => marktHinweisService.getAll(), + staleTime: 60_000, + }) +} + +export function useCreateMarktHinweis() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (data: CreateMarktHinweisInput) => marktHinweisService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['marktHinweise'] }) + }, + }) +} diff --git a/src/mock-data/marktHinweise.ts b/src/mock-data/marktHinweise.ts new file mode 100644 index 0000000..db05de2 --- /dev/null +++ b/src/mock-data/marktHinweise.ts @@ -0,0 +1,130 @@ +import { AssetType } from '../domain/enums' +import type { MarktHinweis } from '../domain/marktHinweis' + +export const marktHinweise: MarktHinweis[] = [ + { + id: 'mh-001', + createdAt: '2026-05-22T09:15:00Z', + createdBy: 'Thomas Müller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + direction: 'SUCHE', + assetType: AssetType.OFFICE, + locationHint: 'Zürich Innenstadt', + areaSqmMin: 200, + areaSqmMax: 400, + companyName: 'Fintech Solutions AG', + isAnonymized: false, + quelle: 'NETZWERKEVENT', + note: 'Sucht modernes Büro, Nähe HB, max CHF 350/m²/J. Kontakt: CEO Andreas Baum', + visibility: 'INTERN', + status: 'OFFEN', + }, + { + id: 'mh-002', + createdAt: '2026-05-21T14:30:00Z', + createdBy: 'Sarah Keller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + direction: 'SUCHE', + assetType: AssetType.LOGISTICS, + locationHint: 'Pratteln / Muttenz', + areaSqmMin: 800, + areaSqmMax: 1500, + companyName: 'Elektro Bernhard GmbH', + isAnonymized: false, + quelle: 'TELEFONAT', + note: 'Benötigt Lagerfläche mit Rampe und Kran. Einzug Q1/2027.', + visibility: 'INTERN', + status: 'OFFEN', + }, + { + id: 'mh-003', + createdAt: '2026-05-20T11:00:00Z', + createdBy: 'Thomas Müller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + direction: 'VERFUEGBARKEIT', + assetType: AssetType.OFFICE, + locationHint: 'Basel', + areaSqmMin: 350, + areaSqmMax: 350, + companyName: 'Fläche frei per Sept. 2026', + isAnonymized: false, + quelle: 'BESICHTIGUNG', + note: 'Bürofläche 350m², guter Zustand, 4. OG, Aufzug.', + visibility: 'INTERN', + status: 'OFFEN', + }, + { + id: 'mh-004', + createdAt: '2026-05-19T08:45:00Z', + createdBy: 'Thomas Müller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + direction: 'SUCHE', + assetType: AssetType.RETAIL, + locationHint: 'Winterthur Altstadt', + areaSqmMin: 80, + areaSqmMax: 150, + companyName: 'Boutique-Kette', + isAnonymized: false, + quelle: 'MESSE', + note: 'Ladenfläche EG, Fussgängerzone bevorzugt.', + visibility: 'PLATTFORM', + status: 'OFFEN', + }, + { + id: 'mh-005', + createdAt: '2026-05-18T16:20:00Z', + createdBy: 'Petra Züst', + verwaltungId: 'v-002', + verwaltungName: 'Livit AG', + direction: 'SUCHE', + assetType: AssetType.LIGHT_INDUSTRIAL, + locationHint: 'Raum Bern', + areaSqmMin: 400, + areaSqmMax: 700, + isAnonymized: true, + quelle: 'TELEFONAT', + note: 'Produktionsfirma sucht Gewerbehalle, Deckenhöhe mind. 5m.', + visibility: 'PLATTFORM', + status: 'OFFEN', + }, + { + id: 'mh-006', + createdAt: '2026-05-17T10:30:00Z', + createdBy: 'Marco Lüthi', + verwaltungId: 'v-003', + verwaltungName: 'Zurich Invest AG', + direction: 'VERFUEGBARKEIT', + assetType: AssetType.OFFICE, + locationHint: 'Zürich West', + areaSqmMin: 550, + areaSqmMax: 550, + companyName: 'Fläche frei per April 2027', + isAnonymized: false, + quelle: 'SONSTIGES', + note: 'Repräsentatives Büro, Ausbaustandard A, direkte SBB-Anbindung.', + visibility: 'PLATTFORM', + status: 'OFFEN', + }, + { + id: 'mh-007', + createdAt: '2026-05-15T09:00:00Z', + createdBy: 'Sarah Keller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + direction: 'SUCHE', + assetType: AssetType.OFFICE, + locationHint: 'Luzern', + areaSqmMin: 180, + areaSqmMax: 320, + companyName: 'Startup Hub Luzern', + isAnonymized: false, + quelle: 'NETZWERKEVENT', + note: 'Flexibler Ausbau gewünscht, Co-Working-tauglich.', + visibility: 'INTERN', + status: 'IN_BEARBEITUNG', + }, +] diff --git a/src/pages/supply/MarketIntelligence.tsx b/src/pages/supply/MarketIntelligence.tsx index 257ab56..2971577 100644 --- a/src/pages/supply/MarketIntelligence.tsx +++ b/src/pages/supply/MarketIntelligence.tsx @@ -1,21 +1,29 @@ -import { memo, useState, useEffect, useCallback } from 'react' +import { memo, useState, useEffect, useCallback, useMemo } from 'react' import { useNavigate } from 'react-router' import { - Alert, Box, Button, Chip, Divider, IconButton, - Skeleton, TextField, Tooltip, Typography, + Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, + Divider, FormControl, FormControlLabel, IconButton, + InputLabel, MenuItem, Select as MuiSelect, + Skeleton, Switch, Tab, Tabs, TextField, ToggleButton, ToggleButtonGroup, + Tooltip, Typography, } from '@mui/material' import { AlertCircle, Bell, Building2, CheckCircle2, Copy, - ExternalLink, Globe, Linkedin, Mail, MapPin, Phone, - Ruler, Sparkles, TrendingUp, User, + ExternalLink, Globe, Linkedin, Lock, Mail, MapPin, Phone, + Plus, Ruler, Search, Sparkles, TrendingUp, User, Users, } from 'lucide-react' import { useMarketLeads } from '../../hooks/useMarketLeads' import type { MarketLead } from '../../hooks/useMarketLeads' +import { useMarktHinweise, useCreateMarktHinweis } from '../../hooks/useMarktHinweise' +import type { MarktHinweis, HinweisVisibility, HinweisDirection, HinweisQuelle, CreateMarktHinweisInput } from '../../domain/marktHinweis' +import { useProperties } from '../../hooks/useProperties' import type { Property } from '../../domain/property' import type { ExtractedContact } from '../../domain/futureSignal' // ── Constants ──────────────────────────────────────────────────────────────── +const OWN_VERWALTUNG_ID = 'v-001' + const SOURCE_LABELS: Record = { JOB_POSTING: 'Stelleninserate', PRESS: 'Pressebericht', @@ -26,6 +34,24 @@ const SOURCE_LABELS: Record = { LEASE_CONTRACT: 'Mietvertrag', } +const QUELLE_LABELS: Record = { + NETZWERKEVENT: 'Netzwerkevent', + TELEFONAT: 'Telefonat', + BESICHTIGUNG: 'Besichtigung', + MESSE: 'Messe', + EMAIL: 'E-Mail', + SONSTIGES: 'Sonstiges', +} + +const ASSET_TYPE_LABELS: Record = { + OFFICE: 'Büro', + RETAIL: 'Einzelhandel', + LIGHT_INDUSTRIAL: 'Gewerbe', + LOGISTICS: 'Logistik', + PRODUCTION: 'Produktion', + MIXED: 'Gemischt', +} + const CONTACT_ICONS: Record = { EMAIL: , PHONE: , @@ -56,6 +82,10 @@ function areaFitPct(propArea: number, signalArea: number): number { return Math.max(0, Math.round(100 - (Math.abs(propArea - signalArea) / signalArea) * 100)) } +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('de-CH') +} + function buildAnschreiben(company: string, location: string, p: Property, areaSqmEstimate?: number): string { const areaLine = areaSqmEstimate ? `Fläche: ${p.areaSqm.toLocaleString('de-CH')} m² (Sie suchen ca. ${areaSqmEstimate.toLocaleString('de-CH')} m²)` @@ -466,65 +496,709 @@ function LeadDetail({ lead }: { lead: MarketLead }) { ) } -// ── Page ───────────────────────────────────────────────────────────────────── +// ── Netzwerk: HinweisPropertyCard ──────────────────────────────────────────── -export default function MarketIntelligence() { - const [selectedId, setSelectedId] = useState(null) - const { data: leads, isLoading } = useMarketLeads() +const HinweisPropertyCard = memo(function HinweisPropertyCard({ + p, + hinweis, +}: { + p: Property + hinweis: MarktHinweis +}) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + const companyName = hinweis.isAnonymized ? 'Interessent' : (hinweis.companyName ?? 'Interessent') + const areaEstimate = hinweis.areaSqmMax ?? hinweis.areaSqmMin + const [draft, setDraft] = useState(() => + buildAnschreiben(companyName, hinweis.locationHint, p, areaEstimate) + ) + const fit = areaEstimate != null ? areaFitPct(p.areaSqm, areaEstimate) : null - useEffect(() => { - if (!selectedId && leads.length > 0) setSelectedId(leads[0].signal.id) - }, [leads, selectedId]) - - const selectedLead = leads.find(l => l.signal.id === selectedId) ?? null + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(draft) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [draft]) return ( - - - Marktchancen - - KI-erkannte Unternehmen mit Flächenbedarf · automatisch aus Web-Quellen · mit Portfolio-Match und Anschreiben - + + {/* Property header */} + + + + + {p.title} + + + {p.location.city} · {p.areaSqm.toLocaleString('de-CH')} m² · CHF {p.rentPricePerSqm}/m²/J + + + {fit !== null && ( + = 75 ? '#dcfce7' : fit >= 50 ? '#fef9c3' : '#fee2e2', + color: fit >= 75 ? '#16a34a' : fit >= 50 ? '#92400e' : '#dc2626', + }} + /> + )} - - {/* Left pane */} - - - Erkannte Signale - - - - {isLoading - ? [1, 2, 3, 4].map(i => ( - - - - - - )) - : leads.map(lead => ( - setSelectedId(lead.signal.id)} - /> - ))} + {/* Anschreiben toggle */} + + + + + {/* Anschreiben composer */} + {open && ( + + + + Anschreiben-Entwurf + + + setDraft(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { fontSize: '0.8rem', bgcolor: 'white' }, + '& textarea': { lineHeight: 1.6 }, + }} + /> + + Text bearbeitbar — dann kopieren und per E-Mail versenden + + + )} + + ) +}) + +// ── 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 + } + if (hinweis.verwaltungId === OWN_VERWALTUNG_ID) { + return + } + return + }, [hinweis.visibility, hinweis.verwaltungId, hinweis.verwaltungName]) + + return ( + + + + {hinweis.direction === 'SUCHE' + ? + : + } + + + {displayName} + + {visibilityBadge} + + + {hinweis.locationHint}{areaStr} + + + {hinweis.direction === 'SUCHE' + ? + : + } + + + ) +}) + +// ── Netzwerk: HinweisDetail ────────────────────────────────────────────────── + +function HinweisDetail({ hinweis }: { hinweis: MarktHinweis }) { + const { data: properties = [] } = useProperties() + + const matchingProperties = useMemo( + () => properties.filter(p => p.assetType === hinweis.assetType).slice(0, 3), + [properties, hinweis.assetType], + ) + + const displayName = !hinweis.isAnonymized && hinweis.companyName ? hinweis.companyName : 'Anonym' + + 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 null + }, [hinweis.areaSqmMin, hinweis.areaSqmMax]) + + return ( + + {/* Header */} + + + {displayName} + + + {/* Badge row */} + + {hinweis.direction === 'SUCHE' + ? + : + } + {hinweis.visibility === 'INTERN' + ? + : hinweis.verwaltungId === OWN_VERWALTUNG_ID + ? + : + } - {/* Right pane */} - - {selectedLead ? ( - - ) : ( - - Signal aus der Liste auswählen - - )} + {/* Stat chips */} + + } label={hinweis.locationHint} size="small" sx={{ bgcolor: '#f1f5f9', color: '#475569', fontSize: '0.75rem' }} /> + {areaStr && } label={areaStr} size="small" sx={{ bgcolor: '#f1f5f9', color: '#475569', fontSize: '0.75rem' }} />} + + + {/* Quelle row */} + + + Quelle: {QUELLE_LABELS[hinweis.quelle] ?? hinweis.quelle} + {' · '}{hinweis.createdBy} + {' · '}{formatDate(hinweis.createdAt)} + + + + + {/* Body */} + + {hinweis.note && ( + + + Notiz + + + {hinweis.note} + + + )} + + + + {hinweis.direction === 'SUCHE' ? ( + + + Passende Objekte im Portfolio ({matchingProperties.length}) + + {matchingProperties.length === 0 ? ( + + Kein passendes Portfolioobjekt gefunden + + ) : ( + matchingProperties.map(p => ( + + )) + )} + + ) : ( + + Verfügbarkeitssignal — wird auf der Plattform für andere Verwaltungen sichtbar gemacht + + )} ) } + +// ── Netzwerk: HinweisErfassenDialog ───────────────────────────────────────── + +function HinweisErfassenDialog({ + open, + onClose, + onCreated, +}: { + open: boolean + onClose: () => void + onCreated: (id: string) => void +}) { + const mutation = useCreateMarktHinweis() + + const [direction, setDirection] = useState('SUCHE') + const [assetType, setAssetType] = useState('') + const [locationHint, setLocationHint] = useState('') + const [areaSqmMin, setAreaSqmMin] = useState('') + const [areaSqmMax, setAreaSqmMax] = useState('') + const [companyName, setCompanyName] = useState('') + const [isAnonymized, setIsAnonymized] = useState(false) + const [quelle, setQuelle] = useState('NETZWERKEVENT') + const [note, setNote] = useState('') + const [visibility, setVisibility] = useState('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 ( + + Hinweis erfassen + + {/* Richtung */} + + + Richtung + + { if (v) setDirection(v as HinweisDirection) }} + size="small" + sx={{ '& .MuiToggleButton-root': { textTransform: 'none', fontSize: '0.8rem' } }} + > + + Jemand sucht + + + Wird verfügbar + + + + + {/* Asset-Typ */} + + Asset-Typ * + setAssetType(e.target.value)} + > + Büro + Einzelhandel + Gewerbe + Logistik + Produktion + + + + {/* Stadt / Region */} + setLocationHint(e.target.value)} + /> + + {/* Fläche */} + + setAreaSqmMin(e.target.value)} + sx={{ flex: 1 }} + /> + setAreaSqmMax(e.target.value)} + sx={{ flex: 1 }} + /> + + + {/* Firma / Name */} + setCompanyName(e.target.value)} + /> + + {/* Quelle */} + + Quelle + setQuelle(e.target.value as HinweisQuelle)} + > + {Object.entries(QUELLE_LABELS).map(([k, v]) => ( + {v} + ))} + + + + {/* Notiz */} + setNote(e.target.value)} + /> + + {/* Sichtbarkeit */} + + + Sichtbarkeit + + + 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', + }} + > + + + Intern + + Nur für Ihr Team sichtbar + + 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', + }} + > + + + Plattform + + Für alle Verwaltungen auf Property Match sichtbar + + + + + {/* Anonymisieren (only for PLATTFORM) */} + {visibility === 'PLATTFORM' && ( + setIsAnonymized(e.target.checked)} + size="small" + /> + } + label={Firmenname anonymisieren} + /> + )} + + + + + + + + ) +} + +// ── Page ───────────────────────────────────────────────────────────────────── + +export default function MarketIntelligence() { + const [activeTab, setActiveTab] = useState(0) + const [selectedSignalId, setSelectedSignalId] = useState(null) + const [selectedHinweisId, setSelectedHinweisId] = useState(null) + const [erfassenOpen, setErfassenOpen] = useState(false) + const [hinweisFilter, setHinweisFilter] = useState<'ALL' | 'INTERN' | 'PLATTFORM'>('ALL') + + const { data: leads, isLoading: leadsLoading } = useMarketLeads() + const { data: hinweise = [], isLoading: hinweiseLoading } = useMarktHinweise() + + // auto-select first signal + useEffect(() => { + if (!selectedSignalId && leads.length > 0) setSelectedSignalId(leads[0].signal.id) + }, [leads, selectedSignalId]) + + // auto-select first hinweis + useEffect(() => { + if (!selectedHinweisId && hinweise.length > 0) setSelectedHinweisId(hinweise[0].id) + }, [hinweise, selectedHinweisId]) + + const selectedLead = leads.find(l => l.signal.id === selectedSignalId) ?? null + + const filteredHinweise = useMemo(() => { + if (hinweisFilter === 'ALL') return hinweise + return hinweise.filter(h => h.visibility === hinweisFilter) + }, [hinweise, hinweisFilter]) + + const selectedHinweis = hinweise.find(h => h.id === selectedHinweisId) ?? null + + return ( + + {/* Page header */} + + Marktchancen + + KI-Signale aus Web-Quellen und menschliche Netzwerk-Hinweise — mit Portfolio-Match + + + + {/* Tabs */} + setActiveTab(v)} + sx={{ + borderBottom: '1px solid #e2e8f0', + flexShrink: 0, + bgcolor: 'white', + '& .MuiTab-root': { textTransform: 'none', fontSize: '0.85rem', minHeight: 44, px: 3 }, + }} + > + } iconPosition="start" label="Digitale Signale" /> + } iconPosition="start" label="Netzwerk" /> + + + {/* Tab 0: Digitale Signale */} + {activeTab === 0 && ( + + {/* Left pane */} + + + Erkannte Signale + + + + {leadsLoading + ? [1, 2, 3, 4].map(i => ( + + + + + + )) + : leads.map(lead => ( + setSelectedSignalId(lead.signal.id)} + /> + ))} + + + + {/* Right pane */} + + {selectedLead ? ( + + ) : ( + + Signal aus der Liste auswählen + + )} + + + )} + + {/* Tab 1: Netzwerk */} + {activeTab === 1 && ( + + {/* Left pane */} + + + + Netzwerk-Hinweise + + + + {/* Filter chips */} + + {(['ALL', 'INTERN', 'PLATTFORM'] as const).map(f => ( + setHinweisFilter(f)} + sx={{ + height: 22, fontSize: '0.68rem', cursor: 'pointer', + bgcolor: hinweisFilter === f ? '#1e3a5f' : '#f1f5f9', + color: hinweisFilter === f ? 'white' : '#475569', + }} + /> + ))} + + + + {/* List */} + + {hinweiseLoading + ? [1, 2, 3].map(i => ( + + + + + )) + : filteredHinweise.map(h => ( + setSelectedHinweisId(h.id)} + /> + )) + } + + + + {/* Right pane */} + + {selectedHinweis ? ( + + ) : ( + + Hinweis aus der Liste auswählen + + )} + + + )} + + {/* Dialog */} + setErfassenOpen(false)} + onCreated={(id) => { + setSelectedHinweisId(id) + setErfassenOpen(false) + setActiveTab(1) + }} + /> + + ) +} diff --git a/src/provider/IMarktHinweisProvider.ts b/src/provider/IMarktHinweisProvider.ts new file mode 100644 index 0000000..12b1d5a --- /dev/null +++ b/src/provider/IMarktHinweisProvider.ts @@ -0,0 +1,8 @@ +import type { MarktHinweis, CreateMarktHinweisInput } from '../domain/marktHinweis' + +export interface IMarktHinweisProvider { + getAll(): Promise + getById(id: string): Promise + create(data: CreateMarktHinweisInput): Promise + update(id: string, data: Partial): Promise +} diff --git a/src/provider/MockupMarktHinweisProvider.ts b/src/provider/MockupMarktHinweisProvider.ts new file mode 100644 index 0000000..9d7304a --- /dev/null +++ b/src/provider/MockupMarktHinweisProvider.ts @@ -0,0 +1,36 @@ +import type { IMarktHinweisProvider } from './IMarktHinweisProvider' +import type { MarktHinweis, CreateMarktHinweisInput } from '../domain/marktHinweis' +import { marktHinweise as seed } from '../mock-data/marktHinweise' + +const store: MarktHinweis[] = [...seed] + +export const MockupMarktHinweisProvider: IMarktHinweisProvider = { + async getAll() { + return [...store] + }, + + async getById(id: string) { + return store.find(h => h.id === id) ?? null + }, + + async create(data: CreateMarktHinweisInput) { + const next: MarktHinweis = { + ...data, + id: `mh-${Date.now()}`, + createdAt: new Date().toISOString(), + createdBy: 'Thomas Müller', + verwaltungId: 'v-001', + verwaltungName: 'Wincasa AG', + } + store.push(next) + return next + }, + + async update(id: string, data: Partial) { + const idx = store.findIndex(h => h.id === id) + if (idx === -1) throw new Error(`MarktHinweis ${id} not found`) + const updated: MarktHinweis = { ...store[idx], ...data } + store[idx] = updated + return updated + }, +} diff --git a/src/services/marktHinweisService.ts b/src/services/marktHinweisService.ts new file mode 100644 index 0000000..5d19ede --- /dev/null +++ b/src/services/marktHinweisService.ts @@ -0,0 +1,11 @@ +import { MockupMarktHinweisProvider } from '../provider/MockupMarktHinweisProvider' +import type { MarktHinweis, CreateMarktHinweisInput } from '../domain/marktHinweis' + +const provider = MockupMarktHinweisProvider + +export const marktHinweisService = { + getAll: (): Promise => provider.getAll(), + getById: (id: string): Promise => provider.getById(id), + create: (data: CreateMarktHinweisInput): Promise => provider.create(data), + update: (id: string, data: Partial): Promise => provider.update(id, data), +}