dev #29
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[rows]="priceBreakdownRows()"
|
||||
[total]="costBreakdown().total"
|
||||
[currency]="result().currency"
|
||||
[totalSuffix]="'*'"
|
||||
[totalLabelKey]="'CHECKOUT.TOTAL'"
|
||||
></app-price-breakdown>
|
||||
|
||||
@@ -68,6 +69,7 @@
|
||||
[ngModel]="item.quantity"
|
||||
(ngModelChange)="updateQuantity(i, $event)"
|
||||
(blur)="flushQuantityUpdate(i)"
|
||||
(keydown.enter)="flushQuantityUpdate(i)"
|
||||
class="qty-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<QuoteResult>();
|
||||
recalculationRequired = input<boolean>(false);
|
||||
@@ -62,13 +60,10 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
// Local mutable state for items to handle quantity changes
|
||||
items = signal<QuoteItem[]>([]);
|
||||
private lastSentQuantities = new Map<string, number>();
|
||||
private quantityTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
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 || [];
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
{{ totalLabelKey() ? (totalLabelKey() | translate) : "" }}
|
||||
</ng-template>
|
||||
</span>
|
||||
<span>{{ total() | currency: currency() }}</span>
|
||||
<span>{{ total() | currency: currency() }}{{ totalSuffix() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ export class PriceBreakdownComponent {
|
||||
rows = input<PriceBreakdownRow[]>([]);
|
||||
total = input.required<number>();
|
||||
currency = input<string>('CHF');
|
||||
totalSuffix = input<string>('');
|
||||
totalLabel = input<string>('');
|
||||
totalLabelKey = input<string>('');
|
||||
|
||||
|
||||
@@ -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\"."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user