feat(back-end and front-end): calculator improvements
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 || [];
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>('');
|
||||||
|
|
||||||
|
|||||||
@@ -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\"."
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
Reference in New Issue
Block a user