dev #29

Merged
JoeKung merged 30 commits from dev into main 2026-03-09 09:58:45 +01:00
11 changed files with 35 additions and 41 deletions
Showing only changes of commit 0c4800443f - Show all commits

View File

@@ -7,6 +7,7 @@ public class PrintSettingsDto {
// Common // Common
private String material; // e.g. "PLA", "PLA TOUGH", "PETG" private String material; // e.g. "PLA", "PLA TOUGH", "PETG"
private String color; // e.g. "White", "#FFFFFF" private String color; // e.g. "White", "#FFFFFF"
private Integer quantity;
private Long filamentVariantId; private Long filamentVariantId;
private Long printerMachineId; private Long printerMachineId;
@@ -58,6 +59,14 @@ public class PrintSettingsDto {
this.filamentVariantId = filamentVariantId; this.filamentVariantId = filamentVariantId;
} }
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Long getPrinterMachineId() { public Long getPrinterMachineId() {
return printerMachineId; return printerMachineId;
} }

View File

@@ -239,7 +239,7 @@ public class QuoteSessionItemService {
item.setQuoteSession(session); item.setQuoteSession(session);
item.setOriginalFilename(originalFilename); item.setOriginalFilename(originalFilename);
item.setStoredPath(quoteStorageService.toStoredPath(persistentPath)); item.setStoredPath(quoteStorageService.toStoredPath(persistentPath));
item.setQuantity(1); item.setQuantity(normalizeQuantity(settings.getQuantity()));
item.setColorCode(selectedVariant.getColorName()); item.setColorCode(selectedVariant.getColorName());
item.setFilamentVariant(selectedVariant); item.setFilamentVariant(selectedVariant);
item.setMaterialCode(selectedVariant.getFilamentMaterialType() != null item.setMaterialCode(selectedVariant.getFilamentMaterialType() != null
@@ -279,4 +279,11 @@ public class QuoteSessionItemService {
item.setUpdatedAt(OffsetDateTime.now()); item.setUpdatedAt(OffsetDateTime.now());
return item; return item;
} }
private int normalizeQuantity(Integer quantity) {
if (quantity == null || quantity < 1) {
return 1;
}
return quantity;
}
} }

View File

@@ -5,6 +5,7 @@
[rows]="priceBreakdownRows()" [rows]="priceBreakdownRows()"
[total]="costBreakdown().total" [total]="costBreakdown().total"
[currency]="result().currency" [currency]="result().currency"
[totalSuffix]="'*'"
[totalLabelKey]="'CHECKOUT.TOTAL'" [totalLabelKey]="'CHECKOUT.TOTAL'"
></app-price-breakdown> ></app-price-breakdown>
@@ -68,6 +69,7 @@
[ngModel]="item.quantity" [ngModel]="item.quantity"
(ngModelChange)="updateQuantity(i, $event)" (ngModelChange)="updateQuantity(i, $event)"
(blur)="flushQuantityUpdate(i)" (blur)="flushQuantityUpdate(i)"
(keydown.enter)="flushQuantityUpdate(i)"
class="qty-input" class="qty-input"
/> />
</div> </div>

View File

@@ -1,6 +1,5 @@
import { import {
Component, Component,
OnDestroy,
input, input,
output, output,
signal, signal,
@@ -34,10 +33,9 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
templateUrl: './quote-result.component.html', templateUrl: './quote-result.component.html',
styleUrl: './quote-result.component.scss', styleUrl: './quote-result.component.scss',
}) })
export class QuoteResultComponent implements OnDestroy { export class QuoteResultComponent {
readonly maxInputQuantity = 500; readonly maxInputQuantity = 500;
readonly directOrderLimit = 100; readonly directOrderLimit = 100;
readonly quantityAutoRefreshMs = 2000;
result = input.required<QuoteResult>(); result = input.required<QuoteResult>();
recalculationRequired = input<boolean>(false); recalculationRequired = input<boolean>(false);
@@ -62,13 +60,10 @@ export class QuoteResultComponent implements OnDestroy {
// Local mutable state for items to handle quantity changes // Local mutable state for items to handle quantity changes
items = signal<QuoteItem[]>([]); items = signal<QuoteItem[]>([]);
private lastSentQuantities = new Map<string, number>(); private lastSentQuantities = new Map<string, number>();
private quantityTimers = new Map<string, ReturnType<typeof setTimeout>>();
constructor() { constructor() {
effect( effect(
() => { () => {
this.clearAllQuantityTimers();
// Initialize local items when result inputs change // Initialize local items when result inputs change
// We map to new objects to avoid mutating the input directly if it was a reference // We map to new objects to avoid mutating the input directly if it was a reference
const nextItems = this.result().items.map((i) => ({ ...i })); 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) { updateQuantity(index: number, newQty: number | string) {
const normalizedQty = this.normalizeQuantity(newQty); const normalizedQty = this.normalizeQuantity(newQty);
if (normalizedQty === null) return; if (normalizedQty === null) return;
const item = this.items()[index]; const item = this.items()[index];
if (!item) return; if (!item) return;
const key = item.id ?? item.fileName;
this.items.update((current) => { this.items.update((current) => {
const updated = [...current]; const updated = [...current];
@@ -108,8 +98,6 @@ export class QuoteResultComponent implements OnDestroy {
fileName: item.fileName, fileName: item.fileName,
quantity: normalizedQty, quantity: normalizedQty,
}); });
this.scheduleQuantityRefresh(index, key);
} }
flushQuantityUpdate(index: number): void { flushQuantityUpdate(index: number): void {
@@ -117,7 +105,6 @@ export class QuoteResultComponent implements OnDestroy {
if (!item) return; if (!item) return;
const key = item.id ?? item.fileName; const key = item.id ?? item.fileName;
this.clearQuantityRefreshTimer(key);
const normalizedQty = this.normalizeQuantity(item.quantity); const normalizedQty = this.normalizeQuantity(item.quantity);
if (normalizedQty === null) return; if (normalizedQty === null) return;
@@ -213,27 +200,6 @@ export class QuoteResultComponent implements OnDestroy {
return Math.min(qty, this.maxInputQuantity); 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 { getItemDifferenceLabel(fileName: string, materialCode?: string): string {
const differences = const differences =
this.itemSettingsDiffByFileName()[fileName]?.differences || []; this.itemSettingsDiffByFileName()[fileName]?.differences || [];

View File

@@ -447,6 +447,7 @@ export class QuoteEstimatorService {
return { return {
complexityMode: request.mode === 'easy' ? 'BASIC' : 'ADVANCED', complexityMode: request.mode === 'easy' ? 'BASIC' : 'ADVANCED',
quantity: this.normalizeQuantity(item.quantity),
material: String(item.material || request.material || 'PLA'), material: String(item.material || request.material || 'PLA'),
color: item.color || '#FFFFFF', color: item.color || '#FFFFFF',
filamentVariantId: item.filamentVariantId, 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 { private normalizeQuality(value: string | undefined): string {
const normalized = String(value || 'standard') const normalized = String(value || 'standard')
.trim() .trim()

View File

@@ -20,6 +20,6 @@
{{ totalLabelKey() ? (totalLabelKey() | translate) : "" }} {{ totalLabelKey() ? (totalLabelKey() | translate) : "" }}
</ng-template> </ng-template>
</span> </span>
<span>{{ total() | currency: currency() }}</span> <span>{{ total() | currency: currency() }}{{ totalSuffix() }}</span>
</div> </div>
</div> </div>

View File

@@ -20,6 +20,7 @@ export class PriceBreakdownComponent {
rows = input<PriceBreakdownRow[]>([]); rows = input<PriceBreakdownRow[]>([]);
total = input.required<number>(); total = input.required<number>();
currency = input<string>('CHF'); currency = input<string>('CHF');
totalSuffix = input<string>('');
totalLabel = input<string>(''); totalLabel = input<string>('');
totalLabelKey = input<string>(''); totalLabelKey = input<string>('');

View File

@@ -90,7 +90,7 @@
"PROCESSING": "Verarbeitung...", "PROCESSING": "Verarbeitung...",
"NOTES_PLACEHOLDER": "Spezifische Anweisungen...", "NOTES_PLACEHOLDER": "Spezifische Anweisungen...",
"SETUP_NOTE": "* Beinhaltet {{cost}} als Einrichtungskosten", "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\".", "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_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\"." "ZERO_RESULT_HELP": "Die Berechnung hat ungültige Werte (0) geliefert. Versuche ein anderes Dateiformat oder kontaktiere uns direkt über \"Beratung anfragen\"."

View File

@@ -90,7 +90,7 @@
"PROCESSING": "Processing...", "PROCESSING": "Processing...",
"NOTES_PLACEHOLDER": "Specific instructions...", "NOTES_PLACEHOLDER": "Specific instructions...",
"SETUP_NOTE": "* Includes {{cost}} as setup cost", "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.", "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_TITLE": "Invalid Result",
"ZERO_RESULT_HELP": "The calculation returned invalid zero values. Try another file format or contact us directly via Request Consultation." "ZERO_RESULT_HELP": "The calculation returned invalid zero values. Try another file format or contact us directly via Request Consultation."

View File

@@ -110,7 +110,7 @@
"PROCESSING": "Traitement...", "PROCESSING": "Traitement...",
"NOTES_PLACEHOLDER": "Instructions spécifiques...", "NOTES_PLACEHOLDER": "Instructions spécifiques...",
"SETUP_NOTE": "* Inclut {{cost}} comme coût de setup", "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", "STEP_WARNING": "La visualisation 3D n'est pas compatible avec les fichiers STEP et 3MF",
"REMOVE_FILE": "Supprimer le fichier", "REMOVE_FILE": "Supprimer le fichier",
"FALLBACK_MATERIAL": "PLA (fallback)", "FALLBACK_MATERIAL": "PLA (fallback)",

View File

@@ -110,7 +110,7 @@
"PROCESSING": "Elaborazione...", "PROCESSING": "Elaborazione...",
"NOTES_PLACEHOLDER": "Istruzioni specifiche...", "NOTES_PLACEHOLDER": "Istruzioni specifiche...",
"SETUP_NOTE": "* Include {{cost}} come costo di setup", "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", "STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf",
"REMOVE_FILE": "Rimuovi file", "REMOVE_FILE": "Rimuovi file",
"FALLBACK_MATERIAL": "PLA (fallback)", "FALLBACK_MATERIAL": "PLA (fallback)",