feat(front-end and back-end): new nozle option, also fix quantity reload and reorganized service in back-end
This commit is contained in:
@@ -5,7 +5,7 @@ import com.printcalculator.entity.CustomQuoteRequest;
|
|||||||
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
||||||
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
|
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
|
||||||
import com.printcalculator.repository.CustomQuoteRequestRepository;
|
import com.printcalculator.repository.CustomQuoteRequestRepository;
|
||||||
import com.printcalculator.service.ClamAVService;
|
import com.printcalculator.service.storage.ClamAVService;
|
||||||
import com.printcalculator.service.email.EmailNotificationService;
|
import com.printcalculator.service.email.EmailNotificationService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ package com.printcalculator.controller;
|
|||||||
import com.printcalculator.dto.OptionsResponse;
|
import com.printcalculator.dto.OptionsResponse;
|
||||||
import com.printcalculator.entity.FilamentMaterialType;
|
import com.printcalculator.entity.FilamentMaterialType;
|
||||||
import com.printcalculator.entity.FilamentVariant;
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
import com.printcalculator.entity.LayerHeightOption;
|
|
||||||
import com.printcalculator.entity.MaterialOrcaProfileMap;
|
import com.printcalculator.entity.MaterialOrcaProfileMap;
|
||||||
import com.printcalculator.entity.NozzleOption;
|
import com.printcalculator.entity.NozzleOption;
|
||||||
import com.printcalculator.entity.PrinterMachine;
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
import com.printcalculator.entity.PrinterMachineProfile;
|
import com.printcalculator.entity.PrinterMachineProfile;
|
||||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||||
import com.printcalculator.repository.FilamentVariantRepository;
|
import com.printcalculator.repository.FilamentVariantRepository;
|
||||||
import com.printcalculator.repository.LayerHeightOptionRepository;
|
|
||||||
import com.printcalculator.repository.MaterialOrcaProfileMapRepository;
|
import com.printcalculator.repository.MaterialOrcaProfileMapRepository;
|
||||||
import com.printcalculator.repository.NozzleOptionRepository;
|
import com.printcalculator.repository.NozzleOptionRepository;
|
||||||
import com.printcalculator.repository.PrinterMachineRepository;
|
import com.printcalculator.repository.PrinterMachineRepository;
|
||||||
|
import com.printcalculator.service.NozzleLayerHeightPolicyService;
|
||||||
import com.printcalculator.service.OrcaProfileResolver;
|
import com.printcalculator.service.OrcaProfileResolver;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -24,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -32,26 +32,26 @@ public class OptionsController {
|
|||||||
|
|
||||||
private final FilamentMaterialTypeRepository materialRepo;
|
private final FilamentMaterialTypeRepository materialRepo;
|
||||||
private final FilamentVariantRepository variantRepo;
|
private final FilamentVariantRepository variantRepo;
|
||||||
private final LayerHeightOptionRepository layerHeightRepo;
|
|
||||||
private final NozzleOptionRepository nozzleRepo;
|
private final NozzleOptionRepository nozzleRepo;
|
||||||
private final PrinterMachineRepository printerMachineRepo;
|
private final PrinterMachineRepository printerMachineRepo;
|
||||||
private final MaterialOrcaProfileMapRepository materialOrcaMapRepo;
|
private final MaterialOrcaProfileMapRepository materialOrcaMapRepo;
|
||||||
private final OrcaProfileResolver orcaProfileResolver;
|
private final OrcaProfileResolver orcaProfileResolver;
|
||||||
|
private final NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService;
|
||||||
|
|
||||||
public OptionsController(FilamentMaterialTypeRepository materialRepo,
|
public OptionsController(FilamentMaterialTypeRepository materialRepo,
|
||||||
FilamentVariantRepository variantRepo,
|
FilamentVariantRepository variantRepo,
|
||||||
LayerHeightOptionRepository layerHeightRepo,
|
|
||||||
NozzleOptionRepository nozzleRepo,
|
NozzleOptionRepository nozzleRepo,
|
||||||
PrinterMachineRepository printerMachineRepo,
|
PrinterMachineRepository printerMachineRepo,
|
||||||
MaterialOrcaProfileMapRepository materialOrcaMapRepo,
|
MaterialOrcaProfileMapRepository materialOrcaMapRepo,
|
||||||
OrcaProfileResolver orcaProfileResolver) {
|
OrcaProfileResolver orcaProfileResolver,
|
||||||
|
NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService) {
|
||||||
this.materialRepo = materialRepo;
|
this.materialRepo = materialRepo;
|
||||||
this.variantRepo = variantRepo;
|
this.variantRepo = variantRepo;
|
||||||
this.layerHeightRepo = layerHeightRepo;
|
|
||||||
this.nozzleRepo = nozzleRepo;
|
this.nozzleRepo = nozzleRepo;
|
||||||
this.printerMachineRepo = printerMachineRepo;
|
this.printerMachineRepo = printerMachineRepo;
|
||||||
this.materialOrcaMapRepo = materialOrcaMapRepo;
|
this.materialOrcaMapRepo = materialOrcaMapRepo;
|
||||||
this.orcaProfileResolver = orcaProfileResolver;
|
this.orcaProfileResolver = orcaProfileResolver;
|
||||||
|
this.nozzleLayerHeightPolicyService = nozzleLayerHeightPolicyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/api/calculator/options")
|
@GetMapping("/api/calculator/options")
|
||||||
@@ -116,15 +116,6 @@ public class OptionsController {
|
|||||||
new OptionsResponse.InfillPatternOption("cubic", "Cubic")
|
new OptionsResponse.InfillPatternOption("cubic", "Cubic")
|
||||||
);
|
);
|
||||||
|
|
||||||
List<OptionsResponse.LayerHeightOptionDTO> layers = layerHeightRepo.findAll().stream()
|
|
||||||
.filter(l -> Boolean.TRUE.equals(l.getIsActive()))
|
|
||||||
.sorted(Comparator.comparing(LayerHeightOption::getLayerHeightMm))
|
|
||||||
.map(l -> new OptionsResponse.LayerHeightOptionDTO(
|
|
||||||
l.getLayerHeightMm().doubleValue(),
|
|
||||||
String.format("%.2f mm", l.getLayerHeightMm())
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
List<OptionsResponse.NozzleOptionDTO> nozzles = nozzleRepo.findAll().stream()
|
List<OptionsResponse.NozzleOptionDTO> nozzles = nozzleRepo.findAll().stream()
|
||||||
.filter(n -> Boolean.TRUE.equals(n.getIsActive()))
|
.filter(n -> Boolean.TRUE.equals(n.getIsActive()))
|
||||||
.sorted(Comparator.comparing(NozzleOption::getNozzleDiameterMm))
|
.sorted(Comparator.comparing(NozzleOption::getNozzleDiameterMm))
|
||||||
@@ -137,7 +128,31 @@ public class OptionsController {
|
|||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(new OptionsResponse(materialOptions, qualities, patterns, layers, nozzles));
|
Map<BigDecimal, List<BigDecimal>> rulesByNozzle = nozzleLayerHeightPolicyService.getActiveRulesByNozzle();
|
||||||
|
BigDecimal selectedNozzle = nozzleLayerHeightPolicyService.resolveNozzle(
|
||||||
|
nozzleDiameter != null ? BigDecimal.valueOf(nozzleDiameter) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
List<OptionsResponse.LayerHeightOptionDTO> layers = toLayerDtos(rulesByNozzle.getOrDefault(selectedNozzle, List.of()));
|
||||||
|
if (layers.isEmpty()) {
|
||||||
|
layers = rulesByNozzle.values().stream().findFirst().map(this::toLayerDtos).orElse(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OptionsResponse.NozzleLayerHeightOptionsDTO> layerHeightsByNozzle = rulesByNozzle.entrySet().stream()
|
||||||
|
.map(entry -> new OptionsResponse.NozzleLayerHeightOptionsDTO(
|
||||||
|
entry.getKey().doubleValue(),
|
||||||
|
toLayerDtos(entry.getValue())
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(new OptionsResponse(
|
||||||
|
materialOptions,
|
||||||
|
qualities,
|
||||||
|
patterns,
|
||||||
|
layers,
|
||||||
|
nozzles,
|
||||||
|
layerHeightsByNozzle
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Long> resolveCompatibleMaterialTypeIds(Long printerMachineId, Double nozzleDiameter) {
|
private Set<Long> resolveCompatibleMaterialTypeIds(Long printerMachineId, Double nozzleDiameter) {
|
||||||
@@ -152,9 +167,9 @@ public class OptionsController {
|
|||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal nozzle = nozzleDiameter != null
|
BigDecimal nozzle = nozzleLayerHeightPolicyService.resolveNozzle(
|
||||||
? BigDecimal.valueOf(nozzleDiameter)
|
nozzleDiameter != null ? BigDecimal.valueOf(nozzleDiameter) : null
|
||||||
: BigDecimal.valueOf(0.40);
|
);
|
||||||
|
|
||||||
PrinterMachineProfile machineProfile = orcaProfileResolver
|
PrinterMachineProfile machineProfile = orcaProfileResolver
|
||||||
.resolveMachineProfile(machine, nozzle)
|
.resolveMachineProfile(machine, nozzle)
|
||||||
@@ -172,6 +187,16 @@ public class OptionsController {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<OptionsResponse.LayerHeightOptionDTO> toLayerDtos(List<BigDecimal> layers) {
|
||||||
|
return layers.stream()
|
||||||
|
.sorted(Comparator.naturalOrder())
|
||||||
|
.map(layer -> new OptionsResponse.LayerHeightOptionDTO(
|
||||||
|
layer.doubleValue(),
|
||||||
|
String.format("%.2f mm", layer)
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveHexColor(FilamentVariant variant) {
|
private String resolveHexColor(FilamentVariant variant) {
|
||||||
if (variant.getColorHex() != null && !variant.getColorHex().isBlank()) {
|
if (variant.getColorHex() != null && !variant.getColorHex().isBlank()) {
|
||||||
return variant.getColorHex();
|
return variant.getColorHex();
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package com.printcalculator.controller;
|
|||||||
import com.printcalculator.dto.*;
|
import com.printcalculator.dto.*;
|
||||||
import com.printcalculator.entity.*;
|
import com.printcalculator.entity.*;
|
||||||
import com.printcalculator.repository.*;
|
import com.printcalculator.repository.*;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.OrderService;
|
import com.printcalculator.service.OrderService;
|
||||||
import com.printcalculator.service.PaymentService;
|
import com.printcalculator.service.payment.PaymentService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import com.printcalculator.service.TwintPaymentService;
|
import com.printcalculator.service.payment.TwintPaymentService;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|||||||
@@ -4,17 +4,23 @@ import com.printcalculator.entity.PrinterMachine;
|
|||||||
import com.printcalculator.model.PrintStats;
|
import com.printcalculator.model.PrintStats;
|
||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
import com.printcalculator.repository.PrinterMachineRepository;
|
import com.printcalculator.repository.PrinterMachineRepository;
|
||||||
|
import com.printcalculator.service.NozzleLayerHeightPolicyService;
|
||||||
import com.printcalculator.service.QuoteCalculator;
|
import com.printcalculator.service.QuoteCalculator;
|
||||||
import com.printcalculator.service.SlicerService;
|
import com.printcalculator.service.SlicerService;
|
||||||
|
import com.printcalculator.service.storage.ClamAVService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class QuoteController {
|
public class QuoteController {
|
||||||
@@ -22,17 +28,23 @@ public class QuoteController {
|
|||||||
private final SlicerService slicerService;
|
private final SlicerService slicerService;
|
||||||
private final QuoteCalculator quoteCalculator;
|
private final QuoteCalculator quoteCalculator;
|
||||||
private final PrinterMachineRepository machineRepo;
|
private final PrinterMachineRepository machineRepo;
|
||||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
private final ClamAVService clamAVService;
|
||||||
|
private final NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService;
|
||||||
|
|
||||||
// Defaults (using aliases defined in ProfileManager)
|
// Defaults (using aliases defined in ProfileManager)
|
||||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||||
private static final String DEFAULT_PROCESS = "standard";
|
private static final String DEFAULT_PROCESS = "standard";
|
||||||
|
|
||||||
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo, com.printcalculator.service.ClamAVService clamAVService) {
|
public QuoteController(SlicerService slicerService,
|
||||||
|
QuoteCalculator quoteCalculator,
|
||||||
|
PrinterMachineRepository machineRepo,
|
||||||
|
ClamAVService clamAVService,
|
||||||
|
NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService) {
|
||||||
this.slicerService = slicerService;
|
this.slicerService = slicerService;
|
||||||
this.quoteCalculator = quoteCalculator;
|
this.quoteCalculator = quoteCalculator;
|
||||||
this.machineRepo = machineRepo;
|
this.machineRepo = machineRepo;
|
||||||
this.clamAVService = clamAVService;
|
this.clamAVService = clamAVService;
|
||||||
|
this.nozzleLayerHeightPolicyService = nozzleLayerHeightPolicyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/quote")
|
@PostMapping("/api/quote")
|
||||||
@@ -69,15 +81,27 @@ public class QuoteController {
|
|||||||
if (infillPattern != null && !infillPattern.isEmpty()) {
|
if (infillPattern != null && !infillPattern.isEmpty()) {
|
||||||
processOverrides.put("sparse_infill_pattern", infillPattern);
|
processOverrides.put("sparse_infill_pattern", infillPattern);
|
||||||
}
|
}
|
||||||
|
BigDecimal normalizedNozzle = nozzleLayerHeightPolicyService.resolveNozzle(
|
||||||
|
nozzleDiameter != null ? BigDecimal.valueOf(nozzleDiameter) : null
|
||||||
|
);
|
||||||
if (layerHeight != null) {
|
if (layerHeight != null) {
|
||||||
processOverrides.put("layer_height", String.valueOf(layerHeight));
|
BigDecimal normalizedLayer = nozzleLayerHeightPolicyService.normalizeLayer(BigDecimal.valueOf(layerHeight));
|
||||||
|
if (!nozzleLayerHeightPolicyService.isAllowed(normalizedNozzle, normalizedLayer)) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
BAD_REQUEST,
|
||||||
|
"Layer height " + normalizedLayer.stripTrailingZeros().toPlainString()
|
||||||
|
+ " is not allowed for nozzle " + normalizedNozzle.stripTrailingZeros().toPlainString()
|
||||||
|
+ ". Allowed: " + nozzleLayerHeightPolicyService.allowedLayersLabel(normalizedNozzle)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
processOverrides.put("layer_height", normalizedLayer.stripTrailingZeros().toPlainString());
|
||||||
}
|
}
|
||||||
if (supportEnabled != null) {
|
if (supportEnabled != null) {
|
||||||
processOverrides.put("enable_support", supportEnabled ? "1" : "0");
|
processOverrides.put("enable_support", supportEnabled ? "1" : "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nozzleDiameter != null) {
|
if (nozzleDiameter != null) {
|
||||||
machineOverrides.put("nozzle_diameter", String.valueOf(nozzleDiameter));
|
machineOverrides.put("nozzle_diameter", normalizedNozzle.stripTrailingZeros().toPlainString());
|
||||||
// Also need to ensure the printer profile is compatible or just override?
|
// Also need to ensure the printer profile is compatible or just override?
|
||||||
// Usually nozzle diameter changes require a different printer profile or deep overrides.
|
// Usually nozzle diameter changes require a different printer profile or deep overrides.
|
||||||
// For now, we trust the override key works on the base profile.
|
// For now, we trust the override key works on the base profile.
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import com.printcalculator.repository.PrinterMachineRepository;
|
|||||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||||
import com.printcalculator.repository.QuoteSessionRepository;
|
import com.printcalculator.repository.QuoteSessionRepository;
|
||||||
import com.printcalculator.service.OrcaProfileResolver;
|
import com.printcalculator.service.OrcaProfileResolver;
|
||||||
|
import com.printcalculator.service.NozzleLayerHeightPolicyService;
|
||||||
import com.printcalculator.service.QuoteCalculator;
|
import com.printcalculator.service.QuoteCalculator;
|
||||||
import com.printcalculator.service.QuoteSessionTotalsService;
|
import com.printcalculator.service.QuoteSessionTotalsService;
|
||||||
import com.printcalculator.service.SlicerService;
|
import com.printcalculator.service.SlicerService;
|
||||||
|
import com.printcalculator.service.storage.ClamAVService;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -61,8 +63,9 @@ public class QuoteSessionController {
|
|||||||
private final FilamentMaterialTypeRepository materialRepo;
|
private final FilamentMaterialTypeRepository materialRepo;
|
||||||
private final FilamentVariantRepository variantRepo;
|
private final FilamentVariantRepository variantRepo;
|
||||||
private final OrcaProfileResolver orcaProfileResolver;
|
private final OrcaProfileResolver orcaProfileResolver;
|
||||||
|
private final NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService;
|
||||||
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
||||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
private final ClamAVService clamAVService;
|
||||||
private final QuoteSessionTotalsService quoteSessionTotalsService;
|
private final QuoteSessionTotalsService quoteSessionTotalsService;
|
||||||
|
|
||||||
public QuoteSessionController(QuoteSessionRepository sessionRepo,
|
public QuoteSessionController(QuoteSessionRepository sessionRepo,
|
||||||
@@ -73,8 +76,9 @@ public class QuoteSessionController {
|
|||||||
FilamentMaterialTypeRepository materialRepo,
|
FilamentMaterialTypeRepository materialRepo,
|
||||||
FilamentVariantRepository variantRepo,
|
FilamentVariantRepository variantRepo,
|
||||||
OrcaProfileResolver orcaProfileResolver,
|
OrcaProfileResolver orcaProfileResolver,
|
||||||
|
NozzleLayerHeightPolicyService nozzleLayerHeightPolicyService,
|
||||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
||||||
com.printcalculator.service.ClamAVService clamAVService,
|
ClamAVService clamAVService,
|
||||||
QuoteSessionTotalsService quoteSessionTotalsService) {
|
QuoteSessionTotalsService quoteSessionTotalsService) {
|
||||||
this.sessionRepo = sessionRepo;
|
this.sessionRepo = sessionRepo;
|
||||||
this.lineItemRepo = lineItemRepo;
|
this.lineItemRepo = lineItemRepo;
|
||||||
@@ -84,6 +88,7 @@ public class QuoteSessionController {
|
|||||||
this.materialRepo = materialRepo;
|
this.materialRepo = materialRepo;
|
||||||
this.variantRepo = variantRepo;
|
this.variantRepo = variantRepo;
|
||||||
this.orcaProfileResolver = orcaProfileResolver;
|
this.orcaProfileResolver = orcaProfileResolver;
|
||||||
|
this.nozzleLayerHeightPolicyService = nozzleLayerHeightPolicyService;
|
||||||
this.pricingRepo = pricingRepo;
|
this.pricingRepo = pricingRepo;
|
||||||
this.clamAVService = clamAVService;
|
this.clamAVService = clamAVService;
|
||||||
this.quoteSessionTotalsService = quoteSessionTotalsService;
|
this.quoteSessionTotalsService = quoteSessionTotalsService;
|
||||||
@@ -167,7 +172,23 @@ public class QuoteSessionController {
|
|||||||
applyPrintSettings(settings);
|
applyPrintSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal nozzleDiameter = BigDecimal.valueOf(settings.getNozzleDiameter() != null ? settings.getNozzleDiameter() : 0.4);
|
BigDecimal nozzleDiameter = nozzleLayerHeightPolicyService.resolveNozzle(
|
||||||
|
settings.getNozzleDiameter() != null ? BigDecimal.valueOf(settings.getNozzleDiameter()) : null
|
||||||
|
);
|
||||||
|
BigDecimal layerHeight = nozzleLayerHeightPolicyService.resolveLayer(
|
||||||
|
settings.getLayerHeight() != null ? BigDecimal.valueOf(settings.getLayerHeight()) : null,
|
||||||
|
nozzleDiameter
|
||||||
|
);
|
||||||
|
if (!nozzleLayerHeightPolicyService.isAllowed(nozzleDiameter, layerHeight)) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
BAD_REQUEST,
|
||||||
|
"Layer height " + layerHeight.stripTrailingZeros().toPlainString()
|
||||||
|
+ " is not allowed for nozzle " + nozzleDiameter.stripTrailingZeros().toPlainString()
|
||||||
|
+ ". Allowed: " + nozzleLayerHeightPolicyService.allowedLayersLabel(nozzleDiameter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
settings.setNozzleDiameter(nozzleDiameter.doubleValue());
|
||||||
|
settings.setLayerHeight(layerHeight.doubleValue());
|
||||||
|
|
||||||
// Pick machine (selected machine if provided, otherwise first active)
|
// Pick machine (selected machine if provided, otherwise first active)
|
||||||
PrinterMachine machine = resolvePrinterMachine(settings.getPrinterMachineId());
|
PrinterMachine machine = resolvePrinterMachine(settings.getPrinterMachineId());
|
||||||
@@ -190,7 +211,7 @@ public class QuoteSessionController {
|
|||||||
if (!cadSession) {
|
if (!cadSession) {
|
||||||
session.setMaterialCode(selectedVariant.getFilamentMaterialType().getMaterialCode());
|
session.setMaterialCode(selectedVariant.getFilamentMaterialType().getMaterialCode());
|
||||||
session.setNozzleDiameterMm(nozzleDiameter);
|
session.setNozzleDiameterMm(nozzleDiameter);
|
||||||
session.setLayerHeightMm(BigDecimal.valueOf(settings.getLayerHeight() != null ? settings.getLayerHeight() : 0.2));
|
session.setLayerHeightMm(layerHeight);
|
||||||
session.setInfillPattern(settings.getInfillPattern());
|
session.setInfillPattern(settings.getInfillPattern());
|
||||||
session.setInfillPercent(settings.getInfillDensity() != null ? settings.getInfillDensity().intValue() : 20);
|
session.setInfillPercent(settings.getInfillDensity() != null ? settings.getInfillDensity().intValue() : 20);
|
||||||
session.setSupportsEnabled(settings.getSupportsEnabled() != null ? settings.getSupportsEnabled() : false);
|
session.setSupportsEnabled(settings.getSupportsEnabled() != null ? settings.getSupportsEnabled() : false);
|
||||||
@@ -209,7 +230,7 @@ public class QuoteSessionController {
|
|||||||
|
|
||||||
// Build overrides map from settings
|
// Build overrides map from settings
|
||||||
Map<String, String> processOverrides = new HashMap<>();
|
Map<String, String> processOverrides = new HashMap<>();
|
||||||
if (settings.getLayerHeight() != null) processOverrides.put("layer_height", String.valueOf(settings.getLayerHeight()));
|
processOverrides.put("layer_height", layerHeight.stripTrailingZeros().toPlainString());
|
||||||
if (settings.getInfillDensity() != null) processOverrides.put("sparse_infill_density", settings.getInfillDensity() + "%");
|
if (settings.getInfillDensity() != null) processOverrides.put("sparse_infill_density", settings.getInfillDensity() + "%");
|
||||||
if (settings.getInfillPattern() != null) processOverrides.put("sparse_infill_pattern", settings.getInfillPattern());
|
if (settings.getInfillPattern() != null) processOverrides.put("sparse_infill_pattern", settings.getInfillPattern());
|
||||||
|
|
||||||
@@ -289,6 +310,10 @@ public class QuoteSessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyPrintSettings(com.printcalculator.dto.PrintSettingsDto settings) {
|
private void applyPrintSettings(com.printcalculator.dto.PrintSettingsDto settings) {
|
||||||
|
if (settings.getNozzleDiameter() == null) {
|
||||||
|
settings.setNozzleDiameter(0.40);
|
||||||
|
}
|
||||||
|
|
||||||
if ("BASIC".equalsIgnoreCase(settings.getComplexityMode())) {
|
if ("BASIC".equalsIgnoreCase(settings.getComplexityMode())) {
|
||||||
// Set defaults based on Quality
|
// Set defaults based on Quality
|
||||||
String quality = settings.getQuality() != null ? settings.getQuality().toLowerCase() : "standard";
|
String quality = settings.getQuality() != null ? settings.getQuality().toLowerCase() : "standard";
|
||||||
@@ -313,7 +338,6 @@ public class QuoteSessionController {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ADVANCED Mode: Use values from Frontend, set defaults if missing
|
// ADVANCED Mode: Use values from Frontend, set defaults if missing
|
||||||
if (settings.getLayerHeight() == null) settings.setLayerHeight(0.20);
|
|
||||||
if (settings.getInfillDensity() == null) settings.setInfillDensity(20.0);
|
if (settings.getInfillDensity() == null) settings.setInfillDensity(20.0);
|
||||||
if (settings.getInfillPattern() == null) settings.setInfillPattern("grid");
|
if (settings.getInfillPattern() == null) settings.setInfillPattern("grid");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import com.printcalculator.event.OrderShippedEvent;
|
|||||||
import com.printcalculator.repository.OrderItemRepository;
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
import com.printcalculator.repository.OrderRepository;
|
import com.printcalculator.repository.OrderRepository;
|
||||||
import com.printcalculator.repository.PaymentRepository;
|
import com.printcalculator.repository.PaymentRepository;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.PaymentService;
|
import com.printcalculator.service.payment.PaymentService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
@@ -34,7 +34,6 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.InvalidPathException;
|
import java.nio.file.InvalidPathException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ public record OptionsResponse(
|
|||||||
List<QualityOption> qualities,
|
List<QualityOption> qualities,
|
||||||
List<InfillPatternOption> infillPatterns,
|
List<InfillPatternOption> infillPatterns,
|
||||||
List<LayerHeightOptionDTO> layerHeights,
|
List<LayerHeightOptionDTO> layerHeights,
|
||||||
List<NozzleOptionDTO> nozzleDiameters
|
List<NozzleOptionDTO> nozzleDiameters,
|
||||||
|
List<NozzleLayerHeightOptionsDTO> layerHeightsByNozzle
|
||||||
) {
|
) {
|
||||||
public record MaterialOption(String code, String label, List<VariantOption> variants) {}
|
public record MaterialOption(String code, String label, List<VariantOption> variants) {}
|
||||||
public record VariantOption(
|
public record VariantOption(
|
||||||
@@ -24,4 +25,5 @@ public record OptionsResponse(
|
|||||||
public record InfillPatternOption(String id, String label) {}
|
public record InfillPatternOption(String id, String label) {}
|
||||||
public record LayerHeightOptionDTO(double value, String label) {}
|
public record LayerHeightOptionDTO(double value, String label) {}
|
||||||
public record NozzleOptionDTO(double value, String label) {}
|
public record NozzleOptionDTO(double value, String label) {}
|
||||||
|
public record NozzleLayerHeightOptionsDTO(double nozzleDiameter, List<LayerHeightOptionDTO> layerHeights) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.printcalculator.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.ColumnDefault;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "nozzle_layer_height_option",
|
||||||
|
uniqueConstraints = @UniqueConstraint(
|
||||||
|
name = "ux_nozzle_layer_height_option_nozzle_layer",
|
||||||
|
columnNames = {"nozzle_diameter_mm", "layer_height_mm"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public class NozzleLayerHeightOption {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "nozzle_layer_height_option_id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "nozzle_diameter_mm", nullable = false, precision = 4, scale = 2)
|
||||||
|
private BigDecimal nozzleDiameterMm;
|
||||||
|
|
||||||
|
@Column(name = "layer_height_mm", nullable = false, precision = 5, scale = 3)
|
||||||
|
private BigDecimal layerHeightMm;
|
||||||
|
|
||||||
|
@ColumnDefault("true")
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private Boolean isActive;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getNozzleDiameterMm() {
|
||||||
|
return nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
|
||||||
|
this.nozzleDiameterMm = nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLayerHeightMm() {
|
||||||
|
return layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayerHeightMm(BigDecimal layerHeightMm) {
|
||||||
|
this.layerHeightMm = layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(Boolean isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ import com.printcalculator.event.OrderShippedEvent;
|
|||||||
import com.printcalculator.event.PaymentConfirmedEvent;
|
import com.printcalculator.event.PaymentConfirmedEvent;
|
||||||
import com.printcalculator.event.PaymentReportedEvent;
|
import com.printcalculator.event.PaymentReportedEvent;
|
||||||
import com.printcalculator.repository.OrderItemRepository;
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import com.printcalculator.service.email.EmailNotificationService;
|
import com.printcalculator.service.email.EmailNotificationService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.printcalculator.repository;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.NozzleLayerHeightOption;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface NozzleLayerHeightOptionRepository extends JpaRepository<NozzleLayerHeightOption, Long> {
|
||||||
|
List<NozzleLayerHeightOption> findByIsActiveTrueOrderByNozzleDiameterMmAscLayerHeightMmAsc();
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.NozzleLayerHeightOption;
|
||||||
|
import com.printcalculator.repository.NozzleLayerHeightOptionRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NozzleLayerHeightPolicyService {
|
||||||
|
private static final BigDecimal DEFAULT_NOZZLE = BigDecimal.valueOf(0.40).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
private static final BigDecimal DEFAULT_LAYER = BigDecimal.valueOf(0.20).setScale(3, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
private final NozzleLayerHeightOptionRepository ruleRepo;
|
||||||
|
|
||||||
|
public NozzleLayerHeightPolicyService(NozzleLayerHeightOptionRepository ruleRepo) {
|
||||||
|
this.ruleRepo = ruleRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BigDecimal, List<BigDecimal>> getActiveRulesByNozzle() {
|
||||||
|
List<NozzleLayerHeightOption> rules = ruleRepo.findByIsActiveTrueOrderByNozzleDiameterMmAscLayerHeightMmAsc();
|
||||||
|
if (rules.isEmpty()) {
|
||||||
|
return fallbackRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<BigDecimal, List<BigDecimal>> byNozzle = new LinkedHashMap<>();
|
||||||
|
for (NozzleLayerHeightOption rule : rules) {
|
||||||
|
BigDecimal nozzle = normalizeNozzle(rule.getNozzleDiameterMm());
|
||||||
|
BigDecimal layer = normalizeLayer(rule.getLayerHeightMm());
|
||||||
|
if (nozzle == null || layer == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byNozzle.computeIfAbsent(nozzle, ignored -> new ArrayList<>()).add(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
byNozzle.values().forEach(this::sortAndDeduplicate);
|
||||||
|
return byNozzle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal normalizeNozzle(BigDecimal value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal normalizeLayer(BigDecimal value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.setScale(3, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal resolveNozzle(BigDecimal requestedNozzle) {
|
||||||
|
return normalizeNozzle(requestedNozzle != null ? requestedNozzle : DEFAULT_NOZZLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal resolveLayer(BigDecimal requestedLayer, BigDecimal nozzleDiameter) {
|
||||||
|
if (requestedLayer != null) {
|
||||||
|
return normalizeLayer(requestedLayer);
|
||||||
|
}
|
||||||
|
return defaultLayerForNozzle(nozzleDiameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BigDecimal> allowedLayersForNozzle(BigDecimal nozzleDiameter) {
|
||||||
|
BigDecimal nozzle = resolveNozzle(nozzleDiameter);
|
||||||
|
List<BigDecimal> allowed = getActiveRulesByNozzle().get(nozzle);
|
||||||
|
return allowed != null ? allowed : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowed(BigDecimal nozzleDiameter, BigDecimal layerHeight) {
|
||||||
|
BigDecimal layer = normalizeLayer(layerHeight);
|
||||||
|
if (layer == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return allowedLayersForNozzle(nozzleDiameter)
|
||||||
|
.stream()
|
||||||
|
.anyMatch(allowed -> allowed.compareTo(layer) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal defaultLayerForNozzle(BigDecimal nozzleDiameter) {
|
||||||
|
List<BigDecimal> allowed = allowedLayersForNozzle(nozzleDiameter);
|
||||||
|
if (allowed.isEmpty()) {
|
||||||
|
return DEFAULT_LAYER;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal preferred = normalizeLayer(DEFAULT_LAYER);
|
||||||
|
for (BigDecimal candidate : allowed) {
|
||||||
|
if (candidate.compareTo(preferred) == 0) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String allowedLayersLabel(BigDecimal nozzleDiameter) {
|
||||||
|
List<BigDecimal> allowed = allowedLayersForNozzle(nozzleDiameter);
|
||||||
|
if (allowed.isEmpty()) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
return allowed.stream()
|
||||||
|
.map(value -> String.format(Locale.ROOT, "%.2f", value))
|
||||||
|
.reduce((a, b) -> a + ", " + b)
|
||||||
|
.orElse("none");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortAndDeduplicate(List<BigDecimal> values) {
|
||||||
|
values.sort(Comparator.naturalOrder());
|
||||||
|
for (int i = values.size() - 1; i > 0; i--) {
|
||||||
|
if (values.get(i).compareTo(values.get(i - 1)) == 0) {
|
||||||
|
values.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BigDecimal, List<BigDecimal>> fallbackRules() {
|
||||||
|
Map<BigDecimal, List<BigDecimal>> fallback = new LinkedHashMap<>();
|
||||||
|
fallback.put(scaleNozzle(0.20), scaleLayers(0.04, 0.06, 0.08, 0.10, 0.12));
|
||||||
|
fallback.put(scaleNozzle(0.40), scaleLayers(0.08, 0.12, 0.16, 0.20, 0.24, 0.28));
|
||||||
|
fallback.put(scaleNozzle(0.60), scaleLayers(0.16, 0.20, 0.24, 0.30, 0.36));
|
||||||
|
fallback.put(scaleNozzle(0.80), scaleLayers(0.20, 0.28, 0.36, 0.40, 0.48, 0.56));
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal scaleNozzle(double value) {
|
||||||
|
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BigDecimal> scaleLayers(double... values) {
|
||||||
|
List<BigDecimal> scaled = new ArrayList<>();
|
||||||
|
for (double value : values) {
|
||||||
|
scaled.add(BigDecimal.valueOf(value).setScale(3, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ import com.printcalculator.repository.OrderRepository;
|
|||||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||||
import com.printcalculator.repository.QuoteSessionRepository;
|
import com.printcalculator.repository.QuoteSessionRepository;
|
||||||
import com.printcalculator.event.OrderCreatedEvent;
|
import com.printcalculator.event.OrderCreatedEvent;
|
||||||
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
|
import com.printcalculator.service.payment.PaymentService;
|
||||||
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.payment;
|
||||||
|
|
||||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||||
import com.openhtmltopdf.svgsupport.BatikSVGDrawer;
|
import com.openhtmltopdf.svgsupport.BatikSVGDrawer;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.payment;
|
||||||
|
|
||||||
import com.printcalculator.entity.Order;
|
import com.printcalculator.entity.Order;
|
||||||
import com.printcalculator.entity.Payment;
|
import com.printcalculator.entity.Payment;
|
||||||
@@ -65,7 +65,7 @@ public class PaymentService {
|
|||||||
payment.setReportedAt(OffsetDateTime.now());
|
payment.setReportedAt(OffsetDateTime.now());
|
||||||
|
|
||||||
// We intentionally do not update the payment method here based on user input,
|
// We intentionally do not update the payment method here based on user input,
|
||||||
// because the user cannot reliably determine the actual method without an integration.
|
// because the system cannot reliably determine the actual method without an integration.
|
||||||
// It will be updated by the backoffice admin manually.
|
// It will be updated by the backoffice admin manually.
|
||||||
|
|
||||||
payment = paymentRepo.save(payment);
|
payment = paymentRepo.save(payment);
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.payment;
|
||||||
|
|
||||||
import com.printcalculator.entity.Order;
|
import com.printcalculator.entity.Order;
|
||||||
import net.codecrete.qrbill.generator.Bill;
|
import net.codecrete.qrbill.generator.Bill;
|
||||||
import net.codecrete.qrbill.generator.GraphicsFormat;
|
|
||||||
import net.codecrete.qrbill.generator.QRBill;
|
import net.codecrete.qrbill.generator.QRBill;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class QrBillService {
|
public class QrBillService {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.payment;
|
||||||
|
|
||||||
import io.nayuki.qrcodegen.QrCode;
|
import io.nayuki.qrcodegen.QrCode;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.storage;
|
||||||
|
|
||||||
import com.printcalculator.exception.VirusDetectedException;
|
import com.printcalculator.exception.VirusDetectedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.storage;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -7,7 +7,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import com.printcalculator.exception.StorageException;
|
import com.printcalculator.exception.StorageException;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service.storage;
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -8,12 +8,12 @@ import com.printcalculator.repository.OrderRepository;
|
|||||||
import com.printcalculator.repository.PaymentRepository;
|
import com.printcalculator.repository.PaymentRepository;
|
||||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||||
import com.printcalculator.repository.QuoteSessionRepository;
|
import com.printcalculator.repository.QuoteSessionRepository;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.OrderService;
|
import com.printcalculator.service.OrderService;
|
||||||
import com.printcalculator.service.PaymentService;
|
import com.printcalculator.service.payment.PaymentService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import com.printcalculator.service.TwintPaymentService;
|
import com.printcalculator.service.payment.TwintPaymentService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import com.printcalculator.entity.Order;
|
|||||||
import com.printcalculator.repository.OrderItemRepository;
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
import com.printcalculator.repository.OrderRepository;
|
import com.printcalculator.repository.OrderRepository;
|
||||||
import com.printcalculator.repository.PaymentRepository;
|
import com.printcalculator.repository.PaymentRepository;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.PaymentService;
|
import com.printcalculator.service.payment.PaymentService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import com.printcalculator.entity.Customer;
|
|||||||
import com.printcalculator.entity.Order;
|
import com.printcalculator.entity.Order;
|
||||||
import com.printcalculator.event.OrderCreatedEvent;
|
import com.printcalculator.event.OrderCreatedEvent;
|
||||||
import com.printcalculator.repository.OrderItemRepository;
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.payment.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.storage.StorageService;
|
||||||
import com.printcalculator.service.email.EmailNotificationService;
|
import com.printcalculator.service.email.EmailNotificationService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
class="mode-option"
|
class="mode-option"
|
||||||
[class.active]="mode() === 'easy'"
|
[class.active]="mode() === 'easy'"
|
||||||
[class.disabled]="cadSessionLocked()"
|
[class.disabled]="cadSessionLocked()"
|
||||||
(click)="!cadSessionLocked() && mode.set('easy')"
|
(click)="switchMode('easy')"
|
||||||
>
|
>
|
||||||
{{ "CALC.MODE_EASY" | translate }}
|
{{ "CALC.MODE_EASY" | translate }}
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
class="mode-option"
|
class="mode-option"
|
||||||
[class.active]="mode() === 'advanced'"
|
[class.active]="mode() === 'advanced'"
|
||||||
[class.disabled]="cadSessionLocked()"
|
[class.disabled]="cadSessionLocked()"
|
||||||
(click)="!cadSessionLocked() && mode.set('advanced')"
|
(click)="switchMode('advanced')"
|
||||||
>
|
>
|
||||||
{{ "CALC.MODE_ADVANCED" | translate }}
|
{{ "CALC.MODE_ADVANCED" | translate }}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
[loading]="loading()"
|
[loading]="loading()"
|
||||||
[uploadProgress]="uploadProgress()"
|
[uploadProgress]="uploadProgress()"
|
||||||
(submitRequest)="onCalculate($event)"
|
(submitRequest)="onCalculate($event)"
|
||||||
|
(itemQuantityChange)="onUploadItemQuantityChange($event)"
|
||||||
|
(printSettingsChange)="onUploadPrintSettingsChange($event)"
|
||||||
></app-upload-form>
|
></app-upload-form>
|
||||||
</app-card>
|
</app-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,8 +66,10 @@
|
|||||||
} @else if (result()) {
|
} @else if (result()) {
|
||||||
<app-quote-result
|
<app-quote-result
|
||||||
[result]="result()!"
|
[result]="result()!"
|
||||||
|
[recalculationRequired]="requiresRecalculation()"
|
||||||
(consult)="onConsult()"
|
(consult)="onConsult()"
|
||||||
(proceed)="onProceed()"
|
(proceed)="onProceed()"
|
||||||
|
(itemQuantityPreviewChange)="onQuoteItemQuantityPreviewChange($event)"
|
||||||
(itemChange)="onItemChange($event)"
|
(itemChange)="onItemChange($event)"
|
||||||
></app-quote-result>
|
></app-quote-result>
|
||||||
} @else if (isZeroQuoteError()) {
|
} @else if (isZeroQuoteError()) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { LanguageService } from '../../core/services/language.service';
|
|||||||
styleUrl: './calculator-page.component.scss',
|
styleUrl: './calculator-page.component.scss',
|
||||||
})
|
})
|
||||||
export class CalculatorPageComponent implements OnInit {
|
export class CalculatorPageComponent implements OnInit {
|
||||||
mode = signal<any>('easy');
|
mode = signal<'easy' | 'advanced'>('easy');
|
||||||
step = signal<'upload' | 'quote' | 'details' | 'success'>('upload');
|
step = signal<'upload' | 'quote' | 'details' | 'success'>('upload');
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
@@ -56,6 +56,17 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
orderSuccess = signal(false);
|
orderSuccess = signal(false);
|
||||||
|
requiresRecalculation = signal(false);
|
||||||
|
private baselinePrintSettings: {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
||||||
@ViewChild('resultCol') resultCol!: ElementRef;
|
@ViewChild('resultCol') resultCol!: ElementRef;
|
||||||
@@ -101,6 +112,10 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.error.set(false);
|
this.error.set(false);
|
||||||
this.errorKey.set('CALC.ERROR_GENERIC');
|
this.errorKey.set('CALC.ERROR_GENERIC');
|
||||||
this.result.set(result);
|
this.result.set(result);
|
||||||
|
this.baselinePrintSettings = this.toTrackedSettingsFromSession(
|
||||||
|
data.session,
|
||||||
|
);
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
|
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
|
||||||
this.cadSessionLocked.set(isCadSession);
|
this.cadSessionLocked.set(isCadSession);
|
||||||
this.step.set('quote');
|
this.step.set('quote');
|
||||||
@@ -238,6 +253,8 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.error.set(false);
|
this.error.set(false);
|
||||||
this.errorKey.set('CALC.ERROR_GENERIC');
|
this.errorKey.set('CALC.ERROR_GENERIC');
|
||||||
this.result.set(res);
|
this.result.set(res);
|
||||||
|
this.baselinePrintSettings = this.toTrackedSettingsFromRequest(req);
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.uploadProgress.set(100);
|
this.uploadProgress.set(100);
|
||||||
this.step.set('quote');
|
this.step.set('quote');
|
||||||
@@ -295,9 +312,10 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
index: number;
|
index: number;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
source?: 'left' | 'right';
|
||||||
}) {
|
}) {
|
||||||
// 1. Update local form for consistency (UI feedback)
|
// 1. Update local form for consistency (UI feedback)
|
||||||
if (this.uploadForm) {
|
if (event.source !== 'left' && this.uploadForm) {
|
||||||
this.uploadForm.updateItemQuantityByIndex(event.index, event.quantity);
|
this.uploadForm.updateItemQuantityByIndex(event.index, event.quantity);
|
||||||
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity);
|
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity);
|
||||||
}
|
}
|
||||||
@@ -340,6 +358,33 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUploadItemQuantityChange(event: {
|
||||||
|
index: number;
|
||||||
|
fileName: string;
|
||||||
|
quantity: number;
|
||||||
|
}) {
|
||||||
|
const resultItems = this.result()?.items || [];
|
||||||
|
const byIndex = resultItems[event.index];
|
||||||
|
const byName = resultItems.find((item) => item.fileName === event.fileName);
|
||||||
|
const id = byIndex?.id ?? byName?.id;
|
||||||
|
|
||||||
|
this.onItemChange({
|
||||||
|
...event,
|
||||||
|
id,
|
||||||
|
source: 'left',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onQuoteItemQuantityPreviewChange(event: {
|
||||||
|
index: number;
|
||||||
|
fileName: string;
|
||||||
|
quantity: number;
|
||||||
|
}) {
|
||||||
|
if (!this.uploadForm) return;
|
||||||
|
this.uploadForm.updateItemQuantityByIndex(event.index, event.quantity);
|
||||||
|
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
onSubmitOrder(orderData: any) {
|
onSubmitOrder(orderData: any) {
|
||||||
console.log('Order Submitted:', orderData);
|
console.log('Order Submitted:', orderData);
|
||||||
this.orderSuccess.set(true);
|
this.orderSuccess.set(true);
|
||||||
@@ -349,15 +394,37 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
onNewQuote() {
|
onNewQuote() {
|
||||||
this.step.set('upload');
|
this.step.set('upload');
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
|
this.baselinePrintSettings = null;
|
||||||
this.cadSessionLocked.set(false);
|
this.cadSessionLocked.set(false);
|
||||||
this.orderSuccess.set(false);
|
this.orderSuccess.set(false);
|
||||||
this.mode.set('easy'); // Reset to default
|
this.switchMode('easy'); // Reset to default and sync URL
|
||||||
}
|
}
|
||||||
|
|
||||||
private currentRequest: QuoteRequest | null = null;
|
private currentRequest: QuoteRequest | null = null;
|
||||||
|
|
||||||
|
onUploadPrintSettingsChange(settings: {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
}) {
|
||||||
|
if (!this.result()) return;
|
||||||
|
if (!this.baselinePrintSettings) return;
|
||||||
|
this.requiresRecalculation.set(
|
||||||
|
!this.sameTrackedSettings(this.baselinePrintSettings, settings),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onConsult() {
|
onConsult() {
|
||||||
if (!this.currentRequest) {
|
const currentFormRequest = this.uploadForm?.getCurrentRequestDraft();
|
||||||
|
const req = currentFormRequest ?? this.currentRequest;
|
||||||
|
|
||||||
|
if (!req) {
|
||||||
this.router.navigate([
|
this.router.navigate([
|
||||||
'/',
|
'/',
|
||||||
this.languageService.selectedLang(),
|
this.languageService.selectedLang(),
|
||||||
@@ -366,7 +433,6 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = this.currentRequest;
|
|
||||||
let details = `Richiesta Preventivo:\n`;
|
let details = `Richiesta Preventivo:\n`;
|
||||||
details += `- Materiale: ${req.material}\n`;
|
details += `- Materiale: ${req.material}\n`;
|
||||||
details += `- Qualità: ${req.quality}\n`;
|
details += `- Qualità: ${req.quality}\n`;
|
||||||
@@ -411,5 +477,120 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.errorKey.set(key);
|
this.errorKey.set(key);
|
||||||
this.error.set(true);
|
this.error.set(true);
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
|
this.baselinePrintSettings = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchMode(nextMode: 'easy' | 'advanced'): void {
|
||||||
|
if (this.cadSessionLocked()) return;
|
||||||
|
|
||||||
|
const targetPath = nextMode === 'easy' ? 'basic' : 'advanced';
|
||||||
|
const currentPath = this.route.snapshot.routeConfig?.path;
|
||||||
|
|
||||||
|
this.mode.set(nextMode);
|
||||||
|
|
||||||
|
if (currentPath === targetPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['..', targetPath], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParamsHandling: 'preserve',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toTrackedSettingsFromRequest(req: QuoteRequest): {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
mode: req.mode,
|
||||||
|
material: this.normalizeString(req.material || 'PLA'),
|
||||||
|
quality: this.normalizeString(req.quality || 'standard'),
|
||||||
|
nozzleDiameter: this.normalizeNumber(req.nozzleDiameter, 0.4, 2),
|
||||||
|
layerHeight: this.normalizeNumber(req.layerHeight, 0.2, 3),
|
||||||
|
infillDensity: this.normalizeNumber(req.infillDensity, 20, 2),
|
||||||
|
infillPattern: this.normalizeString(req.infillPattern || 'grid'),
|
||||||
|
supportEnabled: Boolean(req.supportEnabled),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toTrackedSettingsFromSession(session: any): {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
} {
|
||||||
|
const layer = this.normalizeNumber(session?.layerHeightMm, 0.2, 3);
|
||||||
|
return {
|
||||||
|
mode: this.mode(),
|
||||||
|
material: this.normalizeString(session?.materialCode || 'PLA'),
|
||||||
|
quality:
|
||||||
|
layer >= 0.24 ? 'draft' : layer <= 0.12 ? 'extra_fine' : 'standard',
|
||||||
|
nozzleDiameter: this.normalizeNumber(session?.nozzleDiameterMm, 0.4, 2),
|
||||||
|
layerHeight: layer,
|
||||||
|
infillDensity: this.normalizeNumber(session?.infillPercent, 20, 2),
|
||||||
|
infillPattern: this.normalizeString(session?.infillPattern || 'grid'),
|
||||||
|
supportEnabled: Boolean(session?.supportsEnabled),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sameTrackedSettings(
|
||||||
|
a: {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
},
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
a.mode === b.mode &&
|
||||||
|
a.material === this.normalizeString(b.material) &&
|
||||||
|
a.quality === this.normalizeString(b.quality) &&
|
||||||
|
Math.abs(a.nozzleDiameter - this.normalizeNumber(b.nozzleDiameter, 0.4, 2)) < 0.0001 &&
|
||||||
|
Math.abs(a.layerHeight - this.normalizeNumber(b.layerHeight, 0.2, 3)) < 0.0001 &&
|
||||||
|
Math.abs(a.infillDensity - this.normalizeNumber(b.infillDensity, 20, 2)) < 0.0001 &&
|
||||||
|
a.infillPattern === this.normalizeString(b.infillPattern) &&
|
||||||
|
a.supportEnabled === Boolean(b.supportEnabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeString(value: string): string {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeNumber(
|
||||||
|
value: unknown,
|
||||||
|
fallback: number,
|
||||||
|
decimals: number,
|
||||||
|
): number {
|
||||||
|
const numeric = Number(value);
|
||||||
|
const resolved = Number.isFinite(numeric) ? numeric : fallback;
|
||||||
|
const factor = 10 ** decimals;
|
||||||
|
return Math.round(resolved * factor) / factor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ export class UploadFormComponent implements OnInit {
|
|||||||
loading = input<boolean>(false);
|
loading = input<boolean>(false);
|
||||||
uploadProgress = input<number>(0);
|
uploadProgress = input<number>(0);
|
||||||
submitRequest = output<QuoteRequest>();
|
submitRequest = output<QuoteRequest>();
|
||||||
|
itemQuantityChange = output<{
|
||||||
|
index: number;
|
||||||
|
fileName: string;
|
||||||
|
quantity: number;
|
||||||
|
}>();
|
||||||
|
printSettingsChange = output<{
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
private estimator = inject(QuoteEstimatorService);
|
private estimator = inject(QuoteEstimatorService);
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
@@ -81,6 +96,8 @@ export class UploadFormComponent implements OnInit {
|
|||||||
|
|
||||||
// Store full material options to lookup variants/colors if needed later
|
// Store full material options to lookup variants/colors if needed later
|
||||||
private fullMaterialOptions: MaterialOption[] = [];
|
private fullMaterialOptions: MaterialOption[] = [];
|
||||||
|
private allLayerHeights: SimpleOption[] = [];
|
||||||
|
private layerHeightsByNozzle: Record<string, SimpleOption[]> = {};
|
||||||
private isPatchingSettings = false;
|
private isPatchingSettings = false;
|
||||||
|
|
||||||
// Computed variants for valid material
|
// Computed variants for valid material
|
||||||
@@ -141,6 +158,14 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (this.mode() !== 'easy' || this.isPatchingSettings) return;
|
if (this.mode() !== 'easy' || this.isPatchingSettings) return;
|
||||||
this.applyAdvancedPresetFromQuality(quality);
|
this.applyAdvancedPresetFromQuality(quality);
|
||||||
});
|
});
|
||||||
|
this.form.get('nozzleDiameter')?.valueChanges.subscribe((nozzle) => {
|
||||||
|
if (this.isPatchingSettings) return;
|
||||||
|
this.updateLayerHeightOptionsForNozzle(nozzle, true);
|
||||||
|
});
|
||||||
|
this.form.valueChanges.subscribe(() => {
|
||||||
|
if (this.isPatchingSettings) return;
|
||||||
|
this.emitPrintSettingsChange();
|
||||||
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
this.applySettingsLock(this.lockedSettings());
|
this.applySettingsLock(this.lockedSettings());
|
||||||
@@ -187,6 +212,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
|
|
||||||
const preset = presets[normalized] || presets['standard'];
|
const preset = presets[normalized] || presets['standard'];
|
||||||
this.form.patchValue(preset, { emitEvent: false });
|
this.form.patchValue(preset, { emitEvent: false });
|
||||||
|
this.updateLayerHeightOptionsForNozzle(preset.nozzleDiameter, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -204,9 +230,19 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.infillPatterns.set(
|
this.infillPatterns.set(
|
||||||
options.infillPatterns.map((p) => ({ label: p.label, value: p.id })),
|
options.infillPatterns.map((p) => ({ label: p.label, value: p.id })),
|
||||||
);
|
);
|
||||||
this.layerHeights.set(
|
this.allLayerHeights = options.layerHeights.map((l) => ({
|
||||||
options.layerHeights.map((l) => ({ label: l.label, value: l.value })),
|
label: l.label,
|
||||||
);
|
value: l.value,
|
||||||
|
}));
|
||||||
|
this.layerHeightsByNozzle = {};
|
||||||
|
(options.layerHeightsByNozzle || []).forEach((entry) => {
|
||||||
|
this.layerHeightsByNozzle[this.toNozzleKey(entry.nozzleDiameter)] =
|
||||||
|
entry.layerHeights.map((layer) => ({
|
||||||
|
label: layer.label,
|
||||||
|
value: layer.value,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
this.layerHeights.set(this.allLayerHeights);
|
||||||
this.nozzleDiameters.set(
|
this.nozzleDiameters.set(
|
||||||
options.nozzleDiameters.map((n) => ({
|
options.nozzleDiameters.map((n) => ({
|
||||||
label: n.label,
|
label: n.label,
|
||||||
@@ -231,6 +267,11 @@ export class UploadFormComponent implements OnInit {
|
|||||||
value: 'standard',
|
value: 'standard',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
this.allLayerHeights = [{ label: '0.20 mm', value: 0.2 }];
|
||||||
|
this.layerHeightsByNozzle = {
|
||||||
|
[this.toNozzleKey(0.4)]: this.allLayerHeights,
|
||||||
|
};
|
||||||
|
this.layerHeights.set(this.allLayerHeights);
|
||||||
this.nozzleDiameters.set([{ label: '0.4 mm', value: 0.4 }]);
|
this.nozzleDiameters.set([{ label: '0.4 mm', value: 0.4 }]);
|
||||||
this.setDefaults();
|
this.setDefaults();
|
||||||
},
|
},
|
||||||
@@ -240,7 +281,15 @@ export class UploadFormComponent implements OnInit {
|
|||||||
private setDefaults() {
|
private setDefaults() {
|
||||||
// Set Defaults if available
|
// Set Defaults if available
|
||||||
if (this.materials().length > 0 && !this.form.get('material')?.value) {
|
if (this.materials().length > 0 && !this.form.get('material')?.value) {
|
||||||
this.form.get('material')?.setValue(this.materials()[0].value);
|
const exactPla = this.materials().find(
|
||||||
|
(m) => typeof m.value === 'string' && m.value.toUpperCase() === 'PLA',
|
||||||
|
);
|
||||||
|
const anyPla = this.materials().find(
|
||||||
|
(m) =>
|
||||||
|
typeof m.value === 'string' && m.value.toUpperCase().startsWith('PLA'),
|
||||||
|
);
|
||||||
|
const preferredMaterial = exactPla ?? anyPla ?? this.materials()[0];
|
||||||
|
this.form.get('material')?.setValue(preferredMaterial.value);
|
||||||
}
|
}
|
||||||
if (this.qualities().length > 0 && !this.form.get('quality')?.value) {
|
if (this.qualities().length > 0 && !this.form.get('quality')?.value) {
|
||||||
// Try to find 'standard' or use first
|
// Try to find 'standard' or use first
|
||||||
@@ -255,18 +304,20 @@ export class UploadFormComponent implements OnInit {
|
|||||||
) {
|
) {
|
||||||
this.form.get('nozzleDiameter')?.setValue(0.4); // Prefer 0.4
|
this.form.get('nozzleDiameter')?.setValue(0.4); // Prefer 0.4
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
this.layerHeights().length > 0 &&
|
this.updateLayerHeightOptionsForNozzle(
|
||||||
!this.form.get('layerHeight')?.value
|
this.form.get('nozzleDiameter')?.value,
|
||||||
) {
|
true,
|
||||||
this.form.get('layerHeight')?.setValue(0.2); // Prefer 0.2
|
);
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
this.infillPatterns().length > 0 &&
|
this.infillPatterns().length > 0 &&
|
||||||
!this.form.get('infillPattern')?.value
|
!this.form.get('infillPattern')?.value
|
||||||
) {
|
) {
|
||||||
this.form.get('infillPattern')?.setValue(this.infillPatterns()[0].value);
|
this.form.get('infillPattern')?.setValue(this.infillPatterns()[0].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emitPrintSettingsChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilesDropped(newFiles: File[]) {
|
onFilesDropped(newFiles: File[]) {
|
||||||
@@ -369,7 +420,15 @@ export class UploadFormComponent implements OnInit {
|
|||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const parsed = parseInt(input.value, 10);
|
const parsed = parseInt(input.value, 10);
|
||||||
const quantity = Number.isFinite(parsed) ? parsed : 1;
|
const quantity = Number.isFinite(parsed) ? parsed : 1;
|
||||||
|
const currentItem = this.items()[index];
|
||||||
|
if (!currentItem) return;
|
||||||
|
const normalizedQty = this.normalizeQuantity(quantity);
|
||||||
this.updateItemQuantityByIndex(index, quantity);
|
this.updateItemQuantityByIndex(index, quantity);
|
||||||
|
this.itemQuantityChange.emit({
|
||||||
|
index,
|
||||||
|
fileName: currentItem.file.name,
|
||||||
|
quantity: normalizedQty,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateItemColor(
|
updateItemColor(
|
||||||
@@ -514,6 +573,11 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.isPatchingSettings = true;
|
this.isPatchingSettings = true;
|
||||||
this.form.patchValue(patch, { emitEvent: false });
|
this.form.patchValue(patch, { emitEvent: false });
|
||||||
this.isPatchingSettings = false;
|
this.isPatchingSettings = false;
|
||||||
|
this.updateLayerHeightOptionsForNozzle(
|
||||||
|
this.form.get('nozzleDiameter')?.value,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.emitPrintSettingsChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
@@ -561,6 +625,86 @@ export class UploadFormComponent implements OnInit {
|
|||||||
return (fileName || '').split(/[\\/]/).pop()?.trim().toLowerCase() ?? '';
|
return (fileName || '').split(/[\\/]/).pop()?.trim().toLowerCase() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateLayerHeightOptionsForNozzle(
|
||||||
|
nozzleValue: unknown,
|
||||||
|
preserveCurrent: boolean,
|
||||||
|
): void {
|
||||||
|
const key = this.toNozzleKey(nozzleValue);
|
||||||
|
const nozzleSpecific = this.layerHeightsByNozzle[key] || [];
|
||||||
|
const available =
|
||||||
|
nozzleSpecific.length > 0 ? nozzleSpecific : this.allLayerHeights;
|
||||||
|
this.layerHeights.set(available);
|
||||||
|
|
||||||
|
const control = this.form.get('layerHeight');
|
||||||
|
if (!control) return;
|
||||||
|
|
||||||
|
const currentValue = Number(control.value);
|
||||||
|
const currentAllowed = available.some(
|
||||||
|
(option) => Math.abs(Number(option.value) - currentValue) < 0.0001,
|
||||||
|
);
|
||||||
|
if (preserveCurrent && currentAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferred = available.find(
|
||||||
|
(option) => Math.abs(Number(option.value) - 0.2) < 0.0001,
|
||||||
|
);
|
||||||
|
const next = preferred ?? available[0];
|
||||||
|
if (next) {
|
||||||
|
control.setValue(next.value, { emitEvent: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toNozzleKey(value: unknown): string {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) return '';
|
||||||
|
return numeric.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentRequestDraft(): QuoteRequest | null {
|
||||||
|
if (this.items().length === 0) return null;
|
||||||
|
const raw = this.form.getRawValue();
|
||||||
|
return {
|
||||||
|
items: this.items(),
|
||||||
|
material: raw.material,
|
||||||
|
quality: raw.quality,
|
||||||
|
notes: raw.notes,
|
||||||
|
infillDensity: raw.infillDensity,
|
||||||
|
infillPattern: raw.infillPattern,
|
||||||
|
supportEnabled: raw.supportEnabled,
|
||||||
|
layerHeight: raw.layerHeight,
|
||||||
|
nozzleDiameter: raw.nozzleDiameter,
|
||||||
|
mode: this.mode(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPrintSettings(): {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
} {
|
||||||
|
const raw = this.form.getRawValue();
|
||||||
|
return {
|
||||||
|
mode: this.mode(),
|
||||||
|
material: String(raw.material || 'PLA'),
|
||||||
|
quality: String(raw.quality || 'standard'),
|
||||||
|
nozzleDiameter: Number(raw.nozzleDiameter ?? 0.4),
|
||||||
|
layerHeight: Number(raw.layerHeight ?? 0.2),
|
||||||
|
infillDensity: Number(raw.infillDensity ?? 20),
|
||||||
|
infillPattern: String(raw.infillPattern || 'grid'),
|
||||||
|
supportEnabled: Boolean(raw.supportEnabled),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitPrintSettingsChange(): void {
|
||||||
|
this.printSettingsChange.emit(this.getCurrentPrintSettings());
|
||||||
|
}
|
||||||
|
|
||||||
private applySettingsLock(locked: boolean): void {
|
private applySettingsLock(locked: boolean): void {
|
||||||
const controlsToLock = [
|
const controlsToLock = [
|
||||||
'material',
|
'material',
|
||||||
|
|||||||
@@ -102,12 +102,18 @@ export interface NumericOption {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NozzleLayerHeightsOption {
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeights: NumericOption[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface OptionsResponse {
|
export interface OptionsResponse {
|
||||||
materials: MaterialOption[];
|
materials: MaterialOption[];
|
||||||
qualities: QualityOption[];
|
qualities: QualityOption[];
|
||||||
infillPatterns: InfillOption[];
|
infillPatterns: InfillOption[];
|
||||||
layerHeights: NumericOption[];
|
layerHeights: NumericOption[];
|
||||||
nozzleDiameters: NumericOption[];
|
nozzleDiameters: NumericOption[];
|
||||||
|
layerHeightsByNozzle?: NozzleLayerHeightsOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI Option for Select Component
|
// UI Option for Select Component
|
||||||
|
|||||||
Reference in New Issue
Block a user