@bytebrand/fe-ui-core-autobahn 1.0.80 → 1.0.82
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.
- package/cards/CarCard.tsx +14 -13
- package/cards/cardParts.tsx +7 -5
- package/compat/CookieModal/modals/ManageCookieModal/ManageCookieModal.tsx +10 -8
- package/compat/MdAccordionGroup/MdAccordionGroup.tsx +1 -1
- package/compat/MdField/MdField.tsx +29 -5
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +9 -2
- package/locales/en/autobahn.json +9 -2
- package/package.json +1 -1
- package/theme/md3.css +167 -4
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/FinanceCalculator.tsx +146 -39
- package/vdp/VdpModules.tsx +184 -20
package/utils/financeFormat.ts
CHANGED
|
@@ -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
|
+
}
|
package/utils/priceRating.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -196,27 +196,100 @@ export const ToggleRow: React.FunctionComponent<{
|
|
|
196
196
|
className?: string;
|
|
197
197
|
/** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
|
|
198
198
|
staticNote?: string;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
199
|
+
/** when set, the info icon becomes clickable and opens a popover with this body (e.g. the
|
|
200
|
+
* Rundum-Sorglos-Paket contents) — "Mit Schlussrate" rows omit this, keeping their icon inert. */
|
|
201
|
+
tooltip?: React.ReactNode;
|
|
202
|
+
/** aria-label for the popover's close button — required together with `tooltip`. */
|
|
203
|
+
tooltipCloseLabel?: string;
|
|
204
|
+
}> = ({ label, price, checked, disabled, onChange, className, staticNote, tooltip, tooltipCloseLabel }) => {
|
|
205
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
className={className}
|
|
209
|
+
style={{
|
|
210
|
+
display: 'flex',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
justifyContent: staticNote ? undefined : 'space-between',
|
|
213
|
+
gap: 10,
|
|
214
|
+
position: tooltip ? 'relative' : undefined,
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<span
|
|
218
|
+
className="md-title-sm"
|
|
219
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 5, cursor: tooltip ? 'pointer' : undefined }}
|
|
220
|
+
role={tooltip ? 'button' : undefined}
|
|
221
|
+
tabIndex={tooltip ? 0 : undefined}
|
|
222
|
+
onClick={tooltip ? (e) => { e.stopPropagation(); setTooltipOpen((v) => !v); } : undefined}
|
|
223
|
+
>
|
|
224
|
+
{label}
|
|
225
|
+
{tooltip && <Icon name="info" size={13} color="var(--md-on-surface-variant)" />}
|
|
226
|
+
</span>
|
|
227
|
+
{tooltip && tooltipOpen && (
|
|
228
|
+
<div className="pp-uvp-tooltip fin-toggle-tooltip" onClick={(e) => e.stopPropagation()}>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
className="pp-uvp-tooltip-close fin-toggle-tooltip-close"
|
|
232
|
+
aria-label={tooltipCloseLabel}
|
|
233
|
+
onClick={(e) => {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
setTooltipOpen(false);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
239
|
+
<path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
<div className="pp-uvp-tooltip-body fin-toggle-tooltip-body">{tooltip}</div>
|
|
243
|
+
</div>
|
|
217
244
|
)}
|
|
245
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
|
|
246
|
+
{price && (
|
|
247
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
|
|
248
|
+
{price}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
{staticNote ? (
|
|
252
|
+
<span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
|
|
253
|
+
{staticNote}
|
|
254
|
+
</span>
|
|
255
|
+
) : (
|
|
256
|
+
<Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
218
259
|
</div>
|
|
219
|
-
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** body content for the "Rundum-Sorglos-Paket" ToggleRow tooltip — intro sentence, bold heading,
|
|
264
|
+
* the package's contents as a bullet list, then (financing/leasing only) a closing paragraph
|
|
265
|
+
* explaining the one-off-vs-financed payment choice. Shared by every addServicesToggle
|
|
266
|
+
* ToggleRow (FinanceCalculator's own card + VdpModules' modal/mobile-sheet instances); intro/
|
|
267
|
+
* outro vary by mode since cash mode has no "financed into the rate" option. */
|
|
268
|
+
export const AddServicesTooltipBody: React.FunctionComponent<{ t: TranslationFunction; isCash?: boolean; isLease?: boolean }> = ({
|
|
269
|
+
t,
|
|
270
|
+
isCash,
|
|
271
|
+
isLease,
|
|
272
|
+
}) => (
|
|
273
|
+
<>
|
|
274
|
+
<p style={{ margin: '0 0 8px' }}>
|
|
275
|
+
{t(isCash ? 'autobahn:finance.addServicesTooltipIntroCash' : 'autobahn:finance.addServicesTooltipIntroFinanced')}
|
|
276
|
+
</p>
|
|
277
|
+
<p style={{ fontFamily: 'Arial', fontWeight: 700, fontSize: 10, lineHeight: '13px', color: '#0E2238', margin: '0 0 4px' }}>
|
|
278
|
+
{t('autobahn:finance.addServicesTooltipHeading')}
|
|
279
|
+
</p>
|
|
280
|
+
<ul style={{ margin: 0, padding: '0 0 0 14px' }}>
|
|
281
|
+
{t('autobahn:finance.addServicesTooltipItems')
|
|
282
|
+
.split('\n')
|
|
283
|
+
.map((item) => (
|
|
284
|
+
<li key={item}>{item}</li>
|
|
285
|
+
))}
|
|
286
|
+
</ul>
|
|
287
|
+
{!isCash && (
|
|
288
|
+
<p style={{ margin: '8px 0 0' }}>
|
|
289
|
+
{t(isLease ? 'autobahn:finance.addServicesTooltipOutroLeasing' : 'autobahn:finance.addServicesTooltipOutroFinancing')}
|
|
290
|
+
</p>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
220
293
|
);
|
|
221
294
|
|
|
222
295
|
export interface FinanceCalculatorProps {
|
|
@@ -278,6 +351,12 @@ export interface FinanceCalculatorProps {
|
|
|
278
351
|
phone?: string;
|
|
279
352
|
/** primary CTA click (Angebot sichern). */
|
|
280
353
|
onSecure?: () => void;
|
|
354
|
+
/** true once the customer already has an offer request/checkout in progress for this car —
|
|
355
|
+
* turns the primary CTA green (see .fin-cta-secured) instead of the default filled style. */
|
|
356
|
+
secured?: boolean;
|
|
357
|
+
/** overrides the primary CTA label while `secured` is true (e.g. "Zur Bestellung"). Ignored
|
|
358
|
+
* when `secured` is false — the CTA keeps its own default label in that case. */
|
|
359
|
+
secureLabel?: string;
|
|
281
360
|
/** secondary CTA click (phone). */
|
|
282
361
|
onCall?: () => void;
|
|
283
362
|
/** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
|
|
@@ -374,6 +453,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
374
453
|
available,
|
|
375
454
|
phone = '0800 40 30 182',
|
|
376
455
|
onSecure,
|
|
456
|
+
secured,
|
|
457
|
+
secureLabel,
|
|
377
458
|
onCall,
|
|
378
459
|
onShowDetails,
|
|
379
460
|
onOpenAdjust,
|
|
@@ -393,10 +474,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
393
474
|
// price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
|
|
394
475
|
const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
|
|
395
476
|
const evalTier = tierIdx >= 0 ? tierIdx : 1;
|
|
396
|
-
// both green tiers ('top' and 'good') collapse onto one "Top-Preis" claim (same convention as
|
|
397
|
-
// CarCard's PriceEvalCol) — footnote 4 carries the disclaimer text.
|
|
398
477
|
const isGoodTier = TIER_KEYS[evalTier] === 'good';
|
|
399
|
-
|
|
478
|
+
// each tier shows its own real label now (Top/Guter/Fairer/Erhöht/Hoher Preis) — no more
|
|
479
|
+
// collapsing 'good' onto the "Top-Preis" claim. Pill styling (colour/class/footnote) unchanged.
|
|
480
|
+
const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
|
|
400
481
|
const evalPillColor = isGoodTier ? TIER_COLORS[0] : TIER_COLORS[evalTier];
|
|
401
482
|
// condition summary (current selection) + its own details link — financing: "0 € Anzahlung ·
|
|
402
483
|
// 60 Monate · Finanzierungsdetails"; leasing: "25.000 € Sonderzahlung · 60 Mon. · 10.000
|
|
@@ -456,8 +537,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
456
537
|
>
|
|
457
538
|
{k === 'cash' ? (
|
|
458
539
|
<>
|
|
459
|
-
{/*
|
|
460
|
-
<span className="hide-mobile">{t(
|
|
540
|
+
{/* cash tab is "Kaufen" on both phone and desktop now — same key both spans. */}
|
|
541
|
+
<span className="hide-mobile">{t('autobahn:card.price.buy')}</span>
|
|
461
542
|
<span className="show-mobile" style={{ justifyContent: 'center' }}>
|
|
462
543
|
{t('autobahn:card.price.buy')}
|
|
463
544
|
</span>
|
|
@@ -636,7 +717,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
636
717
|
3
|
|
637
718
|
</span>
|
|
638
719
|
{uvpTooltipOpen && (
|
|
639
|
-
<div
|
|
720
|
+
<div
|
|
721
|
+
className={'pp-uvp-tooltip' + (mode === 'leasing' ? ' pp-uvp-tooltip--leasing' : '')}
|
|
722
|
+
onClick={(e) => e.stopPropagation()}
|
|
723
|
+
>
|
|
640
724
|
<button
|
|
641
725
|
type="button"
|
|
642
726
|
className="pp-uvp-tooltip-close"
|
|
@@ -737,17 +821,29 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
737
821
|
</button>
|
|
738
822
|
{adjust &&
|
|
739
823
|
(isCash ? (
|
|
740
|
-
|
|
741
|
-
<
|
|
742
|
-
|
|
743
|
-
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
824
|
+
<>
|
|
825
|
+
<div style={{ marginTop: 4 }}>
|
|
826
|
+
<CalcRow label={t('autobahn:finance.paymentLabel')} last={!available && !(addServicesCost != null && onToggleAddServices)}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
827
|
+
{available && (
|
|
828
|
+
<CalcRow label={t('autobahn:finance.availabilityLabel')} last={!(addServicesCost != null && onToggleAddServices)}>
|
|
829
|
+
{t('autobahn:finance.availabilityValue')}
|
|
830
|
+
</CalcRow>
|
|
831
|
+
)}
|
|
832
|
+
</div>
|
|
833
|
+
{addServicesCost != null && onToggleAddServices && (
|
|
834
|
+
<ToggleRow
|
|
835
|
+
className="fin-toggle-addservices fin-toggle-addservices--cash"
|
|
836
|
+
label={t('autobahn:finance.addServicesToggle')}
|
|
837
|
+
price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
|
|
838
|
+
checked={!!withAddServices}
|
|
839
|
+
disabled={addServicesDisabled}
|
|
840
|
+
onChange={onToggleAddServices}
|
|
841
|
+
staticNote={t('autobahn:finance.addServicesIncluded')}
|
|
842
|
+
tooltip={<AddServicesTooltipBody t={t} isCash />}
|
|
843
|
+
tooltipCloseLabel={t('autobahn:modal.close')}
|
|
844
|
+
/>
|
|
749
845
|
)}
|
|
750
|
-
|
|
846
|
+
</>
|
|
751
847
|
) : (
|
|
752
848
|
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
|
|
753
849
|
<div style={{ marginBottom: 15 }}>
|
|
@@ -808,6 +904,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
808
904
|
checked={!!withAddServices}
|
|
809
905
|
disabled={addServicesDisabled}
|
|
810
906
|
onChange={onToggleAddServices}
|
|
907
|
+
tooltip={<AddServicesTooltipBody t={t} isLease={isLease} />}
|
|
908
|
+
tooltipCloseLabel={t('autobahn:modal.close')}
|
|
811
909
|
/>
|
|
812
910
|
)}
|
|
813
911
|
</div>
|
|
@@ -815,8 +913,16 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
815
913
|
</div>
|
|
816
914
|
|
|
817
915
|
{/* REQUEST CTA */}
|
|
818
|
-
<Button
|
|
819
|
-
|
|
916
|
+
<Button
|
|
917
|
+
variant="filled"
|
|
918
|
+
block
|
|
919
|
+
size="lg"
|
|
920
|
+
icon="shield-check"
|
|
921
|
+
className={secured ? 'fin-cta-secured' : undefined}
|
|
922
|
+
style={{ marginTop: 14 }}
|
|
923
|
+
onClick={onSecure}
|
|
924
|
+
>
|
|
925
|
+
{secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
|
|
820
926
|
</Button>
|
|
821
927
|
{/* phone CTA — touch only (hidden on desktop via .fin-call) */}
|
|
822
928
|
<div className="fin-call">
|
|
@@ -848,11 +954,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
848
954
|
<Button
|
|
849
955
|
variant="filled"
|
|
850
956
|
size="lg"
|
|
957
|
+
className={secured ? 'fin-cta-secured' : undefined}
|
|
851
958
|
style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: 18 }}
|
|
852
959
|
onClick={onSecure}
|
|
853
960
|
>
|
|
854
|
-
<DocCheckIcon size={23} />
|
|
855
|
-
{t('autobahn:finance.secureOffer')}
|
|
961
|
+
{secured ? <Icon name="shield-check" size={18} /> : <DocCheckIcon size={23} />}
|
|
962
|
+
{secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
|
|
856
963
|
</Button>
|
|
857
964
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
|
858
965
|
<Button variant="outlined" size="lg" icon="phone" style={{ width: 112, height: 50 }} onClick={onCall}>
|