feat(back-end):improvement
This commit is contained in:
@@ -26,13 +26,14 @@ public class CustomQuoteRequestController {
|
|||||||
private final CustomQuoteRequestRepository requestRepo;
|
private final CustomQuoteRequestRepository requestRepo;
|
||||||
private final CustomQuoteRequestAttachmentRepository attachmentRepo;
|
private final CustomQuoteRequestAttachmentRepository attachmentRepo;
|
||||||
|
|
||||||
// TODO: Inject Storage Service
|
private final com.printcalculator.service.StorageService storageService;
|
||||||
private static final String STORAGE_ROOT = "storage_requests";
|
|
||||||
|
|
||||||
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
|
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
|
||||||
CustomQuoteRequestAttachmentRepository attachmentRepo) {
|
CustomQuoteRequestAttachmentRepository attachmentRepo,
|
||||||
|
com.printcalculator.service.StorageService storageService) {
|
||||||
this.requestRepo = requestRepo;
|
this.requestRepo = requestRepo;
|
||||||
this.attachmentRepo = attachmentRepo;
|
this.attachmentRepo = attachmentRepo;
|
||||||
|
this.storageService = storageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create Custom Quote Request
|
// 1. Create Custom Quote Request
|
||||||
@@ -91,10 +92,8 @@ public class CustomQuoteRequestController {
|
|||||||
attachment.setStoredRelativePath(relativePath);
|
attachment.setStoredRelativePath(relativePath);
|
||||||
attachmentRepo.save(attachment);
|
attachmentRepo.save(attachment);
|
||||||
|
|
||||||
// Save file to disk
|
// Save file to disk via StorageService
|
||||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
storageService.store(file, Paths.get(relativePath));
|
||||||
Files.createDirectories(absolutePath.getParent());
|
|
||||||
Files.copy(file.getInputStream(), absolutePath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ public class OrderController {
|
|||||||
private final CustomerRepository customerRepo;
|
private final CustomerRepository customerRepo;
|
||||||
private final com.printcalculator.service.StorageService storageService;
|
private final com.printcalculator.service.StorageService storageService;
|
||||||
|
|
||||||
// TODO: Inject Storage Service or use a base path property
|
|
||||||
// private static final String STORAGE_ROOT = "storage_orders";
|
|
||||||
|
|
||||||
public OrderController(OrderRepository orderRepo,
|
public OrderController(OrderRepository orderRepo,
|
||||||
OrderItemRepository orderItemRepo,
|
OrderItemRepository orderItemRepo,
|
||||||
@@ -207,7 +205,11 @@ public class OrderController {
|
|||||||
order.setSetupCostChf(session.getSetupCostChf());
|
order.setSetupCostChf(session.getSetupCostChf());
|
||||||
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0?
|
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0?
|
||||||
order.setDiscountChf(BigDecimal.ZERO);
|
order.setDiscountChf(BigDecimal.ZERO);
|
||||||
// TODO: Calc implementation for shipping
|
// Calculate Shipping (Basic implementation: Flat rate 9.00 if not pickup)
|
||||||
|
// Future: Check delivery method from request if available
|
||||||
|
if (order.getShippingCostChf() == null) {
|
||||||
|
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||||
|
}
|
||||||
|
|
||||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||||
order.setTotalChf(total);
|
order.setTotalChf(total);
|
||||||
|
|||||||
@@ -108,7 +108,11 @@ public class QuoteController {
|
|||||||
try {
|
try {
|
||||||
file.transferTo(tempInput.toFile());
|
file.transferTo(tempInput.toFile());
|
||||||
|
|
||||||
String slicerMachineProfile = "bambu_a1"; // TODO: Add to PrinterMachine entity
|
// Use profile from machine or fallback
|
||||||
|
String slicerMachineProfile = machine.getSlicerMachineProfile();
|
||||||
|
if (slicerMachineProfile == null || slicerMachineProfile.isEmpty()) {
|
||||||
|
slicerMachineProfile = "bambu_a1";
|
||||||
|
}
|
||||||
|
|
||||||
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ public class PrinterMachine {
|
|||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private OffsetDateTime createdAt;
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "slicer_machine_profile")
|
||||||
|
private String slicerMachineProfile;
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -57,6 +60,14 @@ public class PrinterMachine {
|
|||||||
this.printerDisplayName = printerDisplayName;
|
this.printerDisplayName = printerDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSlicerMachineProfile() {
|
||||||
|
return slicerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlicerMachineProfile(String slicerMachineProfile) {
|
||||||
|
this.slicerMachineProfile = slicerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getBuildVolumeXMm() {
|
public Integer getBuildVolumeXMm() {
|
||||||
return buildVolumeXMm;
|
return buildVolumeXMm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.printcalculator.exception;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
@Slf4j
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(StorageException.class)
|
||||||
|
public ResponseEntity<?> handleStorageException(StorageException exc) {
|
||||||
|
// Log the full exception for internal debugging
|
||||||
|
log.error("Storage Exception occurred", exc);
|
||||||
|
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
|
||||||
|
// Check for specific virus case
|
||||||
|
if (exc.getMessage() != null && exc.getMessage().contains("antivirus scanner")) {
|
||||||
|
response.put("error", "Security Violation");
|
||||||
|
// Safe message for client
|
||||||
|
response.put("message", "File rejected by security policy.");
|
||||||
|
response.put("code", "VIRUS_DETECTED");
|
||||||
|
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic fallback for other storage errors to avoid leaking internal paths/details
|
||||||
|
response.put("error", "Storage Operation Failed");
|
||||||
|
response.put("message", "Unable to process the file upload.");
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||||
|
public ResponseEntity<?> handleMaxSizeException(MaxUploadSizeExceededException exc) {
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
response.put("error", "File too large");
|
||||||
|
response.put("message", "The uploaded file exceeds the maximum allowed size.");
|
||||||
|
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ public class ClamAVService {
|
|||||||
return true;
|
return true;
|
||||||
} else if (result instanceof ScanResult.VirusFound) {
|
} else if (result instanceof ScanResult.VirusFound) {
|
||||||
Map<String, Collection<String>> viruses = ((ScanResult.VirusFound) result).getFoundViruses();
|
Map<String, Collection<String>> viruses = ((ScanResult.VirusFound) result).getFoundViruses();
|
||||||
logger.warn("Virus found: {}", viruses);
|
logger.warn("VIRUS DETECTED: {}", viruses);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Unknown scan result: {}", result);
|
logger.warn("Unknown scan result: {}", result);
|
||||||
|
|||||||
@@ -89,26 +89,15 @@ public class SlicerService {
|
|||||||
|
|
||||||
command.add(inputStl.getAbsolutePath());
|
command.add(inputStl.getAbsolutePath());
|
||||||
|
|
||||||
logger.info("Executing Slicer: " + String.join(" ", command));
|
logger.info("Slicing file: " + inputStl.getAbsolutePath() + " (Size: " + inputStl.length() + " bytes)");
|
||||||
|
if (!inputStl.exists()) {
|
||||||
|
throw new IOException("Input file not found: " + inputStl.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Run Process
|
// 4. Run Process
|
||||||
ProcessBuilder pb = new ProcessBuilder(command);
|
runSlicerCommand(command, tempDir);
|
||||||
pb.directory(tempDir.toFile());
|
|
||||||
// pb.inheritIO(); // Useful for debugging, but maybe capture instead?
|
|
||||||
|
|
||||||
Process process = pb.start();
|
// 5. Find Output GCode
|
||||||
boolean finished = process.waitFor(5, TimeUnit.MINUTES);
|
|
||||||
|
|
||||||
if (!finished) {
|
|
||||||
process.destroy();
|
|
||||||
throw new IOException("Slicer timed out");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.exitValue() != 0) {
|
|
||||||
// Read stderr
|
|
||||||
String error = new String(process.getErrorStream().readAllBytes());
|
|
||||||
throw new IOException("Slicer failed with exit code " + process.exitValue() + ": " + error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Find Output GCode
|
// 5. Find Output GCode
|
||||||
// Usually [basename].gcode or plate_1.gcode
|
// Usually [basename].gcode or plate_1.gcode
|
||||||
@@ -131,9 +120,6 @@ public class SlicerService {
|
|||||||
// 6. Parse Results
|
// 6. Parse Results
|
||||||
return gCodeParser.parse(gcodeFile);
|
return gCodeParser.parse(gcodeFile);
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new IOException("Interrupted during slicing", e);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Cleanup temp dir
|
// Cleanup temp dir
|
||||||
// In production we should delete, for debugging we might want to keep?
|
// In production we should delete, for debugging we might want to keep?
|
||||||
@@ -143,4 +129,30 @@ public class SlicerService {
|
|||||||
// Implementation detail: Use a utility to clean up.
|
// Implementation detail: Use a utility to clean up.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(command);
|
||||||
|
pb.directory(tempDir.toFile());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process process = pb.start();
|
||||||
|
boolean finished = process.waitFor(5, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
if (!finished) {
|
||||||
|
process.destroy();
|
||||||
|
throw new IOException("Slicer timed out");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.exitValue() != 0) {
|
||||||
|
// Read stderr and stdout
|
||||||
|
String stderr = new String(process.getErrorStream().readAllBytes());
|
||||||
|
String stdout = new String(process.getInputStream().readAllBytes()); // STDOUT
|
||||||
|
throw new IOException("Slicer failed with exit code " + process.exitValue() +
|
||||||
|
"\nSTDERR: " + stderr +
|
||||||
|
"\nSTDOUT: " + stdout);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Interrupted during slicing", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.printcalculator.model.PrintStats;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
|
||||||
|
class SlicerServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProfileManager profileManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GCodeParser gCodeParser;
|
||||||
|
|
||||||
|
private ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private SlicerService slicerService;
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
// Captured execution details
|
||||||
|
private List<String> lastCommand;
|
||||||
|
private Path lastTempDir;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
|
||||||
|
// Subclass to override runSlicerCommand
|
||||||
|
slicerService = new SlicerService("orca-slicer", profileManager, gCodeParser, mapper) {
|
||||||
|
@Override
|
||||||
|
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException {
|
||||||
|
lastCommand = command;
|
||||||
|
lastTempDir = tempDir;
|
||||||
|
// Don't run actual process.
|
||||||
|
// Simulate GCode output creation for the parser to find?
|
||||||
|
// Or just let it fail at parser step since we only care about JSON generation here?
|
||||||
|
// For a full test, we should create a dummy GCode file.
|
||||||
|
|
||||||
|
File stl = new File(command.get(command.size() - 1));
|
||||||
|
String basename = stl.getName().replace(".stl", "");
|
||||||
|
Files.createFile(tempDir.resolve(basename + ".gcode"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock Profile Responses
|
||||||
|
ObjectNode emptyNode = mapper.createObjectNode();
|
||||||
|
when(profileManager.getMergedProfile(anyString(), eq("machine"))).thenReturn(emptyNode.deepCopy());
|
||||||
|
when(profileManager.getMergedProfile(anyString(), eq("filament"))).thenReturn(emptyNode.deepCopy());
|
||||||
|
when(profileManager.getMergedProfile(anyString(), eq("process"))).thenReturn(emptyNode.deepCopy());
|
||||||
|
|
||||||
|
// Mock Parser
|
||||||
|
when(gCodeParser.parse(any(File.class))).thenReturn(new PrintStats(100, "1m 40s", 10.5, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSlice_WithDefaults_ShouldGenerateConfig() throws IOException {
|
||||||
|
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||||
|
Files.createFile(dummyStl.toPath());
|
||||||
|
|
||||||
|
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, null);
|
||||||
|
|
||||||
|
assertNotNull(lastTempDir);
|
||||||
|
assertTrue(Files.exists(lastTempDir.resolve("process.json")));
|
||||||
|
assertTrue(Files.exists(lastTempDir.resolve("machine.json")));
|
||||||
|
assertTrue(Files.exists(lastTempDir.resolve("filament.json")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSlice_WithLayerHeightOverride_ShouldUpdateProcessJson() throws IOException {
|
||||||
|
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||||
|
Files.createFile(dummyStl.toPath());
|
||||||
|
|
||||||
|
Map<String, String> processOverrides = new HashMap<>();
|
||||||
|
processOverrides.put("layer_height", "0.12");
|
||||||
|
|
||||||
|
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, processOverrides);
|
||||||
|
|
||||||
|
File processJsonFile = lastTempDir.resolve("process.json").toFile();
|
||||||
|
ObjectNode processJson = (ObjectNode) mapper.readTree(processJsonFile);
|
||||||
|
|
||||||
|
assertTrue(processJson.has("layer_height"));
|
||||||
|
assertEquals("0.12", processJson.get("layer_height").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSlice_WithInfillAndSupportOverrides_ShouldUpdateProcessJson() throws IOException {
|
||||||
|
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||||
|
Files.createFile(dummyStl.toPath());
|
||||||
|
|
||||||
|
Map<String, String> processOverrides = new HashMap<>();
|
||||||
|
processOverrides.put("sparse_infill_density", "25%");
|
||||||
|
processOverrides.put("enable_support", "1");
|
||||||
|
|
||||||
|
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, processOverrides);
|
||||||
|
|
||||||
|
File processJsonFile = lastTempDir.resolve("process.json").toFile();
|
||||||
|
ObjectNode processJson = (ObjectNode) mapper.readTree(processJsonFile);
|
||||||
|
|
||||||
|
assertEquals("25%", processJson.get("sparse_infill_density").asText());
|
||||||
|
assertEquals("1", processJson.get("enable_support").asText());
|
||||||
|
}
|
||||||
|
}
|
||||||
1
db.sql
1
db.sql
@@ -12,6 +12,7 @@ create table printer_machine
|
|||||||
fleet_weight numeric(6, 3) not null default 1.000,
|
fleet_weight numeric(6, 3) not null default 1.000,
|
||||||
|
|
||||||
is_active boolean not null default true,
|
is_active boolean not null default true,
|
||||||
|
slicer_machine_profile varchar(255),
|
||||||
created_at timestamptz not null default now()
|
created_at timestamptz not null default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ services:
|
|||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- TEMP_DIR=/app/temp
|
- TEMP_DIR=/app/temp
|
||||||
- PROFILES_DIR=/app/profiles
|
- PROFILES_DIR=/app/profiles
|
||||||
- CLAMAV_HOST=192.168.1.147
|
- CLAMAV_HOST=host.docker.internal
|
||||||
- CLAMAV_PORT=3310
|
- CLAMAV_PORT=3310
|
||||||
|
- STORAGE_LOCATION=/app/storage
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- backend_profiles_${ENV}:/app/profiles
|
- backend_profiles_${ENV}:/app/profiles
|
||||||
- /mnt/cache/appdata/print-calculator/${ENV}/storage_quotes:/app/storage_quotes
|
- /mnt/cache/appdata/print-calculator/${ENV}/storage_quotes:/app/storage/quotes
|
||||||
- /mnt/cache/appdata/print-calculator/${ENV}/storage_orders:/app/storage_orders
|
- /mnt/cache/appdata/print-calculator/${ENV}/storage_orders:/app/storage/orders
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ services:
|
|||||||
- PROFILES_DIR=/app/profiles
|
- PROFILES_DIR=/app/profiles
|
||||||
- CLAMAV_HOST=192.168.1.147
|
- CLAMAV_HOST=192.168.1.147
|
||||||
- CLAMAV_PORT=3310
|
- CLAMAV_PORT=3310
|
||||||
|
- STORAGE_LOCATION=/app/storage
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- clamav
|
- clamav
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<h1>{{ 'CALC.TITLE' | translate }}</h1>
|
<h1>{{ 'CALC.TITLE' | translate }}</h1>
|
||||||
<p class="subtitle">{{ 'CALC.SUBTITLE' | translate }}</p>
|
<p class="subtitle">{{ 'CALC.SUBTITLE' | translate }}</p>
|
||||||
|
|
||||||
@if (error()) {
|
@if (error() === 'VIRUS_DETECTED') {
|
||||||
|
<app-alert type="error">{{ 'CALC.ERROR_VIRUS' | translate }}</app-alert>
|
||||||
|
} @else if (error()) {
|
||||||
<app-alert type="error">{{ 'CALC.ERROR_GENERIC' | translate }}</app-alert>
|
<app-alert type="error">{{ 'CALC.ERROR_GENERIC' | translate }}</app-alert>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
uploadProgress = signal(0);
|
uploadProgress = signal(0);
|
||||||
result = signal<QuoteResult | null>(null);
|
result = signal<QuoteResult | null>(null);
|
||||||
error = signal<boolean>(false);
|
error = signal<string | null>(null);
|
||||||
|
|
||||||
orderSuccess = signal(false);
|
orderSuccess = signal(false);
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.currentRequest = req;
|
this.currentRequest = req;
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.uploadProgress.set(0);
|
this.uploadProgress.set(0);
|
||||||
this.error.set(false);
|
this.error.set(null);
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
this.orderSuccess.set(false);
|
this.orderSuccess.set(false);
|
||||||
|
|
||||||
@@ -175,8 +175,12 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (err) => {
|
||||||
this.error.set(true);
|
if (typeof err === 'string') {
|
||||||
|
this.error.set(err);
|
||||||
|
} else {
|
||||||
|
this.error.set('GENERIC');
|
||||||
|
}
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ export class QuoteEstimatorService {
|
|||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Item upload failed', err);
|
console.error('Item upload failed', err);
|
||||||
finalResponses[index] = { success: false, fileName: item.file.name };
|
const errorMsg = err.error?.code === 'VIRUS_DETECTED' ? 'VIRUS_DETECTED' : 'UPLOAD_FAILED';
|
||||||
|
finalResponses[index] = { success: false, fileName: item.file.name, error: errorMsg };
|
||||||
completedRequests++;
|
completedRequests++;
|
||||||
checkCompletion();
|
checkCompletion();
|
||||||
}
|
}
|
||||||
@@ -262,7 +263,13 @@ export class QuoteEstimatorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (validCount === 0) {
|
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.');
|
observer.error('All calculations failed.');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"CTA_START": "Start Now",
|
"CTA_START": "Start Now",
|
||||||
"BUSINESS": "Business",
|
"BUSINESS": "Business",
|
||||||
"PRIVATE": "Private",
|
"PRIVATE": "Private",
|
||||||
"MODE_EASY": "Quick",
|
"MODE_EASY": "Easy Print",
|
||||||
"MODE_ADVANCED": "Advanced",
|
"MODE_ADVANCED": "Advanced",
|
||||||
"UPLOAD_LABEL": "Drag your 3D file here",
|
"UPLOAD_LABEL": "Drag your 3D file here",
|
||||||
"UPLOAD_SUB": "Supports STL, 3MF, STEP, OBJ up to 50MB",
|
"UPLOAD_SUB": "Supports STL, 3MF, STEP, OBJ up to 50MB",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"CTA_START": "Inizia Ora",
|
"CTA_START": "Inizia Ora",
|
||||||
"BUSINESS": "Aziende",
|
"BUSINESS": "Aziende",
|
||||||
"PRIVATE": "Privati",
|
"PRIVATE": "Privati",
|
||||||
"MODE_EASY": "Base",
|
"MODE_EASY": "Stampa Facile",
|
||||||
"MODE_ADVANCED": "Avanzata",
|
"MODE_ADVANCED": "Avanzata",
|
||||||
"UPLOAD_LABEL": "Trascina il tuo file 3D qui",
|
"UPLOAD_LABEL": "Trascina il tuo file 3D qui",
|
||||||
"UPLOAD_SUB": "Supportiamo STL, 3MF, STEP, OBJ fino a 50MB",
|
"UPLOAD_SUB": "Supportiamo STL, 3MF, STEP, OBJ fino a 50MB",
|
||||||
|
|||||||
Reference in New Issue
Block a user