fix(back-end) calculator improvements
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 41s
Build, Test and Deploy / build-and-push (push) Successful in 40s
Build, Test and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-02-25 15:05:23 +01:00
parent 54d12f4da0
commit fecb394272
14 changed files with 290 additions and 106 deletions

View File

@@ -208,9 +208,31 @@ export class CalculatorPageComponent implements OnInit {
// 2. Update backend session if ID exists
if (event.id) {
const currentSessionId = this.result()?.sessionId;
if (!currentSessionId) return;
this.loading.set(true);
this.estimator.updateLineItem(event.id, { quantity: event.quantity }).subscribe({
next: (res) => console.log('Line item updated', res),
error: (err) => console.error('Failed to update line item', err)
next: () => {
// 3. Fetch the updated session totals from the backend
this.estimator.getQuoteSession(currentSessionId).subscribe({
next: (sessionData) => {
const newResult = this.estimator.mapSessionToQuoteResult(sessionData);
// Preserve notes
newResult.notes = this.result()?.notes;
this.result.set(newResult);
this.loading.set(false);
},
error: (err) => {
console.error('Failed to refresh session totals', err);
this.loading.set(false);
}
});
},
error: (err) => {
console.error('Failed to update line item', err);
this.loading.set(false);
}
});
}
}

View File

@@ -21,7 +21,8 @@
</div>
<div class="setup-note">
<small>{{ 'CALC.SETUP_NOTE' | translate:{cost: (result().setupCost | currency:result().currency)} }}</small>
<small>{{ 'CALC.SETUP_NOTE' | translate:{cost: (result().setupCost | currency:result().currency)} }}</small><br>
<small class="shipping-note" style="color: #666;">{{ 'CALC.SHIPPING_NOTE' | translate }}</small>
</div>
@if (result().notes) {

View File

@@ -10,11 +10,15 @@ import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl
import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component';
import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, MaterialOption, VariantOption } from '../../services/quote-estimator.service';
import { getColorHex } from '../../../../core/constants/colors.const';
import * as THREE from 'three';
// @ts-ignore
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
interface FormItem {
file: File;
quantity: number;
color: string;
dimensions?: {x: number, y: number, z: number};
}
@Component({
@@ -69,6 +73,35 @@ export class UploadFormComponent implements OnInit {
return name.endsWith('.stl');
}
private async getStlDimensions(file: File): Promise<{x: number, y: number, z: number} | null> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const loader = new STLLoader();
const geometry = loader.parse(e.target?.result as ArrayBuffer);
geometry.computeBoundingBox();
if (geometry.boundingBox) {
const size = new THREE.Vector3();
geometry.boundingBox.getSize(size);
resolve({
x: Math.round(size.x * 10) / 10,
y: Math.round(size.y * 10) / 10,
z: Math.round(size.z * 10) / 10
});
return;
}
resolve(null);
} catch (err) {
console.error("Error parsing STL for dimensions:", err);
resolve(null);
}
};
reader.onerror = () => resolve(null);
reader.readAsArrayBuffer(file);
});
}
constructor() {
this.form = this.fb.group({
itemsTouched: [false], // Hack to track touched state for custom items list
@@ -77,7 +110,7 @@ export class UploadFormComponent implements OnInit {
items: [[]], // Track items in form for validation if needed
notes: [''],
// Advanced fields
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
infillDensity: [15, [Validators.min(0), Validators.max(100)]],
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
nozzleDiameter: [0.4, Validators.required],
infillPattern: ['grid'],
@@ -136,7 +169,7 @@ export class UploadFormComponent implements OnInit {
}
}
onFilesDropped(newFiles: File[]) {
async onFilesDropped(newFiles: File[]) {
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
const validItems: FormItem[] = [];
let hasError = false;
@@ -145,8 +178,13 @@ export class UploadFormComponent implements OnInit {
if (file.size > MAX_SIZE) {
hasError = true;
} else {
let dimensions = undefined;
if (file.name.toLowerCase().endsWith('.stl')) {
const dims = await this.getStlDimensions(file);
if (dims) dimensions = dims;
}
// Default color is Black
validItems.push({ file, quantity: 1, color: 'Black' });
validItems.push({ file, quantity: 1, color: 'Black', dimensions });
}
}
@@ -238,11 +276,16 @@ export class UploadFormComponent implements OnInit {
});
}
setFiles(files: File[]) {
async setFiles(files: File[]) {
const validItems: FormItem[] = [];
for (const file of files) {
let dimensions = undefined;
if (file.name.toLowerCase().endsWith('.stl')) {
const dims = await this.getStlDimensions(file);
if (dims) dimensions = dims;
}
// Default color is Black or derive from somewhere if possible, but here we just init
validItems.push({ file, quantity: 1, color: 'Black' });
validItems.push({ file, quantity: 1, color: 'Black', dimensions });
}
if (validItems.length > 0) {

View File

@@ -5,7 +5,7 @@ import { map, catchError, tap } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
export interface QuoteRequest {
items: { file: File, quantity: number, color?: string }[];
items: { file: File, quantity: number, color?: string, dimensions?: {x: number, y: number, z: number} }[];
material: string;
quality: string;
notes?: string;
@@ -32,6 +32,7 @@ export interface QuoteResult {
sessionId?: string;
items: QuoteItem[];
setupCost: number;
globalMachineCost: number;
currency: string;
totalPrice: number;
totalTimeHours: number;
@@ -219,6 +220,9 @@ export class QuoteEstimatorService {
quality: request.quality,
supportsEnabled: request.supportEnabled,
color: item.color || '#FFFFFF',
boundingBoxX: item.dimensions?.x,
boundingBoxY: item.dimensions?.y,
boundingBoxZ: item.dimensions?.z,
layerHeight: request.mode === 'advanced' ? request.layerHeight : null,
infillDensity: request.mode === 'advanced' ? request.infillDensity : null,
infillPattern: request.mode === 'advanced' ? request.infillPattern : null,
@@ -260,59 +264,19 @@ export class QuoteEstimatorService {
});
const finalize = (responses: any[], setupCost: number, sessionId: string) => {
observer.next(100);
const items: QuoteItem[] = [];
let grandTotal = 0;
let totalTime = 0;
let totalWeight = 0;
let validCount = 0;
responses.forEach((res, idx) => {
if (!res || !res.success) return;
validCount++;
const unitPrice = res.unitPriceChf || 0;
const quantity = res.originalQty || 1;
items.push({
id: res.id,
fileName: res.fileName,
unitPrice: unitPrice,
unitTime: res.printTimeSeconds || 0,
unitWeight: res.materialGrams || 0,
quantity: quantity,
material: request.material,
color: res.originalItem.color || 'Default'
// Store ID if needed for updates? QuoteItem interface might need update
// or we map it in component
});
grandTotal += unitPrice * quantity;
totalTime += (res.printTimeSeconds || 0) * quantity;
totalWeight += (res.materialGrams || 0) * quantity;
this.http.get<any>(`${environment.apiUrl}/api/quote-sessions/${sessionId}`, { headers }).subscribe({
next: (sessionData) => {
observer.next(100);
const result = this.mapSessionToQuoteResult(sessionData);
result.notes = request.notes;
observer.next(result);
observer.complete();
},
error: (err) => {
console.error('Failed to fetch final session calculation', err);
observer.error('Failed to calculate final quote');
}
});
if (validCount === 0) {
observer.error('All calculations failed.');
return;
}
grandTotal += setupCost;
const result: QuoteResult = {
sessionId: sessionId,
items,
setupCost: setupCost,
currency: 'CHF',
totalPrice: Math.round(grandTotal * 100) / 100,
totalTimeHours: Math.floor(totalTime / 3600),
totalTimeMinutes: Math.ceil((totalTime % 3600) / 60),
totalWeight: Math.ceil(totalWeight),
notes: request.notes
};
observer.next(result);
observer.complete();
};
});
}
@@ -361,10 +325,11 @@ export class QuoteEstimatorService {
// But line items might have different colors.
color: item.colorCode
})),
setupCost: session.setupCostChf,
currency: 'CHF', // Fixed for now
totalPrice: sessionData.grandTotalChf,
totalTimeHours: Math.floor(totalTime / 3600),
setupCost: session.setupCostChf || 0,
globalMachineCost: sessionData.globalMachineCostChf || 0,
currency: 'CHF', // Fixed for now
totalPrice: (sessionData.itemsTotalChf || 0) + (session.setupCostChf || 0) + (sessionData.shippingCostChf || 0),
totalTimeHours: Math.floor(totalTime / 3600),
totalTimeMinutes: Math.ceil((totalTime % 3600) / 60),
totalWeight: Math.ceil(totalWeight),
notes: session.notes

View File

@@ -143,11 +143,11 @@
</div>
<div class="total-row">
<span>{{ 'CHECKOUT.SHIPPING' | translate }}</span>
<span>{{ 9.00 | currency:'CHF' }}</span>
<span>{{ session.shippingCostChf | currency:'CHF' }}</span>
</div>
<div class="grand-total">
<span>{{ 'CHECKOUT.TOTAL' | translate }}</span>
<span>{{ (session.grandTotalChf + 9.00) | currency:'CHF' }}</span>
<span>{{ session.grandTotalChf | currency:'CHF' }}</span>
</div>
</div>
</app-card>

View File

@@ -57,6 +57,7 @@
"CALCULATE": "Calculate Quote",
"RESULT": "Estimated Quote",
"TIME": "Print Time",
"MACHINE_COST": "Machine Cost",
"COST": "Total Cost",
"ORDER": "Order Now",
"CONSULT": "Request Consultation",
@@ -178,6 +179,8 @@
"PROCESSING": "Processing...",
"SUMMARY_TITLE": "Order Summary",
"SUBTOTAL": "Subtotal",
"ITEMS_BASE_SUBTOTAL": "Items Subtotal",
"MACHINE_COST": "Machine Cost",
"SETUP_FEE": "Setup Fee",
"TOTAL": "Total",
"QTY": "Qty",

View File

@@ -81,6 +81,7 @@
"CALCULATE": "Calcola Preventivo",
"RESULT": "Preventivo Stimato",
"TIME": "Tempo Stampa",
"MACHINE_COST": "Costo Macchina",
"COST": "Costo Totale",
"ORDER": "Ordina Ora",
"CONSULT": "Richiedi Consulenza",
@@ -102,6 +103,7 @@
"PROCESSING": "Elaborazione...",
"NOTES_PLACEHOLDER": "Istruzioni specifiche...",
"SETUP_NOTE": "* Include {{cost}} Costo di Setup",
"SHIPPING_NOTE": "** costi di spedizione esclusi calcolati al prossimo passaggio",
"STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf"
},
"QUOTE": {
@@ -167,7 +169,7 @@
},
"LOCATIONS": {
"TITLE": "Le Nostre Sedi",
"SUBTITLE": "Siamo presenti in due sedi per coprire meglio il territorio. Seleziona la sede per vedere i dettagli.",
"SUBTITLE": "Siamo presenti in due sedi. Seleziona la sede per vedere i dettagli.",
"TICINO": "Ticino",
"BIENNE": "Bienne",
"ADDRESS_TICINO": "Via G. Pioda 29a, 6710 Biasca (TI)",
@@ -244,19 +246,23 @@
"PROCESSING": "Elaborazione...",
"SUMMARY_TITLE": "Riepilogo Ordine",
"SUBTOTAL": "Subtotale",
"ITEMS_BASE_SUBTOTAL": "Costo Base Articoli",
"MACHINE_COST": "Costo Macchina",
"SETUP_FEE": "Costo di Avvio",
"TOTAL": "Totale",
"QTY": "Qtà",
"SHIPPING": "Spedizione",
"SHIPPING": "Spedizione (CH)",
"INVALID_EMAIL": "Email non valida",
"COMPANY_OPTIONAL": "Nome Azienda (Opzionale)",
"REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)"
"REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)",
"SHIPPING_CALCULATED_NEXT_STEP": "il costo di spedizione viene calcolato al prossimo passaggio",
"EXCLUDES_SHIPPING": "Escluso costo di spedizione"
},
"PAYMENT": {
"TITLE": "Pagamento",
"METHOD": "Metodo di Pagamento",
"TWINT_TITLE": "Paga con TWINT",
"TWINT_DESC": "Inquadra il codice con l'app TWINT",
"TWINT_DESC": "Inquadra il codice con l'app TWINT, da mobile clicca il bottone.",
"TWINT_OPEN": "Apri direttamente in TWINT",
"TWINT_LINK": "Apri link di pagamento",
"BANK_TITLE": "Bonifico Bancario",