From 875c6ffd2d30dc55b32b7fb4e7a9c10bd413d4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 16 Feb 2026 16:09:36 +0100 Subject: [PATCH] fix(back-end): fix process and new feature --- .../controller/QuoteController.java | 33 +++- .../controller/QuoteSessionController.java | 49 +++-- .../exception/GlobalExceptionHandler.java | 25 +++ .../exception/ModelTooLargeException.java | 45 +++++ .../com/printcalculator/model/StlBounds.java | 16 ++ .../service/SlicerService.java | 14 +- .../printcalculator/service/StlService.java | 134 ++++++++++++++ .../calculator/calculator-page.component.html | 12 +- .../calculator/calculator-page.component.ts | 30 ++-- .../quote-result/quote-result.component.html | 8 + .../quote-result/quote-result.component.ts | 8 +- .../services/quote-estimator.service.ts | 170 +++++++++--------- 12 files changed, 421 insertions(+), 123 deletions(-) create mode 100644 backend/src/main/java/com/printcalculator/exception/ModelTooLargeException.java create mode 100644 backend/src/main/java/com/printcalculator/model/StlBounds.java create mode 100644 backend/src/main/java/com/printcalculator/service/StlService.java diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteController.java b/backend/src/main/java/com/printcalculator/controller/QuoteController.java index 0947e61..f57f250 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteController.java @@ -1,11 +1,14 @@ package com.printcalculator.controller; import com.printcalculator.entity.PrinterMachine; +import com.printcalculator.exception.ModelTooLargeException; import com.printcalculator.model.PrintStats; import com.printcalculator.model.QuoteResult; +import com.printcalculator.model.StlBounds; import com.printcalculator.repository.PrinterMachineRepository; import com.printcalculator.service.QuoteCalculator; import com.printcalculator.service.SlicerService; +import com.printcalculator.service.StlService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -20,6 +23,7 @@ import java.nio.file.Path; public class QuoteController { private final SlicerService slicerService; + private final StlService stlService; private final QuoteCalculator quoteCalculator; private final PrinterMachineRepository machineRepo; @@ -27,8 +31,9 @@ public class QuoteController { private static final String DEFAULT_FILAMENT = "pla_basic"; private static final String DEFAULT_PROCESS = "standard"; - public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) { + public QuoteController(SlicerService slicerService, StlService stlService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) { this.slicerService = slicerService; + this.stlService = stlService; this.quoteCalculator = quoteCalculator; this.machineRepo = machineRepo; } @@ -117,18 +122,36 @@ public class QuoteController { slicerMachineProfile = "bambu_a1"; } + // Validate model size against machine volume + validateModelSize(tempInput.toFile(), machine); + PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides); // Calculate Quote (Pass machine display name for pricing lookup) QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament); return ResponseEntity.ok(result); - - } catch (Exception e) { - e.printStackTrace(); - return ResponseEntity.internalServerError().build(); } finally { Files.deleteIfExists(tempInput); } } + + private void validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException { + StlBounds bounds = stlService.readBounds(stlFile); + double x = bounds.sizeX(); + double y = bounds.sizeY(); + double z = bounds.sizeZ(); + + int bx = machine.getBuildVolumeXMm(); + int by = machine.getBuildVolumeYMm(); + int bz = machine.getBuildVolumeZMm(); + + double eps = 0.01; + boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps) + || (y <= bx + eps && x <= by + eps && z <= bz + eps); + + if (!fits) { + throw new ModelTooLargeException(x, y, z, bx, by, bz); + } + } } diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java index c67e2ec..78b3e8c 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -3,13 +3,16 @@ package com.printcalculator.controller; import com.printcalculator.entity.PrinterMachine; import com.printcalculator.entity.QuoteLineItem; import com.printcalculator.entity.QuoteSession; +import com.printcalculator.exception.ModelTooLargeException; import com.printcalculator.model.PrintStats; import com.printcalculator.model.QuoteResult; +import com.printcalculator.model.StlBounds; import com.printcalculator.repository.PrinterMachineRepository; import com.printcalculator.repository.QuoteLineItemRepository; import com.printcalculator.repository.QuoteSessionRepository; import com.printcalculator.service.QuoteCalculator; import com.printcalculator.service.SlicerService; +import com.printcalculator.service.StlService; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -37,6 +40,7 @@ public class QuoteSessionController { private final QuoteSessionRepository sessionRepo; private final QuoteLineItemRepository lineItemRepo; private final SlicerService slicerService; + private final StlService stlService; private final QuoteCalculator quoteCalculator; private final PrinterMachineRepository machineRepo; private final com.printcalculator.repository.PricingPolicyRepository pricingRepo; @@ -49,6 +53,7 @@ public class QuoteSessionController { public QuoteSessionController(QuoteSessionRepository sessionRepo, QuoteLineItemRepository lineItemRepo, SlicerService slicerService, + StlService stlService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo, com.printcalculator.repository.PricingPolicyRepository pricingRepo, @@ -56,6 +61,7 @@ public class QuoteSessionController { this.sessionRepo = sessionRepo; this.lineItemRepo = lineItemRepo; this.slicerService = slicerService; + this.stlService = stlService; this.quoteCalculator = quoteCalculator; this.machineRepo = machineRepo; this.pricingRepo = pricingRepo; @@ -127,8 +133,11 @@ public class QuoteSessionController { // 1. Pick Machine (default to first active or specific) PrinterMachine machine = machineRepo.findFirstByIsActiveTrue() .orElseThrow(() -> new RuntimeException("No active printer found")); + + // 2. Validate model size against machine volume + StlBounds bounds = validateModelSize(persistentPath.toFile(), machine); - // 2. Pick Profiles + // 3. Pick Profiles String machineProfile = machine.getSlicerMachineProfile(); if (machineProfile == null || machineProfile.isBlank()) { machineProfile = machine.getPrinterDisplayName(); // e.g. "Bambu Lab A1 0.4 nozzle" @@ -194,7 +203,7 @@ public class QuoteSessionController { machineOverrides.put("nozzle_diameter", String.valueOf(settings.getNozzleDiameter())); } - // 3. Slice (Use persistent path) + // 4. Slice (Use persistent path) PrintStats stats = slicerService.slice( persistentPath.toFile(), machineProfile, @@ -204,10 +213,10 @@ public class QuoteSessionController { processOverrides ); - // 4. Calculate Quote + // 5. Calculate Quote QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile); - // 5. Create Line Item + // 6. Create Line Item QuoteLineItem item = new QuoteLineItem(); item.setQuoteSession(session); item.setOriginalFilename(file.getOriginalFilename()); @@ -227,14 +236,10 @@ public class QuoteSessionController { breakdown.put("setup_fee", result.getSetupCost()); item.setPricingBreakdown(breakdown); - // Dimensions - // Cannot get bb from GCodeParser yet? - // If GCodeParser doesn't return size, we might defaults or 0. - // Stats has filament used. - // Let's set dummy for now or upgrade parser later. - item.setBoundingBoxXMm(BigDecimal.ZERO); - item.setBoundingBoxYMm(BigDecimal.ZERO); - item.setBoundingBoxZMm(BigDecimal.ZERO); + // Dimensions from STL + item.setBoundingBoxXMm(BigDecimal.valueOf(bounds.sizeX())); + item.setBoundingBoxYMm(BigDecimal.valueOf(bounds.sizeY())); + item.setBoundingBoxZMm(BigDecimal.valueOf(bounds.sizeZ())); item.setCreatedAt(OffsetDateTime.now()); item.setUpdatedAt(OffsetDateTime.now()); @@ -250,6 +255,26 @@ public class QuoteSessionController { } } + private StlBounds validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException { + StlBounds bounds = stlService.readBounds(stlFile); + double x = bounds.sizeX(); + double y = bounds.sizeY(); + double z = bounds.sizeZ(); + + int bx = machine.getBuildVolumeXMm(); + int by = machine.getBuildVolumeYMm(); + int bz = machine.getBuildVolumeZMm(); + + double eps = 0.01; + boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps) + || (y <= bx + eps && x <= by + eps && z <= bz + eps); + + if (!fits) { + throw new ModelTooLargeException(x, y, z, bx, by, bz); + } + return bounds; + } + private void applyPrintSettings(com.printcalculator.dto.PrintSettingsDto settings) { if ("BASIC".equalsIgnoreCase(settings.getComplexityMode())) { // Set defaults based on Quality diff --git a/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java index 3b5db96..7df1150 100644 --- a/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java @@ -9,6 +9,8 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException; import java.util.HashMap; import java.util.Map; +import java.math.BigDecimal; +import java.math.RoundingMode; @ControllerAdvice @Slf4j @@ -43,4 +45,27 @@ public class GlobalExceptionHandler { response.put("message", "The uploaded file exceeds the maximum allowed size."); return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response); } + + @ExceptionHandler(ModelTooLargeException.class) + public ResponseEntity handleModelTooLarge(ModelTooLargeException exc) { + Map response = new HashMap<>(); + response.put("error", "Model too large"); + response.put("code", "MODEL_TOO_LARGE"); + response.put("message", String.format( + "Model size %.2fx%.2fx%.2f mm exceeds build volume %dx%dx%d mm.", + exc.getModelX(), exc.getModelY(), exc.getModelZ(), + exc.getBuildX(), exc.getBuildY(), exc.getBuildZ() + )); + response.put("model_x_mm", formatMm(exc.getModelX())); + response.put("model_y_mm", formatMm(exc.getModelY())); + response.put("model_z_mm", formatMm(exc.getModelZ())); + response.put("build_x_mm", String.valueOf(exc.getBuildX())); + response.put("build_y_mm", String.valueOf(exc.getBuildY())); + response.put("build_z_mm", String.valueOf(exc.getBuildZ())); + return ResponseEntity.unprocessableEntity().body(response); + } + + private String formatMm(double value) { + return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).toPlainString(); + } } diff --git a/backend/src/main/java/com/printcalculator/exception/ModelTooLargeException.java b/backend/src/main/java/com/printcalculator/exception/ModelTooLargeException.java new file mode 100644 index 0000000..b381dba --- /dev/null +++ b/backend/src/main/java/com/printcalculator/exception/ModelTooLargeException.java @@ -0,0 +1,45 @@ +package com.printcalculator.exception; + +public class ModelTooLargeException extends RuntimeException { + private final double modelX; + private final double modelY; + private final double modelZ; + private final int buildX; + private final int buildY; + private final int buildZ; + + public ModelTooLargeException(double modelX, double modelY, double modelZ, + int buildX, int buildY, int buildZ) { + super("Model size exceeds build volume"); + this.modelX = modelX; + this.modelY = modelY; + this.modelZ = modelZ; + this.buildX = buildX; + this.buildY = buildY; + this.buildZ = buildZ; + } + + public double getModelX() { + return modelX; + } + + public double getModelY() { + return modelY; + } + + public double getModelZ() { + return modelZ; + } + + public int getBuildX() { + return buildX; + } + + public int getBuildY() { + return buildY; + } + + public int getBuildZ() { + return buildZ; + } +} diff --git a/backend/src/main/java/com/printcalculator/model/StlBounds.java b/backend/src/main/java/com/printcalculator/model/StlBounds.java new file mode 100644 index 0000000..57387fc --- /dev/null +++ b/backend/src/main/java/com/printcalculator/model/StlBounds.java @@ -0,0 +1,16 @@ +package com.printcalculator.model; + +public record StlBounds(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ) { + public double sizeX() { + return maxX - minX; + } + + public double sizeY() { + return maxY - minY; + } + + public double sizeZ() { + return maxZ - minZ; + } +} diff --git a/backend/src/main/java/com/printcalculator/service/SlicerService.java b/backend/src/main/java/com/printcalculator/service/SlicerService.java index 3990cfb..399ad29 100644 --- a/backend/src/main/java/com/printcalculator/service/SlicerService.java +++ b/backend/src/main/java/com/printcalculator/service/SlicerService.java @@ -50,17 +50,9 @@ public class SlicerService { if (machineOverrides != null) machineOverrides.forEach(machineProfile::put); if (processOverrides != null) processOverrides.forEach(processProfile::put); - // MANTENIAMO L'IDENTITÀ BAMBU LAB A1 - // Ma puliamo solo i riferimenti a mesh esterne che causano il crash grafico (Unable to create exclude triangles) - machineProfile.put("printer_model", "Bambu Lab A1"); - - // Impostiamo aree di esclusione vuote esplicitamente per evitare che lo slicer tenti di caricarle dai suoi interni - machineProfile.putArray("bed_exclude_area"); - machineProfile.putArray("head_wrap_detect_zone"); - - // Rimuoviamo i modelli 3D del piatto che richiedono caricamento di mesh STL/OBJ interne - machineProfile.remove("bed_custom_model"); - machineProfile.remove("bed_custom_texture"); + // Evitiamo solo asset grafici opzionali che potrebbero richiedere file esterni + if (machineProfile.has("bed_custom_model")) machineProfile.put("bed_custom_model", ""); + if (machineProfile.has("bed_custom_texture")) machineProfile.put("bed_custom_texture", ""); machineProfile.remove("thumbnail"); Path baseTempPath = Paths.get("/app/temp"); diff --git a/backend/src/main/java/com/printcalculator/service/StlService.java b/backend/src/main/java/com/printcalculator/service/StlService.java new file mode 100644 index 0000000..db39326 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/StlService.java @@ -0,0 +1,134 @@ +package com.printcalculator.service; + +import com.printcalculator.model.StlBounds; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +@Service +public class StlService { + + public StlBounds readBounds(File stlFile) throws IOException { + long size = stlFile.length(); + if (size >= 84 && isBinaryStl(stlFile, size)) { + return readBinaryBounds(stlFile); + } + return readAsciiBounds(stlFile); + } + + private boolean isBinaryStl(File stlFile, long size) throws IOException { + try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) { + raf.seek(80); + long triangleCount = readLEUInt32(raf); + long expected = 84L + triangleCount * 50L; + return expected == size; + } + } + + private StlBounds readBinaryBounds(File stlFile) throws IOException { + try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) { + raf.seek(80); + long triangleCount = readLEUInt32(raf); + raf.seek(84); + + BoundsAccumulator acc = new BoundsAccumulator(); + for (long i = 0; i < triangleCount; i++) { + // skip normal + readLEFloat(raf); + readLEFloat(raf); + readLEFloat(raf); + // 3 vertices + acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf)); + acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf)); + acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf)); + // skip attribute byte count + raf.skipBytes(2); + } + return acc.toBounds(); + } + } + + private StlBounds readAsciiBounds(File stlFile) throws IOException { + BoundsAccumulator acc = new BoundsAccumulator(); + try (BufferedReader reader = Files.newBufferedReader(stlFile.toPath(), StandardCharsets.US_ASCII)) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.startsWith("vertex")) continue; + String[] parts = line.split("\\s+"); + if (parts.length < 4) continue; + double x = Double.parseDouble(parts[1]); + double y = Double.parseDouble(parts[2]); + double z = Double.parseDouble(parts[3]); + acc.accept(x, y, z); + } + } + return acc.toBounds(); + } + + private long readLEUInt32(RandomAccessFile raf) throws IOException { + int b1 = raf.read(); + int b2 = raf.read(); + int b3 = raf.read(); + int b4 = raf.read(); + if ((b1 | b2 | b3 | b4) < 0) throw new IOException("Unexpected EOF while reading STL"); + return ((long) b1 & 0xFF) + | (((long) b2 & 0xFF) << 8) + | (((long) b3 & 0xFF) << 16) + | (((long) b4 & 0xFF) << 24); + } + + private int readLEInt(RandomAccessFile raf) throws IOException { + int b1 = raf.read(); + int b2 = raf.read(); + int b3 = raf.read(); + int b4 = raf.read(); + if ((b1 | b2 | b3 | b4) < 0) throw new IOException("Unexpected EOF while reading STL"); + return (b1 & 0xFF) + | ((b2 & 0xFF) << 8) + | ((b3 & 0xFF) << 16) + | ((b4 & 0xFF) << 24); + } + + private float readLEFloat(RandomAccessFile raf) throws IOException { + return Float.intBitsToFloat(readLEInt(raf)); + } + + private static class BoundsAccumulator { + private boolean hasPoint = false; + private double minX; + private double minY; + private double minZ; + private double maxX; + private double maxY; + private double maxZ; + + void accept(double x, double y, double z) { + if (!hasPoint) { + minX = maxX = x; + minY = maxY = y; + minZ = maxZ = z; + hasPoint = true; + return; + } + if (x < minX) minX = x; + if (y < minY) minY = y; + if (z < minZ) minZ = z; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + } + + StlBounds toBounds() throws IOException { + if (!hasPoint) { + throw new IOException("STL appears to contain no vertices"); + } + return new StlBounds(minX, minY, minZ, maxX, maxY, maxZ); + } + } +} diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index 57fd25e..20580fc 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -45,7 +45,8 @@
- @if (loading()) { + @if (loading() && !result()) { +
@@ -54,6 +55,15 @@
} @else if (result()) { + + @if (loading()) { + +
+
+ Analisi in corso... ({{ uploadProgress() }}%) +
+ } + { + this.loading.set(false); + this.uploadProgress.set(100); + }, error: (err) => { if (typeof err === 'string') { this.error.set(err); 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 5a660a4..c87f40e 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 @@ -41,6 +41,10 @@ {{ item.fileName }} @if (item.error) { {{ 'CALC.ERROR_' + item.error | translate }} + } @else if (item.status === 'pending') { + +
Analisi... +
} @else { @@ -63,6 +67,10 @@
{{ (item.unitPrice * item.quantity) | currency:result().currency }}
+ } @else if (item.status === 'pending') { +
+
+
} @else {
-
} 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 e507e43..b891cfb 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 @@ -61,9 +61,11 @@ export class QuoteResultComponent { let weight = 0; currentItems.forEach(i => { - price += i.unitPrice * i.quantity; - time += i.unitTime * i.quantity; - weight += i.unitWeight * i.quantity; + if (i.status === 'done' && !i.error) { + price += i.unitPrice * i.quantity; + time += i.unitTime * i.quantity; + weight += i.unitWeight * i.quantity; + } }); const hours = Math.floor(time / 3600); 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 e8cfa4b..e78c709 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -27,6 +27,7 @@ export interface QuoteItem { material?: string; color?: string; error?: string; + status: 'pending' | 'done' | 'error'; } export interface QuoteResult { @@ -188,18 +189,74 @@ export class QuoteEstimatorService { const sessionId = sessionRes.id; const sessionSetupCost = sessionRes.setupCostChf || 0; + // Initialize items in pending state + const currentItems: QuoteItem[] = request.items.map(item => ({ + fileName: item.file.name, + unitPrice: 0, + unitTime: 0, + unitWeight: 0, + quantity: item.quantity, + status: 'pending', + color: item.color || 'White' // Default color for UI + })); + + // Emit initial state + const initialResult: QuoteResult = { + sessionId: sessionId, + items: [...currentItems], + setupCost: sessionSetupCost, + currency: 'CHF', + totalPrice: 0, // Will be calculated dynamically + totalTimeHours: 0, + totalTimeMinutes: 0, + totalWeight: 0, + notes: request.notes + }; + observer.next(initialResult); + // 2. Upload files to this session const totalItems = request.items.length; const allProgress: number[] = new Array(totalItems).fill(0); - const finalResponses: any[] = []; let completedRequests = 0; - const checkCompletion = () => { + const emitUpdate = () => { const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems); observer.next(avg); + // Helper to calculate totals for current items + let grandTotal = 0; + let totalTime = 0; + let totalWeight = 0; + let validCount = 0; + + currentItems.forEach(item => { + if (item.status === 'done') { + grandTotal += item.unitPrice * item.quantity; + totalTime += item.unitTime * item.quantity; + totalWeight += item.unitWeight * item.quantity; + validCount++; + } + }); + + if (validCount > 0) { + grandTotal += sessionSetupCost; + } + + const result: QuoteResult = { + sessionId: sessionId, + items: [...currentItems], // Create copy to trigger change detection + setupCost: sessionSetupCost, + 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); + if (completedRequests === totalItems) { - finalize(finalResponses, sessionSetupCost, sessionId); + observer.complete(); } }; @@ -229,21 +286,42 @@ export class QuoteEstimatorService { }).subscribe({ next: (event) => { if (event.type === HttpEventType.UploadProgress && event.total) { - allProgress[index] = Math.round((100 * event.loaded) / event.total); - checkCompletion(); + allProgress[index] = Math.round((70 * event.loaded) / event.total); // Upload is 70% of "progress" for user perception + emitUpdate(); } else if (event.type === HttpEventType.Response) { allProgress[index] = 100; - finalResponses[index] = { ...event.body, success: true, fileName: item.file.name, originalQty: item.quantity, originalItem: item }; + const resBody = event.body as any; + + // Update item in list + currentItems[index] = { + id: resBody.id, + fileName: resBody.originalFilename, // use returned filename + unitPrice: resBody.unitPriceChf || 0, + unitTime: resBody.printTimeSeconds || 0, + unitWeight: resBody.materialGrams || 0, + quantity: item.quantity, // Keep original quantity + material: request.material, + color: item.color || 'White', + status: 'done' + }; + completedRequests++; - checkCompletion(); + emitUpdate(); } }, error: (err) => { console.error('Item upload failed', err); const errorMsg = err.error?.code === 'VIRUS_DETECTED' ? 'VIRUS_DETECTED' : 'UPLOAD_FAILED'; - finalResponses[index] = { success: false, fileName: item.file.name, error: errorMsg }; + + currentItems[index] = { + ...currentItems[index], + status: 'error', + error: errorMsg + }; + + allProgress[index] = 100; // Mark as done despite error completedRequests++; - checkCompletion(); + emitUpdate(); } }); }); @@ -253,77 +331,6 @@ export class QuoteEstimatorService { observer.error('Could not initialize quote session'); } }); - - 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) => { - const quantity = res?.originalQty || request.items[idx].quantity || 1; - - if (!res || !res.success) { - items.push({ - fileName: request.items[idx].file.name, - unitPrice: 0, - unitTime: 0, - unitWeight: 0, - quantity: quantity, - error: res?.error || 'UPLOAD_FAILED' - }); - return; - } - - validCount++; - const unitPrice = res.unitPriceChf || 0; - - 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' - }); - - grandTotal += unitPrice * quantity; - totalTime += (res.printTimeSeconds || 0) * quantity; - totalWeight += (res.materialGrams || 0) * quantity; - }); - - if (validCount === 0) { - // Check if any failed due to virus - const virusError = responses.find(r => r.error === 'VIRUS_DETECTED'); - if (virusError) { - observer.error('VIRUS_DETECTED'); - } else { - 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(); - }; }); } @@ -377,7 +384,8 @@ export class QuoteEstimatorService { material: session.materialCode, // Assumption: session has one material for all? or items have it? // Backend model QuoteSession has materialCode. // But line items might have different colors. - color: item.colorCode + color: item.colorCode, + status: 'done' })), setupCost: session.setupCostChf, currency: 'CHF', // Fixed for now