@bytebrand/fe-ui-core-autobahn 1.0.76 → 1.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,62 @@
1
- // Autobahn (auto.de redesign) — shared financing-number formatting.
2
- //
3
- // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
4
- // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
5
- // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
6
- // Node service.
7
- //
8
- // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
9
- // financing means monthly×term is NOT the total) and lives here once, not per-caller.
10
-
11
- /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
12
- * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
13
- export function formatEur(n: number): string {
14
- return n.toLocaleString('de-DE');
15
- }
16
-
17
- /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
18
- export function formatPct(n: number): string {
19
- return `${n.toFixed(2).replace('.', ',')} %`;
20
- }
21
-
22
- export interface FinanceExampleFields {
23
- price: number;
24
- apr: number;
25
- down: number;
26
- /** Real config figure — falls back to price-down ONLY when genuinely absent. */
27
- netLoanAmount?: number;
28
- nominalRate?: number;
29
- finalInstallment?: number;
30
- grossAmount?: number;
31
- }
32
-
33
- export interface FinanceExampleLabels {
34
- vehiclePrice: string;
35
- priceSuffix: string;
36
- apr: string;
37
- nominal: string;
38
- net: string;
39
- down: string;
40
- final: string;
41
- gross: string;
42
- }
43
-
44
- /**
45
- * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
46
- * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
47
- * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
48
- * `exampleLine`, byte-for-byte).
49
- */
50
- export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
51
- const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
52
- const parts: string[] = [
53
- `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
54
- ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
55
- ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
56
- `${labels.net}: ${formatEur(netto)} €`,
57
- `${labels.down}: ${formatEur(fields.down)} €`,
58
- ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
59
- ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
60
- ];
61
- return `${footnoteMarker} ${parts.join('; ')};`;
62
- }
1
+ // Autobahn (auto.de redesign) — shared financing-number formatting.
2
+ //
3
+ // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
4
+ // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
5
+ // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
6
+ // Node service.
7
+ //
8
+ // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
9
+ // financing means monthly×term is NOT the total) and lives here once, not per-caller.
10
+
11
+ /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
12
+ * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
13
+ export function formatEur(n: number): string {
14
+ return n.toLocaleString('de-DE');
15
+ }
16
+
17
+ /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
18
+ export function formatPct(n: number): string {
19
+ return `${n.toFixed(2).replace('.', ',')} %`;
20
+ }
21
+
22
+ export interface FinanceExampleFields {
23
+ price: number;
24
+ apr: number;
25
+ down: number;
26
+ /** Real config figure — falls back to price-down ONLY when genuinely absent. */
27
+ netLoanAmount?: number;
28
+ nominalRate?: number;
29
+ finalInstallment?: number;
30
+ grossAmount?: number;
31
+ }
32
+
33
+ export interface FinanceExampleLabels {
34
+ vehiclePrice: string;
35
+ priceSuffix: string;
36
+ apr: string;
37
+ nominal: string;
38
+ net: string;
39
+ down: string;
40
+ final: string;
41
+ gross: string;
42
+ }
43
+
44
+ /**
45
+ * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
46
+ * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
47
+ * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
48
+ * `exampleLine`, byte-for-byte).
49
+ */
50
+ export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
51
+ const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
52
+ const parts: string[] = [
53
+ `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
54
+ ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
55
+ ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
56
+ `${labels.net}: ${formatEur(netto)} €`,
57
+ `${labels.down}: ${formatEur(fields.down)} €`,
58
+ ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
59
+ ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
60
+ ];
61
+ return `${footnoteMarker} ${parts.join('; ')};`;
62
+ }
@@ -1,58 +1,58 @@
1
- // Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
2
- //
3
- // Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
4
- // `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
5
- // This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
6
- // module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
7
- // (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
8
- // on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
9
- // so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
10
- import { priceRatings, priceRatingConfig } from '../constants/price';
11
- import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
12
-
13
- export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
14
- const round = (value: number) => {
15
- return Number.isFinite(pricePredicted)
16
- ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
17
- : '???';
18
- };
19
-
20
- if (currentPrice === 0) {
21
- console.log('Empty current price');
22
- }
23
-
24
- return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
25
- const { min, max } = priceRatingConfig[key];
26
- return { ...acc, [key]: { min: round(min), max: round(max) } };
27
- }, {}); // tslint:disable-line
28
- };
29
-
30
- export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
31
- if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
32
- return predictablePrice;
33
- }
34
-
35
- return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
36
- }
37
-
38
- export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
39
- const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
40
- // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
41
-
42
- if (!isCorrectCurrentSalesPrice) return 0;
43
-
44
-
45
- const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
46
-
47
- const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
48
-
49
- for (const key of Object.keys(ratingConfig)) {
50
- const { min, max } = ratingConfig[key];
51
- const TOP_PRICE = 5;
52
- if (key === 'highPrice' && currentSalesPrice === max) return 1;
53
- if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
54
- if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
55
- }
56
-
57
- return 0;
58
- };
1
+ // Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
2
+ //
3
+ // Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
4
+ // `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
5
+ // This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
6
+ // module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
7
+ // (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
8
+ // on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
9
+ // so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
10
+ import { priceRatings, priceRatingConfig } from '../constants/price';
11
+ import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
12
+
13
+ export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
14
+ const round = (value: number) => {
15
+ return Number.isFinite(pricePredicted)
16
+ ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
17
+ : '???';
18
+ };
19
+
20
+ if (currentPrice === 0) {
21
+ console.log('Empty current price');
22
+ }
23
+
24
+ return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
25
+ const { min, max } = priceRatingConfig[key];
26
+ return { ...acc, [key]: { min: round(min), max: round(max) } };
27
+ }, {}); // tslint:disable-line
28
+ };
29
+
30
+ export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
31
+ if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
32
+ return predictablePrice;
33
+ }
34
+
35
+ return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
36
+ }
37
+
38
+ export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
39
+ const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
40
+ // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
41
+
42
+ if (!isCorrectCurrentSalesPrice) return 0;
43
+
44
+
45
+ const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
46
+
47
+ const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
48
+
49
+ for (const key of Object.keys(ratingConfig)) {
50
+ const { min, max } = ratingConfig[key];
51
+ const TOP_PRICE = 5;
52
+ if (key === 'highPrice' && currentSalesPrice === max) return 1;
53
+ if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
54
+ if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
55
+ }
56
+
57
+ return 0;
58
+ };
@@ -16,7 +16,7 @@
16
16
  import React, { useState } from 'react';
17
17
  import { TranslationFunction } from '../types';
18
18
 
19
- import { Button, Icon } from '../ui';
19
+ import { Button, Icon, Switch } from '../ui';
20
20
  import { TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
21
21
 
22
22
  /** Payment mode segment keys. Mirrors the design's ['fin','leasing','cash']. */
@@ -184,6 +184,41 @@ export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, ch
184
184
  </div>
185
185
  );
186
186
 
187
+ /** full-width label(+optional trailing price) / Switch row — Switch itself is inline-flex
188
+ * (track+label), so the "label ...... price switch" full-row layout can't be expressed via
189
+ * Switch's `label` prop (which renders track-then-label, not a full-width justified row). */
190
+ export const ToggleRow: React.FunctionComponent<{
191
+ label: string;
192
+ price?: string;
193
+ checked: boolean;
194
+ disabled?: boolean;
195
+ onChange: (checked: boolean) => void;
196
+ className?: string;
197
+ /** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
198
+ staticNote?: string;
199
+ }> = ({ label, price, checked, disabled, onChange, className, staticNote }) => (
200
+ <div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: staticNote ? undefined : 'space-between', gap: 10 }}>
201
+ <span className="md-title-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
202
+ {label}
203
+ <Icon name="info" size={13} color="var(--md-on-surface-variant)" />
204
+ </span>
205
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
206
+ {price && (
207
+ <span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
208
+ {price}
209
+ </span>
210
+ )}
211
+ {staticNote ? (
212
+ <span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
213
+ {staticNote}
214
+ </span>
215
+ ) : (
216
+ <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
217
+ )}
218
+ </div>
219
+ </div>
220
+ );
221
+
187
222
  export interface FinanceCalculatorProps {
188
223
  /** active payment mode (controlled). */
189
224
  mode: FinanceMode;
@@ -226,6 +261,19 @@ export interface FinanceCalculatorProps {
226
261
  annualMileageStep?: number;
227
262
  /** annual-mileage slider changed. */
228
263
  onAnnualMileageChange?: (km: number) => void;
264
+ /** "Mit Schlussrate" toggle state (financing only; leasing has no balloon concept). Omit
265
+ * onToggleFinalInstallment to hide the row. */
266
+ withFinalInstallment?: boolean;
267
+ onToggleFinalInstallment?: (checked: boolean) => void;
268
+ /** "Rundum-Sorglos-Paket" toggle state (fin/leasing). Omit addServicesCost or
269
+ * onToggleAddServices to hide the row. */
270
+ withAddServices?: boolean;
271
+ addServicesCost?: number;
272
+ addServicesDisabled?: boolean;
273
+ onToggleAddServices?: (checked: boolean) => void;
274
+ /** Kaufen tab only — real carStore.isAvailable, drives the Verfügbarkeit row (replaces the
275
+ * static Lieferung row). Same source as VdpTrustRow's "Sofort verfügbar" badge. */
276
+ available?: boolean;
229
277
  /** phone number shown on the secondary CTA (touch only). */
230
278
  phone?: string;
231
279
  /** primary CTA click (Angebot sichern). */
@@ -289,7 +337,7 @@ export const expanderBtnStyle: React.CSSProperties = {
289
337
  display: 'flex',
290
338
  alignItems: 'center',
291
339
  justifyContent: 'space-between',
292
- padding: '10px 0 2px',
340
+ padding: '0 0 2px',
293
341
  color: 'var(--md-primary)',
294
342
  fontWeight: 600,
295
343
  fontSize: 14,
@@ -317,6 +365,13 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
317
365
  annualMileageMax = 60000,
318
366
  annualMileageStep = 5000,
319
367
  onAnnualMileageChange,
368
+ withFinalInstallment,
369
+ onToggleFinalInstallment,
370
+ withAddServices,
371
+ addServicesCost,
372
+ addServicesDisabled,
373
+ onToggleAddServices,
374
+ available,
320
375
  phone = '0800 40 30 182',
321
376
  onSecure,
322
377
  onCall,
@@ -332,6 +387,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
332
387
  // whether the "UVP-Rate" footnote popover (fin/leasing) is open — same widget as CarCard's.
333
388
  const [uvpTooltipOpen, setUvpTooltipOpen] = useState(false);
334
389
  const isCash = mode === 'cash';
390
+ const isLease = mode === 'leasing';
335
391
  const terms = termOptions && termOptions.length ? termOptions : range(termMin, termMax, termStep);
336
392
  const showStrike = listPrice != null && listPrice > price;
337
393
  // price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
@@ -414,14 +470,20 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
414
470
  </div>
415
471
 
416
472
  {/* 2-col grid: rate|pill (row 1) + condLine|bar (row 2) — fixed right column prevents wrapping */}
417
- <div className="fin-price-grid" style={isCash ? { gap: '0px 16px' } : undefined}>
473
+ <div className={'fin-price-grid' + (isCash ? ' fin-price-grid--cash' : '')}>
418
474
  {/* col 1 row 1: hero rate — Figma: "329 €" all-big, then a small stacked column with
419
475
  the struck old rate ABOVE "/mtl." (round 11; was a single baseline row) */}
420
476
  <div className="fin-rate-main">
421
477
  {/* mobile Kaufen (Figma round 14): struck list price ABOVE the price, right-aligned,
422
478
  tight to the figure — replaces the Festpreis + UVP lines, which stay desktop-only */}
423
479
  {isCash && showStrike && (
424
- <div className="show-mobile" style={{ justifyContent: 'flex-end', alignItems: 'baseline', gap: 4, lineHeight: 1, marginTop: 7 }}>
480
+ <div
481
+ className="show-mobile"
482
+ style={{ justifyContent: 'flex-end', alignItems: 'baseline', gap: 4, lineHeight: 1, marginTop: 7 }}
483
+ role="button"
484
+ tabIndex={0}
485
+ onClick={() => setUvpTooltipOpen((v) => !v)}
486
+ >
425
487
  <span style={{ fontSize: 13, color: 'var(--md-on-surface-variant)' }}>{t('autobahn:finance.uvp')}</span>
426
488
  <span
427
489
  className={`num pp-hero-old${
@@ -465,6 +527,50 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
465
527
  </span>
466
528
  </div>
467
529
  )}
530
+ {/* desktop: same struck list price ABOVE the rate, same fin-rate-old-list styling +
531
+ clickable UVP badge/tooltip as the mobile sheet's own Kaufen block. */}
532
+ {isCash && showStrike && (
533
+ <div
534
+ className="hide-mobile"
535
+ style={{ display: 'flex', justifyContent: 'flex-end', lineHeight: 1, marginTop: 0, cursor: 'pointer' }}
536
+ role="button"
537
+ tabIndex={0}
538
+ onClick={() => setUvpTooltipOpen((v) => !v)}
539
+ >
540
+ <s className="num fin-rate-old fin-rate-old-list">
541
+ {eur(listPrice as number)} €
542
+ <span
543
+ className="pp-eyebrow-badge"
544
+ role="button"
545
+ tabIndex={0}
546
+ onClick={(e) => {
547
+ e.stopPropagation();
548
+ setUvpTooltipOpen((v) => !v);
549
+ }}
550
+ >
551
+ 2
552
+ </span>
553
+ {uvpTooltipOpen && (
554
+ <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
555
+ <button
556
+ type="button"
557
+ className="pp-uvp-tooltip-close"
558
+ aria-label={t('autobahn:modal.close')}
559
+ onClick={(e) => {
560
+ e.stopPropagation();
561
+ setUvpTooltipOpen(false);
562
+ }}
563
+ >
564
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
565
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
566
+ </svg>
567
+ </button>
568
+ <div className="pp-uvp-tooltip-body">{t('autobahn:card.price.uvpListTooltipBody')}</div>
569
+ </div>
570
+ )}
571
+ </s>
572
+ </div>
573
+ )}
468
574
  <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
469
575
  {isCash ? (
470
576
  <>
@@ -486,11 +592,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
486
592
  </span>
487
593
  <span
488
594
  className="num hide-mobile"
489
- style={{ fontSize: 48, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
595
+ style={{ fontSize: 45, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
490
596
  >
491
- {eur(price)}
597
+ {eur(price)} €<sup>1</sup>
492
598
  </span>
493
- <span className="md-body-sm hide-mobile" style={{ fontWeight: 600 }}>€</span>
494
599
  </>
495
600
  ) : (
496
601
  <>
@@ -511,7 +616,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
511
616
  <span className="fin-rate-unit">
512
617
  {/* struck old rate (REAL oldMonthlyInstallment; SRP strikes the same source) */}
513
618
  {oldMonthly != null && (
514
- <>
619
+ <span
620
+ style={{ display: 'flex', flexDirection: 'column', cursor: 'pointer' }}
621
+ role="button"
622
+ tabIndex={0}
623
+ onClick={() => setUvpTooltipOpen((v) => !v)}
624
+ >
515
625
  <span className="pp-eyebrow">
516
626
  {t('autobahn:card.price.uvpRateLabel')}
517
627
  <span
@@ -561,7 +671,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
561
671
  >
562
672
  {eur(Math.round(oldMonthly))} €
563
673
  </s>
564
- </>
674
+ </span>
565
675
  )}
566
676
  <span className="fin-rate-example-suffix">
567
677
  {t('autobahn:finance.perMonth')}
@@ -571,11 +681,6 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
571
681
  )}
572
682
  </div>
573
683
  {isCash && <div className="md-label-sm hide-mobile">{t('autobahn:finance.fixedPrice')}</div>}
574
- {isCash && showStrike && (
575
- <div className="fin-kauf hide-mobile">
576
- {t('autobahn:finance.uvp')} <s>{eur(listPrice as number)} €</s>
577
- </div>
578
- )}
579
684
  </div>
580
685
  {/* col 2: pill + eval bar grouped together (display:contents on desktop — no-op,
581
686
  preserves the original rate|pill / cond|bar auto-placement; mobile turns this into
@@ -634,14 +739,18 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
634
739
  (isCash ? (
635
740
  <div style={{ marginTop: 4 }}>
636
741
  <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
637
- <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
638
- <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
639
- {t('autobahn:finance.deliveryValue')}
742
+ <CalcRow label={t('autobahn:finance.warrantyLabel')} last={!available}>
743
+ {t('autobahn:finance.warrantyValue')}
640
744
  </CalcRow>
745
+ {available && (
746
+ <CalcRow label={t('autobahn:finance.availabilityLabel')} last>
747
+ {t('autobahn:finance.availabilityValue')}
748
+ </CalcRow>
749
+ )}
641
750
  </div>
642
751
  ) : (
643
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
644
- <div>
752
+ <div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
753
+ <div style={{ marginBottom: 15 }}>
645
754
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
646
755
  <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
647
756
  <span className="num" style={{ fontWeight: 700 }}>
@@ -658,7 +767,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
658
767
  />
659
768
  </div>
660
769
  {annualMileage != null && onAnnualMileageChange && (
661
- <div>
770
+ <div style={{ marginBottom: 15 }}>
662
771
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
663
772
  <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
664
773
  <span className="num" style={{ fontWeight: 700 }}>
@@ -675,19 +784,32 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
675
784
  />
676
785
  </div>
677
786
  )}
678
- <div>
787
+ <div style={{ marginBottom: 15 }}>
679
788
  <div className="md-title-sm" style={{ marginBottom: 8 }}>
680
789
  {t('autobahn:finance.termSelection')}
681
790
  </div>
682
- <TermChips options={terms} value={term} onChange={onTermChange} t={t} />
683
- </div>
684
- <div>
685
- <CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
686
- <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
687
- <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
688
- {t('autobahn:finance.deliveryValue')}
689
- </CalcRow>
791
+ <TermChips options={terms.filter((v) => v !== 12 && v !== 96)} value={term} onChange={onTermChange} t={t} />
690
792
  </div>
793
+ {/* same Mit-Schlussrate / Rundum-Sorglos-Paket toggles as the mobile sheet / modal
794
+ desktop block — replaces the static Kaufpreis/Garantie/Lieferung rows. */}
795
+ {!isLease && onToggleFinalInstallment && (
796
+ <ToggleRow
797
+ className="fin-toggle-final"
798
+ label={t('autobahn:finance.finalInstallmentToggle')}
799
+ checked={!!withFinalInstallment}
800
+ onChange={onToggleFinalInstallment}
801
+ />
802
+ )}
803
+ {addServicesCost != null && onToggleAddServices && (
804
+ <ToggleRow
805
+ className={'fin-toggle-addservices' + (isLease ? ' fin-toggle-addservices--leasing' : '')}
806
+ label={t('autobahn:finance.addServicesToggle')}
807
+ price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
808
+ checked={!!withAddServices}
809
+ disabled={addServicesDisabled}
810
+ onChange={onToggleAddServices}
811
+ />
812
+ )}
691
813
  </div>
692
814
  ))}
693
815
  </div>