da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
252 lines
11 KiB
TypeScript
252 lines
11 KiB
TypeScript
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<string, string>
|
|
fallbackFields: Record<string, string>
|
|
fieldSelections: ReportObjectFieldSelection[]
|
|
}
|
|
|
|
const FIELD_LABELS: Partial<Record<ReportObjectFieldKey, string>> = {
|
|
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] ?? ''
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
bgcolor: 'white',
|
|
borderRadius: 1.5,
|
|
border: '1px solid #cbd5e1',
|
|
boxShadow: '0 4px 16px rgba(15,23,42,0.08)',
|
|
p: 4,
|
|
height: '100%',
|
|
overflowY: 'auto',
|
|
fontFamily: '"Georgia", "Times New Roman", serif',
|
|
}}
|
|
>
|
|
{/* ── Page 1 — Offer letter ── */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3, color: '#64748b' }}>
|
|
<FileText size={16} />
|
|
<Typography variant="caption" sx={{ textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.7rem' }}>
|
|
Angebotsvorschau (PDF)
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Typography variant="caption" sx={{ display: 'block', color: '#94a3b8', textAlign: 'right' }}>
|
|
Wincasa AG · Zürich
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ display: 'block', color: '#94a3b8', textAlign: 'right', mb: 4 }}>
|
|
{new Date().toLocaleDateString('de-CH')}
|
|
</Typography>
|
|
|
|
<Typography variant="h6" sx={{ fontWeight: 700, color: '#0f172a', mb: 2, fontSize: '1.05rem' }}>
|
|
Angebot: {needTitle}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" sx={{ mb: 1.5, fontSize: '0.875rem' }}>
|
|
{v('recipient_salutation')},
|
|
</Typography>
|
|
|
|
<Typography variant="body2" sx={{ mb: 2, fontSize: '0.875rem', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
|
|
{v('offer_intro')}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 700, fontSize: '0.875rem' }}>
|
|
Hervorgehobene Kriterien
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ mb: 2.5, fontSize: '0.875rem', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
|
|
{v('highlighted_criteria')}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 700, fontSize: '0.875rem' }}>
|
|
Vorgeschlagene Objekte
|
|
</Typography>
|
|
<Box component="ul" sx={{ pl: 2, mb: 2.5 }}>
|
|
{properties.map(p => (
|
|
<Typography
|
|
key={p.id}
|
|
component="li"
|
|
variant="body2"
|
|
sx={{ fontSize: '0.875rem', mb: 0.5, lineHeight: 1.5 }}
|
|
>
|
|
<strong>{p.title}</strong> — {p.location.city}, {p.areaSqm.toLocaleString('de-CH')} m², CHF {p.rentPricePerSqm}/m²/Jahr
|
|
</Typography>
|
|
))}
|
|
{properties.length === 0 && (
|
|
<Typography variant="body2" sx={{ color: '#94a3b8', fontStyle: 'italic' }}>
|
|
Keine Objekte ausgewählt.
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
|
|
<Typography variant="body2" sx={{ mb: 2.5, fontSize: '0.875rem', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
|
|
{v('next_steps')}
|
|
</Typography>
|
|
|
|
<Typography variant="body2" sx={{ fontSize: '0.875rem', whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
|
|
{v('closing')}
|
|
</Typography>
|
|
|
|
{/* ── Page 2 — Property details side by side ── */}
|
|
{hasPropertyPage && (
|
|
<>
|
|
<Box sx={{ my: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
<Divider sx={{ flex: 1, borderColor: '#152642', borderWidth: 1 }} />
|
|
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.68rem', whiteSpace: 'nowrap', letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
|
Seite 2 — Objektdetails
|
|
</Typography>
|
|
<Divider sx={{ flex: 1, borderColor: '#152642', borderWidth: 1 }} />
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'grid',
|
|
gridTemplateColumns: `repeat(${Math.min(properties.length, 3)}, 1fr)`,
|
|
gap: 1.5,
|
|
}}
|
|
>
|
|
{properties.map(p => {
|
|
const selection = fieldSelections.find(fs => fs.propertyId === p.id)
|
|
const optionalFields = selection?.selectedOptionalFields ?? []
|
|
const image = p.images?.[0]
|
|
|
|
return (
|
|
<Box
|
|
key={p.id}
|
|
sx={{
|
|
border: '1px solid #e2e8f0',
|
|
borderRadius: 1,
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
{/* Photo */}
|
|
{image ? (
|
|
<Box
|
|
component="img"
|
|
src={image}
|
|
alt={p.title}
|
|
sx={{ width: '100%', height: 90, objectFit: 'cover', display: 'block' }}
|
|
/>
|
|
) : (
|
|
<Box sx={{ height: 90, bgcolor: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Building2 size={22} color="#94a3b8" />
|
|
</Box>
|
|
)}
|
|
|
|
{/* Info */}
|
|
<Box sx={{ p: 1.25, flex: 1 }}>
|
|
<Typography sx={{ fontWeight: 700, fontSize: '0.78rem', color: '#0f172a', lineHeight: 1.3, mb: 0.4 }}>
|
|
{p.title}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3, mb: 1 }}>
|
|
<MapPin size={10} color="#94a3b8" />
|
|
<Typography sx={{ fontSize: '0.68rem', color: '#64748b' }}>
|
|
{p.location.city}{p.location.district ? ` · ${p.location.district}` : ''}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Selected fields */}
|
|
{optionalFields.length > 0 && (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4 }}>
|
|
{optionalFields.map(key => {
|
|
const val = getFieldValue(p, key)
|
|
if (!val) return null
|
|
return (
|
|
<Box key={key} sx={{ display: 'flex', justifyContent: 'space-between', gap: 0.5 }}>
|
|
<Typography sx={{ fontSize: '0.62rem', color: '#94a3b8' }}>
|
|
{FIELD_LABELS[key] ?? key}
|
|
</Typography>
|
|
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600, color: '#0f172a', textAlign: 'right' }}>
|
|
{val}
|
|
</Typography>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
</>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|