From a825672197607dc8d3bc0dec4b6d554af3c5a285 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Tue, 26 May 2026 15:36:32 +0200 Subject: [PATCH] feat: collapsible filter bar, compact property cards, offer wizard field selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Properties page: collapsible decision/filter strip (localStorage state), slim single-row Datenpflege status bar (chips + button), 4-column card grid - PropertyIntelligenceCard: reduced image height (160→120px), tighter body padding - OfferWizard (latente Anfragen): new "Felder wählen" step between property selection and PDF preview; uses ReportObjectFieldSelector per property - MockPdfPreview: adds second PDF page showing selected properties side by side with photo, title, location and all chosen hard/soft fact fields - offerWizardStore: adds select_fields step type and fieldSelections state Co-Authored-By: Claude Sonnet 4.6 --- .../anfragencenter/MockPdfPreview.tsx | 162 ++++++++++++- .../OfferFieldSelectionStep.tsx | 87 +++++++ .../anfragencenter/OfferPdfReviewStep.tsx | 4 +- .../OfferPropertySelectionStep.tsx | 2 +- src/components/anfragencenter/OfferWizard.tsx | 12 +- .../supply/PropertyIntelligenceCard.tsx | 213 +++++++++++------- src/pages/supply/Properties.tsx | 133 +++++++---- src/stores/offerWizardStore.ts | 14 +- 8 files changed, 486 insertions(+), 141 deletions(-) create mode 100644 src/components/anfragencenter/OfferFieldSelectionStep.tsx diff --git a/src/components/anfragencenter/MockPdfPreview.tsx b/src/components/anfragencenter/MockPdfPreview.tsx index c0270d4..1e0bb76 100644 --- a/src/components/anfragencenter/MockPdfPreview.tsx +++ b/src/components/anfragencenter/MockPdfPreview.tsx @@ -1,18 +1,86 @@ -import { Box, Typography } from '@mui/material' -import { FileText } from 'lucide-react' +import { Box, Divider, Typography } from '@mui/material' +import { Building2, FileText, MapPin } from 'lucide-react' import { useOfferWizardStore } from '../../stores/offerWizardStore' import { mockProperties } from '../../mock-data/properties' +import type { ReportObjectFieldKey, ReportObjectFieldSelection } from '../../domain/inquiryReport' +import type { Property } from '../../domain/property' interface MockPdfPreviewProps { fields: Record fallbackFields: Record + fieldSelections: ReportObjectFieldSelection[] } -export function MockPdfPreview({ fields, fallbackFields }: MockPdfPreviewProps) { +const FIELD_LABELS: Partial> = { + areaSqm: 'Fläche', + rentPricePerSqm: 'Mietpreis', + availabilityDate: 'Verfügbar ab', + leaseTerm: 'Mietlaufzeit', + breakoutOption: 'Breakout-Option', + currentTenant: 'Aktueller Mieter', + propertyNumber: 'Objektnummer', + assetType: 'Asset Type', + floor: 'Stockwerk', + parking: 'Parkplätze', + fitOut: 'Ausbaugrad', + isBarrierFree: 'Barrierefrei', + ceilingHeightM: 'Raumhöhe', + floorLoad: 'Bodenlast', + loadingDocksCount: 'Anlieferung', + goodsLift: 'Warenaufzug', + passengerLift: 'Personenaufzug', + powerSupplyKva: 'Stromanschluss', + hasServerRoom: 'Serverraum', + internet: 'Internet', + deliveryAccess: 'Zufahrt', + publicTransportScore: 'ÖV-Anbindung', + prestigeScore: 'Prestige', + visibilityScore: 'Sichtbarkeit', + footfallScore: 'Passantenfrequenz', + commuterAccessScore: 'Pendlererreichbarkeit', + talentAccessScore: 'Talent Access', + esgScore: 'ESG', + flexibilityScore: 'Flexibilität', + expansionPotentialScore: 'Expansionspotenzial', + taxEnvironmentScore: 'Steuerumfeld', + microLocation: 'Mikrostandort', + competitionEnvironment: 'Konkurrenzumfeld', + infrastructure: 'Infrastruktur', + description: 'Beschreibung', +} + +function getFieldValue(property: Property, key: ReportObjectFieldKey): string | null { + switch (key) { + case 'areaSqm': return property.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : null + case 'rentPricePerSqm': return property.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/J` : null + case 'availabilityDate': return property.availabilityDate ? new Date(property.availabilityDate).toLocaleDateString('de-CH') : null + case 'leaseTerm': return property.leaseTerm ?? null + case 'breakoutOption': return property.breakoutOption != null ? (property.breakoutOption ? 'Ja' : 'Nein') : null + case 'currentTenant': return property.currentTenant ?? null + case 'propertyNumber': return property.propertyNumber ?? null + case 'assetType': return property.assetType ?? null + case 'floor': return property.floorLevel != null ? String(property.floorLevel) : null + case 'parking': return property.softFactors?.parkingSpots != null ? `${property.softFactors.parkingSpots} Pl.` : null + case 'ceilingHeightM': return property.hardFacts?.ceilingHeightM != null ? `${property.hardFacts.ceilingHeightM} m` : null + case 'loadingDocksCount': return property.hardFacts?.loadingDocksCount != null ? String(property.hardFacts.loadingDocksCount) : null + case 'powerSupplyKva': return property.hardFacts?.powerSupplyKva != null ? `${property.hardFacts.powerSupplyKva} kVA` : null + case 'hasServerRoom': return property.hardFacts?.hasServerRoom != null ? (property.hardFacts.hasServerRoom ? 'Ja' : 'Nein') : null + case 'isBarrierFree': return property.hardFacts?.isBarrierFree != null ? (property.hardFacts.isBarrierFree ? 'Ja' : 'Nein') : null + case 'fitOut': return property.hardFacts?.fitOut ?? null + case 'publicTransportScore': return property.softFactors?.publicTransportMinutes != null ? `${property.softFactors.publicTransportMinutes} Min.` : null + case 'prestigeScore': return property.softFactors?.prestige != null ? `${property.softFactors.prestige}/100` : null + case 'visibilityScore': return property.softFactors?.visibilityScore != null ? `${property.softFactors.visibilityScore}/100` : null + case 'description': return property.description ?? null + default: return null + } +} + +export function MockPdfPreview({ fields, fallbackFields, fieldSelections }: MockPdfPreviewProps) { const needTitle = useOfferWizardStore(s => s.needTitle) const propertyIds = useOfferWizardStore(s => s.selectedPropertyIds) const properties = mockProperties.filter(p => propertyIds.includes(p.id)) + const hasPropertyPage = fieldSelections.length > 0 && properties.length > 0 const v = (id: string) => fields[id] ?? fallbackFields[id] ?? '' @@ -29,6 +97,7 @@ export function MockPdfPreview({ fields, fallbackFields }: MockPdfPreviewProps) fontFamily: '"Georgia", "Times New Roman", serif', }} > + {/* ── Page 1 — Offer letter ── */} @@ -90,6 +159,93 @@ export function MockPdfPreview({ fields, fallbackFields }: MockPdfPreviewProps) {v('closing')} + + {/* ── Page 2 — Property details side by side ── */} + {hasPropertyPage && ( + <> + + + + Seite 2 — Objektdetails + + + + + + {properties.map(p => { + const selection = fieldSelections.find(fs => fs.propertyId === p.id) + const optionalFields = selection?.selectedOptionalFields ?? [] + const image = p.images?.[0] + + return ( + + {/* Photo */} + {image ? ( + + ) : ( + + + + )} + + {/* Info */} + + + {p.title} + + + + + {p.location.city}{p.location.district ? ` · ${p.location.district}` : ''} + + + + {/* Selected fields */} + {optionalFields.length > 0 && ( + + {optionalFields.map(key => { + const val = getFieldValue(p, key) + if (!val) return null + return ( + + + {FIELD_LABELS[key] ?? key} + + + {val} + + + ) + })} + + )} + + + ) + })} + + + )} ) } diff --git a/src/components/anfragencenter/OfferFieldSelectionStep.tsx b/src/components/anfragencenter/OfferFieldSelectionStep.tsx new file mode 100644 index 0000000..1d07224 --- /dev/null +++ b/src/components/anfragencenter/OfferFieldSelectionStep.tsx @@ -0,0 +1,87 @@ +import { useEffect } from 'react' +import { Box, Button, Stack, Typography } from '@mui/material' +import { ArrowLeft } from 'lucide-react' +import { useOfferWizardStore } from '../../stores/offerWizardStore' +import { useProperties } from '../../hooks/useProperties' +import { ReportObjectFieldSelector } from './ReportObjectFieldSelector' +import type { ReportObjectFieldKey, ReportObjectFieldSelection } from '../../domain/inquiryReport' + +const DEFAULT_FIELDS: ReportObjectFieldKey[] = ['areaSqm', 'rentPricePerSqm', 'availabilityDate'] +const MANDATORY_FIELDS: ReportObjectFieldKey[] = ['title', 'location', 'mapImageUrl', 'images'] + +export function OfferFieldSelectionStep() { + const propertyIds = useOfferWizardStore(s => s.selectedPropertyIds) + const fieldSelections = useOfferWizardStore(s => s.fieldSelections) + const setFieldSelections = useOfferWizardStore(s => s.setFieldSelections) + const updateFieldSelection = useOfferWizardStore(s => s.updateFieldSelection) + const setStep = useOfferWizardStore(s => s.setStep) + + const { data: allProperties = [] } = useProperties() + const selectedProperties = allProperties.filter(p => propertyIds.includes(p.id)) + + // Initialise selections when properties are loaded or change + useEffect(() => { + if (selectedProperties.length === 0) return + const existing = new Set(fieldSelections.map(fs => fs.propertyId)) + const missing = selectedProperties.filter(p => !existing.has(p.id)) + if (missing.length > 0) { + setFieldSelections([ + ...fieldSelections, + ...missing.map(p => ({ + propertyId: p.id, + mandatoryFields: MANDATORY_FIELDS, + selectedOptionalFields: DEFAULT_FIELDS, + })), + ]) + } + // run only when property list or selections change length + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedProperties.length, fieldSelections.length]) + + return ( + + + + Wählen Sie die Datenfelder, die auf der Objektseite des Angebots angezeigt werden sollen. + Pflichtfelder (Titel, Standort, Foto) sind immer enthalten. + + + + {selectedProperties.map(p => { + const selection: ReportObjectFieldSelection = + fieldSelections.find(fs => fs.propertyId === p.id) ?? { + propertyId: p.id, + mandatoryFields: MANDATORY_FIELDS, + selectedOptionalFields: DEFAULT_FIELDS, + } + return ( + updateFieldSelection(p.id, v)} + /> + ) + })} + + + + + + + + + ) +} diff --git a/src/components/anfragencenter/OfferPdfReviewStep.tsx b/src/components/anfragencenter/OfferPdfReviewStep.tsx index efc833d..ed9bd9a 100644 --- a/src/components/anfragencenter/OfferPdfReviewStep.tsx +++ b/src/components/anfragencenter/OfferPdfReviewStep.tsx @@ -66,6 +66,8 @@ export function OfferPdfReviewStep() { } } + const fieldSelections = useOfferWizardStore(s => s.fieldSelections) + const needTitle = useOfferWizardStore(s => s.needTitle) const addAttachment = useOfferWizardStore(s => s.addAttachment) const setMessageDraft = useOfferWizardStore(s => s.setMessageDraft) @@ -128,7 +130,7 @@ export function OfferPdfReviewStep() { ) : ( - + )} diff --git a/src/components/anfragencenter/OfferPropertySelectionStep.tsx b/src/components/anfragencenter/OfferPropertySelectionStep.tsx index d59928f..41b30b8 100644 --- a/src/components/anfragencenter/OfferPropertySelectionStep.tsx +++ b/src/components/anfragencenter/OfferPropertySelectionStep.tsx @@ -70,7 +70,7 @@ export function OfferPropertySelectionStep() { return } setOfferDraftId(res.data.id) - setStep('pdf_review') + setStep('select_fields') } if (!need) { diff --git a/src/components/anfragencenter/OfferWizard.tsx b/src/components/anfragencenter/OfferWizard.tsx index 74c8869..d2284f4 100644 --- a/src/components/anfragencenter/OfferWizard.tsx +++ b/src/components/anfragencenter/OfferWizard.tsx @@ -12,22 +12,25 @@ import { import { X } from 'lucide-react' import { useOfferWizardStore, type OfferStep } from '../../stores/offerWizardStore' import { OfferPropertySelectionStep } from './OfferPropertySelectionStep' +import { OfferFieldSelectionStep } from './OfferFieldSelectionStep' import { OfferPdfReviewStep } from './OfferPdfReviewStep' import { OfferCheckedAction } from './OfferCheckedAction' import { OfferChatComposer } from './OfferChatComposer' -// 'checked' is internal — merged into pdf_review step; only 3 steps shown +// 'checked' is internal — merged into pdf_review step; 4 visible steps const STEPS: { key: OfferStep; label: string }[] = [ { key: 'select_properties', label: 'Objekte wählen' }, + { key: 'select_fields', label: 'Felder wählen' }, { key: 'pdf_review', label: 'Vorschau & Prüfen' }, { key: 'send', label: 'Senden' }, ] const STEP_DISPLAY_INDEX: Record = { select_properties: 0, - pdf_review: 1, - checked: 1, // same visual step as pdf_review - send: 2, + select_fields: 1, + pdf_review: 2, + checked: 2, // same visual step as pdf_review + send: 3, } export function OfferWizard() { @@ -114,6 +117,7 @@ export function OfferWizard() { {/* Step content */} {currentStep === 'select_properties' && } + {currentStep === 'select_fields' && } {currentStep === 'pdf_review' && } {currentStep === 'checked' && } {currentStep === 'send' && } diff --git a/src/components/supply/PropertyIntelligenceCard.tsx b/src/components/supply/PropertyIntelligenceCard.tsx index 999fe8e..dcb7a20 100644 --- a/src/components/supply/PropertyIntelligenceCard.tsx +++ b/src/components/supply/PropertyIntelligenceCard.tsx @@ -1,6 +1,6 @@ import { memo } from 'react' -import { Box, Button, Chip, LinearProgress, Typography } from '@mui/material' -import { LocationPreview } from '../shared/LocationPreview' +import { Box, Chip, LinearProgress, Typography } from '@mui/material' +import { MapPin, Maximize2, TrendingUp, Calendar } from 'lucide-react' import { getAssetTypeColor, getAssetTypeLabel, getAvailabilityLabel } from './propertyHelpers' import type { Property } from '../../domain/property' @@ -9,133 +9,176 @@ interface Props { onSelect: (id: string) => void } -function availabilityBadgeColor(status: string): string { - if (status === 'AVAILABLE_NOW') return '#1a7a4a' - if (status === 'AVAILABLE_SOON') return '#d97706' - return '#64748b' +function availabilityColor(status: string) { + if (status === 'AVAILABLE_NOW') return { bg: '#dcfce7', text: '#15803d' } + if (status === 'AVAILABLE_SOON') return { bg: '#fef9c3', text: '#a16207' } + return { bg: '#f1f5f9', text: '#64748b' } } export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ property: p, onSelect }: Props) { const confPct = Math.round(p.confidenceScore * 100) - const confColor = p.confidenceScore >= 0.75 ? '#1a7a4a' : p.confidenceScore >= 0.55 ? '#d97706' : '#c0392b' + const confColor = p.confidenceScore >= 0.75 ? '#15803d' : p.confidenceScore >= 0.55 ? '#d97706' : '#dc2626' + const annualRent = Math.round(p.areaSqm * p.rentPricePerSqm / 1000) + const avail = availabilityColor(p.availabilityStatus) + const assetColor = getAssetTypeColor(p.assetType) + const hasImage = !!p.images?.[0] return ( onSelect(p.id)} sx={{ borderRadius: 2, overflow: 'hidden', - border: '1px solid #e8d5c4', - background: 'linear-gradient(160deg,#fdf8f3,#faf0e6)', - boxShadow: '0 2px 12px rgba(0,0,0,0.06)', + border: '1px solid #e2e8f0', + bgcolor: 'white', + boxShadow: '0 1px 4px rgba(0,0,0,0.06)', display: 'flex', flexDirection: 'column', - transition: 'box-shadow 0.15s, transform 0.1s', cursor: 'pointer', + transition: 'box-shadow 0.15s, transform 0.12s', '&:hover': { - boxShadow: '0 6px 24px rgba(0,0,0,0.10)', - transform: 'translateY(-1px)', + boxShadow: '0 8px 24px rgba(0,0,0,0.12)', + transform: 'translateY(-2px)', + borderColor: '#cbd5e1', }, }} - onClick={() => onSelect(p.id)} > - {/* Image zone */} - - - - {/* Availability badge */} - - + {hasImage ? ( + - + ) : ( + + 🏢 + + )} - {/* Asset type chip */} - + {/* Overlay gradient */} + + + {/* Top badges */} + + + + + {/* Bottom: annual rent on image */} + + + Jahresmiete ca. + + CHF {annualRent}k + + + + {p.rentPricePerSqm} /m²/J + - {/* Card body */} - - - {p.title} - - - {p.location.city}{p.location.district ? ` · ${p.location.district}` : ''} - + {/* ── Card body ── */} + - {/* Key specs */} - - - - {p.contractDurationMonths && ( - + {/* Title + location */} + + + {p.title} + + + + + {p.location.city}{p.location.district ? ` · ${p.location.district}` : ''} + + + + + {/* Key metrics row */} + + + + + Fläche + + + {p.areaSqm.toLocaleString('de-CH')} m² + + + {p.availabilityDate && ( + + + + Ab + + + {new Date(p.availabilityDate).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })} + + + )} + {p.softFactors?.prestige != null && ( + + + + Prestige + + + {p.softFactors.prestige} + + )} - {/* Soft factors */} + {/* Soft factor chips */} {p.softFactors && ( - - {p.softFactors.prestige != null && ( - + + {p.softFactors.publicTransportMinutes != null && ( + )} - {p.softFactors.accessibility != null && ( - + {p.hardFacts?.fitOut && ( + + )} + {p.contractDurationMonths && ( + )} )} - {/* Confidence */} - - - Datenkonfidenz - {confPct}% + {/* Confidence bar */} + + + Datenkonfidenz + {confPct}% - - ) diff --git a/src/pages/supply/Properties.tsx b/src/pages/supply/Properties.tsx index 0eb7efe..7388430 100644 --- a/src/pages/supply/Properties.tsx +++ b/src/pages/supply/Properties.tsx @@ -1,8 +1,8 @@ import { useState, useMemo } from 'react' -import { Box, Drawer, useMediaQuery, useTheme } from '@mui/material' +import { Box, Button, Chip, Collapse, Drawer, Typography, useMediaQuery, useTheme } from '@mui/material' import { useNavigate } from 'react-router' +import { ChevronDown, ChevronUp } from 'lucide-react' import { PageHeader } from '../../components/layout' -import { DecisionContextPanel } from '../../components/ui' import { ViewToggle } from '../../components/shared' import { useProperties } from '../../hooks/useProperties' import { PropertyFilterBar, PropertyTable, PropertyDetailView, PropertyIntelligenceCard } from '../../components/supply' @@ -56,6 +56,9 @@ export default function Properties() { const [view, setView] = useState<'list' | 'grid'>(() => (localStorage.getItem('view-properties') as 'list' | 'grid') ?? 'list' ) + const [headerOpen, setHeaderOpen] = useState(() => + localStorage.getItem('props-header-open') !== 'false' + ) const { data: properties = [], isLoading, isError } = useProperties() @@ -103,54 +106,92 @@ export default function Properties() { } /> - {!isLoading && properties.length > 0 && ( - 0 ? 'positive' : 'warning' }, - ...(criticalGaps.length > 0 - ? [{ label: 'kritische Datenlücken', value: criticalGaps.length, severity: 'critical' as const }] - : [] - ), - ...(lowConfidence.length > 0 - ? [{ label: 'Konfidenz < 55%', value: lowConfidence.length, severity: 'warning' as const }] - : [] - ), - ...(staleOrOutdated.length > 0 - ? [{ label: 'veraltete Daten', value: staleOrOutdated.length, severity: 'warning' as const }] - : [] - ), - ]} - missing={allMissingFields.length > 0 - ? [`Fehlende Pflichtfelder bei ${criticalGaps.length} Objekten: ${allMissingFields.join(', ')}`] - : [] - } - risks={[ - ...(criticalGaps.length > 0 - ? [`${criticalGaps.length} Objekte werden potenziellen Mietern nicht angezeigt`] - : [] - ), - ...(staleOrOutdated.length > 0 - ? [`${staleOrOutdated.length} Objekte mit veralteten Preisen oder Verfügbarkeiten`] - : [] - ), - ]} - actions={[ - { - label: 'Datenpflege starten', - primary: criticalGaps.length > 0 || staleOrOutdated.length > 0, - onClick: () => navigate('/supply/data-quality'), - }, - ]} - /> - )} + {/* Collapse toggle strip */} + { + const next = !headerOpen + setHeaderOpen(next) + localStorage.setItem('props-header-open', String(next)) + }} + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + px: 3, + py: 0.5, + bgcolor: '#f8fafc', + borderBottom: '1px solid #e2e8f0', + cursor: 'pointer', + flexShrink: 0, + userSelect: 'none', + '&:hover': { bgcolor: '#f1f5f9' }, + }} + > + + Filter & Übersicht + + {headerOpen ? : } + - + + {!isLoading && properties.length > 0 && ( + + + 0 ? '#f0fdf4' : '#fef9c3', + color: matchReady.length > 0 ? '#1a7a4a' : '#92400e' }} + /> + {criticalGaps.length > 0 && ( + + )} + {staleOrOutdated.length > 0 && ( + + )} + + + + )} + + + {view === 'grid' ? ( - + {filtered.map(p => ( ))} diff --git a/src/stores/offerWizardStore.ts b/src/stores/offerWizardStore.ts index 71ffa8e..9d635c0 100644 --- a/src/stores/offerWizardStore.ts +++ b/src/stores/offerWizardStore.ts @@ -1,7 +1,8 @@ import { create } from 'zustand' import type { Attachment } from '../domain/inquiry' +import type { ReportObjectFieldSelection } from '../domain/inquiryReport' -export type OfferStep = 'select_properties' | 'pdf_review' | 'checked' | 'send' +export type OfferStep = 'select_properties' | 'select_fields' | 'pdf_review' | 'checked' | 'send' interface OfferWizardState { isOpen: boolean @@ -11,6 +12,7 @@ interface OfferWizardState { currentStep: OfferStep offerDraftId: string | null editableFields: Record + fieldSelections: ReportObjectFieldSelection[] pdfPreviewReady: boolean messageDraft: string messageSubject: string @@ -22,6 +24,8 @@ interface OfferWizardState { setStep(step: OfferStep): void setOfferDraftId(id: string): void updateField(fieldId: string, value: string): void + setFieldSelections(selections: ReportObjectFieldSelection[]): void + updateFieldSelection(propertyId: string, selection: ReportObjectFieldSelection): void setPdfReady(): void setMessageDraft(text: string): void setMessageSubject(s: string): void @@ -38,6 +42,7 @@ export const useOfferWizardStore = create((set) => ({ currentStep: 'select_properties', offerDraftId: null, editableFields: {}, + fieldSelections: [], pdfPreviewReady: false, messageDraft: '', messageSubject: '', @@ -51,6 +56,7 @@ export const useOfferWizardStore = create((set) => ({ currentStep: 'select_properties', offerDraftId: null, editableFields: {}, + fieldSelections: [], pdfPreviewReady: false, messageDraft: '', messageSubject: '', @@ -68,6 +74,11 @@ export const useOfferWizardStore = create((set) => ({ setOfferDraftId: (id) => set({ offerDraftId: id }), updateField: (fieldId, value) => set((s) => ({ editableFields: { ...s.editableFields, [fieldId]: value } })), + setFieldSelections: (fieldSelections) => set({ fieldSelections }), + updateFieldSelection: (propertyId, selection) => + set((s) => ({ + fieldSelections: s.fieldSelections.map(fs => fs.propertyId === propertyId ? selection : fs), + })), setPdfReady: () => set({ pdfPreviewReady: true }), setMessageDraft: (messageDraft) => set({ messageDraft }), setMessageSubject: (messageSubject) => set({ messageSubject }), @@ -83,6 +94,7 @@ export const useOfferWizardStore = create((set) => ({ currentStep: 'select_properties', offerDraftId: null, editableFields: {}, + fieldSelections: [], pdfPreviewReady: false, messageDraft: '', messageSubject: '',