From f487435a94b6c82cb4aa5b9c754cc4bfc82a078e Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 24 May 2026 14:35:20 +0200 Subject: [PATCH] fix: correct rent unit labels and monthly/annual calculation consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NeedAlignmentPanel: display budget row in monthly (CHF/m²/Mt.) to match the Preis section, use exact division (no Math.round) to avoid 13×12≠152 - MatchDetailPropertySections + PropertyDetailPublicSections: replace Math.round(rentPricePerSqm/12) with exact division; show 2 decimal places when monthly is not a whole number (e.g. CHF 12.67 instead of CHF 13) - Add /Jahr suffix to all 15 displays showing rentPricePerSqm or maxPerSqm without a time unit across results, compare, match-detail, supply, and anfragencenter components Co-Authored-By: Claude Sonnet 4.6 --- src/components/anfragencenter/MockPdfPreview.tsx | 2 +- .../anfragencenter/RelatedPropertyCardPanel.tsx | 4 ++-- src/components/compare/CompareColumnHeader.tsx | 2 +- src/components/compare/CompareTableBody.tsx | 2 +- src/components/demand/CriteriaReviewPanel.tsx | 2 +- src/components/demand/NeedCardPreview.tsx | 2 +- .../match-detail/LocationIntelligencePanel.tsx | 2 +- .../match-detail/MatchDetailPropertySections.tsx | 7 +++++-- src/components/match-detail/NeedAlignmentPanel.tsx | 12 +++++++++--- .../match-detail/PropertyDetailPublicSections.tsx | 7 +++++-- .../match-detail/PropertyOverviewPanel.tsx | 2 +- src/components/results/UnifiedResultCard.tsx | 2 +- src/components/supply/NegotiationInsightsPanel.tsx | 4 ++-- src/components/supply/PropertyIntelligenceCard.tsx | 2 +- src/pages/demand/MatchDetail.tsx | 2 +- src/pages/demand/Results.tsx | 2 +- 16 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/components/anfragencenter/MockPdfPreview.tsx b/src/components/anfragencenter/MockPdfPreview.tsx index 16990f1..c0270d4 100644 --- a/src/components/anfragencenter/MockPdfPreview.tsx +++ b/src/components/anfragencenter/MockPdfPreview.tsx @@ -73,7 +73,7 @@ export function MockPdfPreview({ fields, fallbackFields }: MockPdfPreviewProps) variant="body2" sx={{ fontSize: '0.875rem', mb: 0.5, lineHeight: 1.5 }} > - {p.title} — {p.location.city}, {p.areaSqm.toLocaleString('de-CH')} m², CHF {p.rentPricePerSqm}/m² + {p.title} — {p.location.city}, {p.areaSqm.toLocaleString('de-CH')} m², CHF {p.rentPricePerSqm}/m²/Jahr ))} {properties.length === 0 && ( diff --git a/src/components/anfragencenter/RelatedPropertyCardPanel.tsx b/src/components/anfragencenter/RelatedPropertyCardPanel.tsx index 9ba6f3b..6905bc9 100644 --- a/src/components/anfragencenter/RelatedPropertyCardPanel.tsx +++ b/src/components/anfragencenter/RelatedPropertyCardPanel.tsx @@ -94,7 +94,7 @@ export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPrope - {property.areaSqm.toLocaleString('de-CH')} m² · CHF {property.rentPricePerSqm}/m² + {property.areaSqm.toLocaleString('de-CH')} m² · CHF {property.rentPricePerSqm}/m²/Jahr @@ -190,7 +190,7 @@ function AdditionalMatchCard({ match }: { match: AdditionalPropertyMatch }) { - {match.location} · {match.areaSqm.toLocaleString('de-CH')} m² · CHF {match.rentPricePerSqm}/m² + {match.location} · {match.areaSqm.toLocaleString('de-CH')} m² · CHF {match.rentPricePerSqm}/m²/Jahr {match.reasons.length > 0 && ( diff --git a/src/components/compare/CompareColumnHeader.tsx b/src/components/compare/CompareColumnHeader.tsx index e94a82d..eeab57d 100644 --- a/src/components/compare/CompareColumnHeader.tsx +++ b/src/components/compare/CompareColumnHeader.tsx @@ -43,7 +43,7 @@ export function CompareColumnHeader({ item, onRemove }: Props) { propertyId: prop?.id, propertyAddress: prop ? `${title}, ${subtitle}${district ? `, ${district}` : ''}` : undefined, areaLabel: prop?.areaSqm ? `${prop.areaSqm.toLocaleString('de-CH')} m²` : undefined, - rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²` : undefined, + rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²/Jahr` : undefined, availabilityLabel: prop?.availabilityDate ?? undefined, }) } diff --git a/src/components/compare/CompareTableBody.tsx b/src/components/compare/CompareTableBody.tsx index fa83728..c3a8006 100644 --- a/src/components/compare/CompareTableBody.tsx +++ b/src/components/compare/CompareTableBody.tsx @@ -155,7 +155,7 @@ export function CompareTableBody({ return ( - CHF {prop.rentPricePerSqm}/m² + CHF {prop.rentPricePerSqm}/m²/Jahr {prop.totalRentMonthly && ( diff --git a/src/components/demand/CriteriaReviewPanel.tsx b/src/components/demand/CriteriaReviewPanel.tsx index 113f159..d031e7b 100644 --- a/src/components/demand/CriteriaReviewPanel.tsx +++ b/src/components/demand/CriteriaReviewPanel.tsx @@ -38,7 +38,7 @@ function displayAreaRange(v: ParsedNeedCriteria['areaRange']): string { return v ? `${v.min}–${v.max} m²` : '' } function displayBudget(v: ParsedNeedCriteria['budgetRange']): string { - return v ? `CHF ${v.maxPerSqm}/m²` : '' + return v ? `CHF ${v.maxPerSqm}/m²/Jahr` : '' } function displayTiming(v: ParsedNeedCriteria['timing']): string { if (!v) return '' diff --git a/src/components/demand/NeedCardPreview.tsx b/src/components/demand/NeedCardPreview.tsx index 77efd59..40ea99e 100644 --- a/src/components/demand/NeedCardPreview.tsx +++ b/src/components/demand/NeedCardPreview.tsx @@ -95,7 +95,7 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi Budget - max. CHF {c.budgetRange.maxPerSqm}/m² + max. CHF {c.budgetRange.maxPerSqm}/m²/Jahr )} diff --git a/src/components/match-detail/LocationIntelligencePanel.tsx b/src/components/match-detail/LocationIntelligencePanel.tsx index e780af6..aaab91e 100644 --- a/src/components/match-detail/LocationIntelligencePanel.tsx +++ b/src/components/match-detail/LocationIntelligencePanel.tsx @@ -241,7 +241,7 @@ export function LocationIntelligencePanel({ property }: Props) { {p.title} - {p.areaSqm} m² · CHF {p.rentPricePerSqm}/m² + {p.areaSqm} m² · CHF {p.rentPricePerSqm}/m²/Jahr {p.rentPricePerSqm < property.rentPricePerSqm && ' · günstiger'} {p.rentPricePerSqm > property.rentPricePerSqm && ' · teurer'} diff --git a/src/components/match-detail/MatchDetailPropertySections.tsx b/src/components/match-detail/MatchDetailPropertySections.tsx index 447918d..1d80d0b 100644 --- a/src/components/match-detail/MatchDetailPropertySections.tsx +++ b/src/components/match-detail/MatchDetailPropertySections.tsx @@ -17,8 +17,11 @@ export function MatchDetailPropertySections({ property, match }: MatchDetailProp const flexibleUnits = units.filter(u => u.isFlexible && u.minLettableSqm != null) const preMarketUnits = units.filter(u => u.schattenmarktRelease?.enabled) const otherUnits = units.filter(u => !u.schattenmarktRelease?.enabled) - const monthlyPerSqm = Math.round(property.rentPricePerSqm / 12) const totalMonthly = Math.round(property.areaSqm * property.rentPricePerSqm / 12) + const rawMonthlyPerSqm = property.rentPricePerSqm / 12 + const monthlyPerSqmLabel = Number.isInteger(rawMonthlyPerSqm) + ? `CHF ${rawMonthlyPerSqm}.–` + : `CHF ${rawMonthlyPerSqm.toLocaleString('de-CH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` const minLettable = property.areaSqmMin ?? (flexibleUnits.length > 0 ? Math.min(...flexibleUnits.map(u => u.minLettableSqm!)) : undefined) const sourceLabel = property.sourceLabel ?? property.sourceMeta?.sourceLabel ?? SOURCE_LABELS[property.sourceType] ?? property.sourceType @@ -31,7 +34,7 @@ export function MatchDetailPropertySections({ property, match }: MatchDetailProp Preis - + {property.ancillaryCosts != null && ( diff --git a/src/components/match-detail/NeedAlignmentPanel.tsx b/src/components/match-detail/NeedAlignmentPanel.tsx index 52d30f7..807b38a 100644 --- a/src/components/match-detail/NeedAlignmentPanel.tsx +++ b/src/components/match-detail/NeedAlignmentPanel.tsx @@ -56,16 +56,22 @@ function buildRows(need: Need, property: Property): AlignmentRow[] { fit: locationFit, }) - // Budget + // Budget — both values are annual CHF/m²; display as monthly for consistency with Preis section const budgetFit: FitStatus = property.rentPricePerSqm <= need.budgetRange.maxPerSqm ? 'MATCH' : property.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.1 ? 'PARTIAL' : 'NO_MATCH' + function fmtMt(annual: number): string { + const m = annual / 12 + return Number.isInteger(m) + ? `CHF ${m}.–/m²/Mt.` + : `CHF ${m.toLocaleString('de-CH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}/m²/Mt.` + } rows.push({ label: 'Budget', - needValue: `max. CHF ${need.budgetRange.maxPerSqm}/m²`, - resultValue: `CHF ${property.rentPricePerSqm}/m²`, + needValue: `max. ${fmtMt(need.budgetRange.maxPerSqm)}`, + resultValue: fmtMt(property.rentPricePerSqm), fit: budgetFit, }) diff --git a/src/components/match-detail/PropertyDetailPublicSections.tsx b/src/components/match-detail/PropertyDetailPublicSections.tsx index 66980c2..285ee32 100644 --- a/src/components/match-detail/PropertyDetailPublicSections.tsx +++ b/src/components/match-detail/PropertyDetailPublicSections.tsx @@ -37,8 +37,11 @@ export function PropertyDetailPublicSections({ property, highlightUnitId }: Prop const otherUnits = (property.units ?? []).filter(u => !u.schattenmarktRelease?.enabled) const flexibleUnits = (property.units ?? []).filter(u => u.isFlexible && u.minLettableSqm !== undefined) - const monthlyPerSqm = Math.round(property.rentPricePerSqm / 12) const totalMonthly = Math.round(property.areaSqm * property.rentPricePerSqm / 12) + const rawMonthlyPerSqm = property.rentPricePerSqm / 12 + const monthlyPerSqmLabel = Number.isInteger(rawMonthlyPerSqm) + ? `CHF ${rawMonthlyPerSqm}.–` + : `CHF ${rawMonthlyPerSqm.toLocaleString('de-CH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` const minLettable = property.areaSqmMin ?? (flexibleUnits.length > 0 ? Math.min(...flexibleUnits.map(u => u.minLettableSqm!)) @@ -58,7 +61,7 @@ export function PropertyDetailPublicSections({ property, highlightUnitId }: Prop Preis - + {property.ancillaryCosts != null && ( , label: 'Standort', value: property ? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}` : '–' }, { icon: , label: 'Nutzfläche', value: property ? `${property.areaSqm} m²` : '–' }, - { icon: , label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²` : '–' }, + { icon: , label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : '–' }, { icon: , label: 'Verfügbar ab', value: property?.availabilityDate ?? '–' }, { icon: , label: 'Objekttyp', value: property?.assetType ?? '–' }, ] diff --git a/src/components/results/UnifiedResultCard.tsx b/src/components/results/UnifiedResultCard.tsx index c076495..868be47 100644 --- a/src/components/results/UnifiedResultCard.tsx +++ b/src/components/results/UnifiedResultCard.tsx @@ -44,7 +44,7 @@ export function UnifiedResultCard({ result, view = 'list' }: Props) { propertyId: prop?.id, propertyAddress: prop ? `${prop.title}, ${prop.location?.city ?? ''}` : undefined, areaLabel: prop?.areaSqm ? `${prop.areaSqm.toLocaleString('de-CH')} m²` : undefined, - rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²` : undefined, + rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²/Jahr` : undefined, }) }, }, diff --git a/src/components/supply/NegotiationInsightsPanel.tsx b/src/components/supply/NegotiationInsightsPanel.tsx index baae88e..66f6567 100644 --- a/src/components/supply/NegotiationInsightsPanel.tsx +++ b/src/components/supply/NegotiationInsightsPanel.tsx @@ -50,7 +50,7 @@ export function NegotiationInsightsPanel({ property }: Props) { Ihr Preis - CHF {property.rentPricePerSqm}/m² + CHF {property.rentPricePerSqm}/m²/Jahr {marketRent && ( @@ -130,7 +130,7 @@ export function NegotiationInsightsPanel({ property }: Props) { {n.companyName} {n.requiredArea?.min ?? 0}–{n.requiredArea?.max ?? 0} m² - {n.budgetRange?.maxPerSqm ? ` · max. CHF ${n.budgetRange.maxPerSqm}/m²` : ''} + {n.budgetRange?.maxPerSqm ? ` · max. CHF ${n.budgetRange.maxPerSqm}/m²/Jahr` : ''} - + {p.contractDurationMonths && ( )} diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx index 9a651af..7dd587c 100644 --- a/src/pages/demand/MatchDetail.tsx +++ b/src/pages/demand/MatchDetail.tsx @@ -108,7 +108,7 @@ export default function MatchDetail() { matchScore: match.matchScore, location: property?.location?.city, areaLabel: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : undefined, - rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²` : undefined, + rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined, }) } diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index 041292c..d3a306f 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -155,7 +155,7 @@ export default function Results() { {activeNeed.budgetRange && activeNeed.budgetRange.maxPerSqm > 0 && ( - Budget: max. CHF {activeNeed.budgetRange.maxPerSqm}/m² + Budget: max. CHF {activeNeed.budgetRange.maxPerSqm}/m²/Jahr )} {activeNeed.timing?.earliestMoveIn && !isNaN(new Date(activeNeed.timing.earliestMoveIn).getTime()) && (