From b517373538fb2dd73b81d27ba1c6cf026cffdd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Mar 2026 15:13:34 +0100 Subject: [PATCH] feat(back-end and front-end): calculator improvements --- .../printcalculator/dto/PrintSettingsDto.java | 9 +++++ .../quote/QuoteSessionItemService.java | 9 ++++- .../quote-result/quote-result.component.html | 2 ++ .../quote-result/quote-result.component.ts | 36 +------------------ .../services/quote-estimator.service.ts | 9 +++++ .../price-breakdown.component.html | 2 +- .../price-breakdown.component.ts | 1 + frontend/src/assets/i18n/de.json | 2 +- frontend/src/assets/i18n/en.json | 2 +- frontend/src/assets/i18n/fr.json | 2 +- frontend/src/assets/i18n/it.json | 2 +- 11 files changed, 35 insertions(+), 41 deletions(-) diff --git a/backend/src/main/java/com/printcalculator/dto/PrintSettingsDto.java b/backend/src/main/java/com/printcalculator/dto/PrintSettingsDto.java index 1d2a4df..c74a249 100644 --- a/backend/src/main/java/com/printcalculator/dto/PrintSettingsDto.java +++ b/backend/src/main/java/com/printcalculator/dto/PrintSettingsDto.java @@ -7,6 +7,7 @@ public class PrintSettingsDto { // Common private String material; // e.g. "PLA", "PLA TOUGH", "PETG" private String color; // e.g. "White", "#FFFFFF" + private Integer quantity; private Long filamentVariantId; private Long printerMachineId; @@ -58,6 +59,14 @@ public class PrintSettingsDto { this.filamentVariantId = filamentVariantId; } + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + public Long getPrinterMachineId() { return printerMachineId; } diff --git a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java index 38175b0..cae1ec5 100644 --- a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java +++ b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java @@ -239,7 +239,7 @@ public class QuoteSessionItemService { item.setQuoteSession(session); item.setOriginalFilename(originalFilename); item.setStoredPath(quoteStorageService.toStoredPath(persistentPath)); - item.setQuantity(1); + item.setQuantity(normalizeQuantity(settings.getQuantity())); item.setColorCode(selectedVariant.getColorName()); item.setFilamentVariant(selectedVariant); item.setMaterialCode(selectedVariant.getFilamentMaterialType() != null @@ -279,4 +279,11 @@ public class QuoteSessionItemService { item.setUpdatedAt(OffsetDateTime.now()); return item; } + + private int normalizeQuantity(Integer quantity) { + if (quantity == null || quantity < 1) { + return 1; + } + return quantity; + } } diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index e656156..d6caa39 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -5,6 +5,7 @@ [rows]="priceBreakdownRows()" [total]="costBreakdown().total" [currency]="result().currency" + [totalSuffix]="'*'" [totalLabelKey]="'CHECKOUT.TOTAL'" > @@ -68,6 +69,7 @@ [ngModel]="item.quantity" (ngModelChange)="updateQuantity(i, $event)" (blur)="flushQuantityUpdate(i)" + (keydown.enter)="flushQuantityUpdate(i)" class="qty-input" /> diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index 5121b07..0fc1c8c 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -1,6 +1,5 @@ import { Component, - OnDestroy, input, output, signal, @@ -34,10 +33,9 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; templateUrl: './quote-result.component.html', styleUrl: './quote-result.component.scss', }) -export class QuoteResultComponent implements OnDestroy { +export class QuoteResultComponent { readonly maxInputQuantity = 500; readonly directOrderLimit = 100; - readonly quantityAutoRefreshMs = 2000; result = input.required(); recalculationRequired = input(false); @@ -62,13 +60,10 @@ export class QuoteResultComponent implements OnDestroy { // Local mutable state for items to handle quantity changes items = signal([]); private lastSentQuantities = new Map(); - private quantityTimers = new Map>(); constructor() { effect( () => { - this.clearAllQuantityTimers(); - // Initialize local items when result inputs change // We map to new objects to avoid mutating the input directly if it was a reference const nextItems = this.result().items.map((i) => ({ ...i })); @@ -84,17 +79,12 @@ export class QuoteResultComponent implements OnDestroy { ); } - ngOnDestroy(): void { - this.clearAllQuantityTimers(); - } - updateQuantity(index: number, newQty: number | string) { const normalizedQty = this.normalizeQuantity(newQty); if (normalizedQty === null) return; const item = this.items()[index]; if (!item) return; - const key = item.id ?? item.fileName; this.items.update((current) => { const updated = [...current]; @@ -108,8 +98,6 @@ export class QuoteResultComponent implements OnDestroy { fileName: item.fileName, quantity: normalizedQty, }); - - this.scheduleQuantityRefresh(index, key); } flushQuantityUpdate(index: number): void { @@ -117,7 +105,6 @@ export class QuoteResultComponent implements OnDestroy { if (!item) return; const key = item.id ?? item.fileName; - this.clearQuantityRefreshTimer(key); const normalizedQty = this.normalizeQuantity(item.quantity); if (normalizedQty === null) return; @@ -213,27 +200,6 @@ export class QuoteResultComponent implements OnDestroy { return Math.min(qty, this.maxInputQuantity); } - private scheduleQuantityRefresh(index: number, key: string): void { - this.clearQuantityRefreshTimer(key); - const timer = setTimeout(() => { - this.quantityTimers.delete(key); - this.flushQuantityUpdate(index); - }, this.quantityAutoRefreshMs); - this.quantityTimers.set(key, timer); - } - - private clearQuantityRefreshTimer(key: string): void { - const timer = this.quantityTimers.get(key); - if (!timer) return; - clearTimeout(timer); - this.quantityTimers.delete(key); - } - - private clearAllQuantityTimers(): void { - this.quantityTimers.forEach((timer) => clearTimeout(timer)); - this.quantityTimers.clear(); - } - getItemDifferenceLabel(fileName: string, materialCode?: string): string { const differences = this.itemSettingsDiffByFileName()[fileName]?.differences || []; diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index 988cc7b..a499a96 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -447,6 +447,7 @@ export class QuoteEstimatorService { return { complexityMode: request.mode === 'easy' ? 'BASIC' : 'ADVANCED', + quantity: this.normalizeQuantity(item.quantity), material: String(item.material || request.material || 'PLA'), color: item.color || '#FFFFFF', filamentVariantId: item.filamentVariantId, @@ -475,6 +476,14 @@ export class QuoteEstimatorService { }; } + private normalizeQuantity(value: number | undefined): number { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric < 1) { + return 1; + } + return Math.floor(numeric); + } + private normalizeQuality(value: string | undefined): string { const normalized = String(value || 'standard') .trim() diff --git a/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.html b/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.html index c4269f8..2670c2d 100644 --- a/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.html +++ b/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.html @@ -20,6 +20,6 @@ {{ totalLabelKey() ? (totalLabelKey() | translate) : "" }} - {{ total() | currency: currency() }} + {{ total() | currency: currency() }}{{ totalSuffix() }} diff --git a/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.ts b/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.ts index 9db3ba3..28b5dca 100644 --- a/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.ts +++ b/frontend/src/app/shared/components/price-breakdown/price-breakdown.component.ts @@ -20,6 +20,7 @@ export class PriceBreakdownComponent { rows = input([]); total = input.required(); currency = input('CHF'); + totalSuffix = input(''); totalLabel = input(''); totalLabelKey = input(''); diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index d5b49f4..2ace005 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -90,7 +90,7 @@ "PROCESSING": "Verarbeitung...", "NOTES_PLACEHOLDER": "Spezifische Anweisungen...", "SETUP_NOTE": "* Beinhaltet {{cost}} als Einrichtungskosten", - "SHIPPING_NOTE": "** Versandkosten ausgeschlossen, werden im nächsten Schritt berechnet", + "SHIPPING_NOTE": "* Versandkosten ausgeschlossen, werden im nächsten Schritt berechnet", "ERROR_ZERO_PRICE": "Bei der Berechnung ist etwas schiefgelaufen. Versuche ein anderes Format oder kontaktiere uns direkt über \"Beratung anfragen\".", "ZERO_RESULT_TITLE": "Ungültiges Ergebnis", "ZERO_RESULT_HELP": "Die Berechnung hat ungültige Werte (0) geliefert. Versuche ein anderes Dateiformat oder kontaktiere uns direkt über \"Beratung anfragen\"." diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index b02eeb0..56fba30 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -90,7 +90,7 @@ "PROCESSING": "Processing...", "NOTES_PLACEHOLDER": "Specific instructions...", "SETUP_NOTE": "* Includes {{cost}} as setup cost", - "SHIPPING_NOTE": "** Shipping costs excluded, calculated at the next step", + "SHIPPING_NOTE": "* Shipping costs excluded, calculated at the next step", "ERROR_ZERO_PRICE": "Something went wrong during the calculation. Try another format or contact us directly via Request Consultation.", "ZERO_RESULT_TITLE": "Invalid Result", "ZERO_RESULT_HELP": "The calculation returned invalid zero values. Try another file format or contact us directly via Request Consultation." diff --git a/frontend/src/assets/i18n/fr.json b/frontend/src/assets/i18n/fr.json index 7e54110..ecd13b0 100644 --- a/frontend/src/assets/i18n/fr.json +++ b/frontend/src/assets/i18n/fr.json @@ -110,7 +110,7 @@ "PROCESSING": "Traitement...", "NOTES_PLACEHOLDER": "Instructions spécifiques...", "SETUP_NOTE": "* Inclut {{cost}} comme coût de setup", - "SHIPPING_NOTE": "** Frais d'expédition exclus, calculés à l'étape suivante", + "SHIPPING_NOTE": "* Frais d'expédition exclus, calculés à l'étape suivante", "STEP_WARNING": "La visualisation 3D n'est pas compatible avec les fichiers STEP et 3MF", "REMOVE_FILE": "Supprimer le fichier", "FALLBACK_MATERIAL": "PLA (fallback)", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 4e6b8f5..435a606 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -110,7 +110,7 @@ "PROCESSING": "Elaborazione...", "NOTES_PLACEHOLDER": "Istruzioni specifiche...", "SETUP_NOTE": "* Include {{cost}} come costo di setup", - "SHIPPING_NOTE": "** Costi di spedizione esclusi, calcolati al passaggio successivo", + "SHIPPING_NOTE": "* Costi di spedizione esclusi, calcolati al passaggio successivo", "STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf", "REMOVE_FILE": "Rimuovi file", "FALLBACK_MATERIAL": "PLA (fallback)",