fix: correct rent unit labels and monthly/annual calculation consistency

- 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 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 14:35:20 +02:00
parent d4171fe9b5
commit f487435a94
16 changed files with 34 additions and 22 deletions
@@ -241,7 +241,7 @@ export function LocationIntelligencePanel({ property }: Props) {
{p.title}
</Typography>
<Typography variant="caption" color="text.secondary">
{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'}
</Typography>
@@ -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
<Typography variant="h6" sx={{ fontWeight: 700 }}>Preis</Typography>
</Box>
<KeyFactRow label="Monatliche Miete" value={`CHF ${totalMonthly.toLocaleString('de-CH')}.`} />
<KeyFactRow label="Pro m²/Monat" value={`CHF ${monthlyPerSqm.toLocaleString('de-CH')}.`} />
<KeyFactRow label="Pro m²/Monat" value={monthlyPerSqmLabel} />
<KeyFactRow label="Pro m²/Jahr" value={`CHF ${property.rentPricePerSqm.toLocaleString('de-CH')}.`} />
{property.ancillaryCosts != null && (
<KeyFactRow label="Nebenkosten" value={`CHF ${Math.round(property.areaSqm * property.ancillaryCosts / 12).toLocaleString('de-CH')}/Mt. (CHF ${property.ancillaryCosts}/m²/a)`} />
@@ -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,
})
@@ -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
<Typography variant="h6" sx={{ fontWeight: 700 }}>Preis</Typography>
</Box>
<KeyFactRow label="Monatliche Miete" value={`CHF ${totalMonthly.toLocaleString('de-CH')}.`} />
<KeyFactRow label="Pro m²/Monat" value={`CHF ${monthlyPerSqm.toLocaleString('de-CH')}.`} />
<KeyFactRow label="Pro m²/Monat" value={monthlyPerSqmLabel} />
<KeyFactRow label="Pro m²/Jahr" value={`CHF ${property.rentPricePerSqm.toLocaleString('de-CH')}.`} />
{property.ancillaryCosts != null && (
<KeyFactRow
@@ -39,7 +39,7 @@ export function PropertyOverviewPanel({ match, property, signal }: Props) {
] : [
{ icon: <MapPin size={15} />, label: 'Standort', value: property ? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}` : '' },
{ icon: <Maximize2 size={15} />, label: 'Nutzfläche', value: property ? `${property.areaSqm}` : '' },
{ icon: <Banknote size={15} />, label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²` : '' },
{ icon: <Banknote size={15} />, label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : '' },
{ icon: <Calendar size={15} />, label: 'Verfügbar ab', value: property?.availabilityDate ?? '' },
{ icon: <Tag size={15} />, label: 'Objekttyp', value: property?.assetType ?? '' },
]