produzione 1 #9
@@ -3,18 +3,27 @@ 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.*; // This line replaces specific entity imports
|
import com.printcalculator.entity.LayerHeightOption;
|
||||||
|
import com.printcalculator.entity.MaterialOrcaProfileMap;
|
||||||
|
import com.printcalculator.entity.NozzleOption;
|
||||||
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
|
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.LayerHeightOptionRepository;
|
||||||
|
import com.printcalculator.repository.MaterialOrcaProfileMapRepository;
|
||||||
import com.printcalculator.repository.NozzleOptionRepository;
|
import com.printcalculator.repository.NozzleOptionRepository;
|
||||||
|
import com.printcalculator.repository.PrinterMachineRepository;
|
||||||
|
import com.printcalculator.service.OrcaProfileResolver;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.math.BigDecimal;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -24,74 +33,99 @@ public class OptionsController {
|
|||||||
private final FilamentVariantRepository variantRepo;
|
private final FilamentVariantRepository variantRepo;
|
||||||
private final LayerHeightOptionRepository layerHeightRepo;
|
private final LayerHeightOptionRepository layerHeightRepo;
|
||||||
private final NozzleOptionRepository nozzleRepo;
|
private final NozzleOptionRepository nozzleRepo;
|
||||||
|
private final PrinterMachineRepository printerMachineRepo;
|
||||||
|
private final MaterialOrcaProfileMapRepository materialOrcaMapRepo;
|
||||||
|
private final OrcaProfileResolver orcaProfileResolver;
|
||||||
|
|
||||||
public OptionsController(FilamentMaterialTypeRepository materialRepo,
|
public OptionsController(FilamentMaterialTypeRepository materialRepo,
|
||||||
FilamentVariantRepository variantRepo,
|
FilamentVariantRepository variantRepo,
|
||||||
LayerHeightOptionRepository layerHeightRepo,
|
LayerHeightOptionRepository layerHeightRepo,
|
||||||
NozzleOptionRepository nozzleRepo) {
|
NozzleOptionRepository nozzleRepo,
|
||||||
|
PrinterMachineRepository printerMachineRepo,
|
||||||
|
MaterialOrcaProfileMapRepository materialOrcaMapRepo,
|
||||||
|
OrcaProfileResolver orcaProfileResolver) {
|
||||||
this.materialRepo = materialRepo;
|
this.materialRepo = materialRepo;
|
||||||
this.variantRepo = variantRepo;
|
this.variantRepo = variantRepo;
|
||||||
this.layerHeightRepo = layerHeightRepo;
|
this.layerHeightRepo = layerHeightRepo;
|
||||||
this.nozzleRepo = nozzleRepo;
|
this.nozzleRepo = nozzleRepo;
|
||||||
|
this.printerMachineRepo = printerMachineRepo;
|
||||||
|
this.materialOrcaMapRepo = materialOrcaMapRepo;
|
||||||
|
this.orcaProfileResolver = orcaProfileResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/api/calculator/options")
|
@GetMapping("/api/calculator/options")
|
||||||
public ResponseEntity<OptionsResponse> getOptions() {
|
public ResponseEntity<OptionsResponse> getOptions(
|
||||||
// 1. Materials & Variants
|
@RequestParam(value = "printerMachineId", required = false) Long printerMachineId,
|
||||||
|
@RequestParam(value = "nozzleDiameter", required = false) Double nozzleDiameter
|
||||||
|
) {
|
||||||
List<FilamentMaterialType> types = materialRepo.findAll();
|
List<FilamentMaterialType> types = materialRepo.findAll();
|
||||||
List<FilamentVariant> allVariants = variantRepo.findAll();
|
List<FilamentVariant> allVariants = variantRepo.findAll().stream()
|
||||||
|
.filter(v -> Boolean.TRUE.equals(v.getIsActive()))
|
||||||
|
.sorted(Comparator
|
||||||
|
.comparing((FilamentVariant v) -> safeMaterialCode(v.getFilamentMaterialType()), String.CASE_INSENSITIVE_ORDER)
|
||||||
|
.thenComparing(v -> safeString(v.getVariantDisplayName()), String.CASE_INSENSITIVE_ORDER))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Set<Long> compatibleMaterialTypeIds = resolveCompatibleMaterialTypeIds(printerMachineId, nozzleDiameter);
|
||||||
|
|
||||||
List<OptionsResponse.MaterialOption> materialOptions = types.stream()
|
List<OptionsResponse.MaterialOption> materialOptions = types.stream()
|
||||||
|
.sorted(Comparator.comparing(t -> safeString(t.getMaterialCode()), String.CASE_INSENSITIVE_ORDER))
|
||||||
.map(type -> {
|
.map(type -> {
|
||||||
|
if (!compatibleMaterialTypeIds.isEmpty() && !compatibleMaterialTypeIds.contains(type.getId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
List<OptionsResponse.VariantOption> variants = allVariants.stream()
|
List<OptionsResponse.VariantOption> variants = allVariants.stream()
|
||||||
.filter(v -> v.getFilamentMaterialType().getId().equals(type.getId()) && v.getIsActive())
|
.filter(v -> v.getFilamentMaterialType() != null
|
||||||
|
&& v.getFilamentMaterialType().getId().equals(type.getId()))
|
||||||
.map(v -> new OptionsResponse.VariantOption(
|
.map(v -> new OptionsResponse.VariantOption(
|
||||||
|
v.getId(),
|
||||||
v.getVariantDisplayName(),
|
v.getVariantDisplayName(),
|
||||||
v.getColorName(),
|
v.getColorName(),
|
||||||
getColorHex(v.getColorName()), // Need helper or store hex in DB
|
resolveHexColor(v),
|
||||||
v.getStockSpools().doubleValue() <= 0
|
v.getFinishType() != null ? v.getFinishType() : "GLOSSY",
|
||||||
|
v.getStockSpools() != null ? v.getStockSpools().doubleValue() : 0d,
|
||||||
|
toStockFilamentGrams(v),
|
||||||
|
v.getStockSpools() == null || v.getStockSpools().doubleValue() <= 0
|
||||||
))
|
))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// Only include material if it has active variants
|
if (variants.isEmpty()) {
|
||||||
if (variants.isEmpty()) return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new OptionsResponse.MaterialOption(
|
return new OptionsResponse.MaterialOption(
|
||||||
type.getMaterialCode(),
|
type.getMaterialCode(),
|
||||||
type.getMaterialCode() + (type.getIsFlexible() ? " (Flexible)" : " (Standard)"),
|
type.getMaterialCode() + (Boolean.TRUE.equals(type.getIsFlexible()) ? " (Flexible)" : " (Standard)"),
|
||||||
variants
|
variants
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter(m -> m != null)
|
.filter(m -> m != null)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
// 2. Qualities (Static as per user request)
|
|
||||||
List<OptionsResponse.QualityOption> qualities = List.of(
|
List<OptionsResponse.QualityOption> qualities = List.of(
|
||||||
new OptionsResponse.QualityOption("draft", "Draft"),
|
new OptionsResponse.QualityOption("draft", "Draft"),
|
||||||
new OptionsResponse.QualityOption("standard", "Standard"),
|
new OptionsResponse.QualityOption("standard", "Standard"),
|
||||||
new OptionsResponse.QualityOption("extra_fine", "High Definition")
|
new OptionsResponse.QualityOption("extra_fine", "High Definition")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Infill Patterns (Static as per user request)
|
|
||||||
List<OptionsResponse.InfillPatternOption> patterns = List.of(
|
List<OptionsResponse.InfillPatternOption> patterns = List.of(
|
||||||
new OptionsResponse.InfillPatternOption("grid", "Grid"),
|
new OptionsResponse.InfillPatternOption("grid", "Grid"),
|
||||||
new OptionsResponse.InfillPatternOption("gyroid", "Gyroid"),
|
new OptionsResponse.InfillPatternOption("gyroid", "Gyroid"),
|
||||||
new OptionsResponse.InfillPatternOption("cubic", "Cubic")
|
new OptionsResponse.InfillPatternOption("cubic", "Cubic")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Layer Heights
|
|
||||||
List<OptionsResponse.LayerHeightOptionDTO> layers = layerHeightRepo.findAll().stream()
|
List<OptionsResponse.LayerHeightOptionDTO> layers = layerHeightRepo.findAll().stream()
|
||||||
.filter(l -> l.getIsActive())
|
.filter(l -> Boolean.TRUE.equals(l.getIsActive()))
|
||||||
.sorted(Comparator.comparing(LayerHeightOption::getLayerHeightMm))
|
.sorted(Comparator.comparing(LayerHeightOption::getLayerHeightMm))
|
||||||
.map(l -> new OptionsResponse.LayerHeightOptionDTO(
|
.map(l -> new OptionsResponse.LayerHeightOptionDTO(
|
||||||
l.getLayerHeightMm().doubleValue(),
|
l.getLayerHeightMm().doubleValue(),
|
||||||
String.format("%.2f mm", l.getLayerHeightMm())
|
String.format("%.2f mm", l.getLayerHeightMm())
|
||||||
))
|
))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
// 5. Nozzles
|
|
||||||
List<OptionsResponse.NozzleOptionDTO> nozzles = nozzleRepo.findAll().stream()
|
List<OptionsResponse.NozzleOptionDTO> nozzles = nozzleRepo.findAll().stream()
|
||||||
.filter(n -> n.getIsActive())
|
.filter(n -> Boolean.TRUE.equals(n.getIsActive()))
|
||||||
.sorted(Comparator.comparing(NozzleOption::getNozzleDiameterMm))
|
.sorted(Comparator.comparing(NozzleOption::getNozzleDiameterMm))
|
||||||
.map(n -> new OptionsResponse.NozzleOptionDTO(
|
.map(n -> new OptionsResponse.NozzleOptionDTO(
|
||||||
n.getNozzleDiameterMm().doubleValue(),
|
n.getNozzleDiameterMm().doubleValue(),
|
||||||
@@ -100,13 +134,76 @@ public class OptionsController {
|
|||||||
? String.format(" (+ %.2f CHF)", n.getExtraNozzleChangeFeeChf())
|
? String.format(" (+ %.2f CHF)", n.getExtraNozzleChangeFeeChf())
|
||||||
: " (Standard)")
|
: " (Standard)")
|
||||||
))
|
))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(new OptionsResponse(materialOptions, qualities, patterns, layers, nozzles));
|
return ResponseEntity.ok(new OptionsResponse(materialOptions, qualities, patterns, layers, nozzles));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary helper until we add hex to DB
|
private Set<Long> resolveCompatibleMaterialTypeIds(Long printerMachineId, Double nozzleDiameter) {
|
||||||
|
PrinterMachine machine = null;
|
||||||
|
if (printerMachineId != null) {
|
||||||
|
machine = printerMachineRepo.findById(printerMachineId).orElse(null);
|
||||||
|
}
|
||||||
|
if (machine == null) {
|
||||||
|
machine = printerMachineRepo.findFirstByIsActiveTrue().orElse(null);
|
||||||
|
}
|
||||||
|
if (machine == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal nozzle = nozzleDiameter != null
|
||||||
|
? BigDecimal.valueOf(nozzleDiameter)
|
||||||
|
: BigDecimal.valueOf(0.40);
|
||||||
|
|
||||||
|
PrinterMachineProfile machineProfile = orcaProfileResolver
|
||||||
|
.resolveMachineProfile(machine, nozzle)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (machineProfile == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MaterialOrcaProfileMap> maps = materialOrcaMapRepo.findByPrinterMachineProfileAndIsActiveTrue(machineProfile);
|
||||||
|
return maps.stream()
|
||||||
|
.map(MaterialOrcaProfileMap::getFilamentMaterialType)
|
||||||
|
.filter(m -> m != null && m.getId() != null)
|
||||||
|
.map(FilamentMaterialType::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveHexColor(FilamentVariant variant) {
|
||||||
|
if (variant.getColorHex() != null && !variant.getColorHex().isBlank()) {
|
||||||
|
return variant.getColorHex();
|
||||||
|
}
|
||||||
|
return getColorHex(variant.getColorName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double toStockFilamentGrams(FilamentVariant variant) {
|
||||||
|
if (variant.getStockSpools() == null || variant.getSpoolNetKg() == null) {
|
||||||
|
return 0d;
|
||||||
|
}
|
||||||
|
return variant.getStockSpools()
|
||||||
|
.multiply(variant.getSpoolNetKg())
|
||||||
|
.multiply(BigDecimal.valueOf(1000))
|
||||||
|
.doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeMaterialCode(FilamentMaterialType type) {
|
||||||
|
if (type == null || type.getMaterialCode() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return type.getMaterialCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeString(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary helper for legacy values where color hex is not yet set in DB
|
||||||
private String getColorHex(String colorName) {
|
private String getColorHex(String colorName) {
|
||||||
|
if (colorName == null) {
|
||||||
|
return "#9e9e9e";
|
||||||
|
}
|
||||||
String lower = colorName.toLowerCase();
|
String lower = colorName.toLowerCase();
|
||||||
if (lower.contains("black") || lower.contains("nero")) return "#1a1a1a";
|
if (lower.contains("black") || lower.contains("nero")) return "#1a1a1a";
|
||||||
if (lower.contains("white") || lower.contains("bianco")) return "#f5f5f5";
|
if (lower.contains("white") || lower.contains("bianco")) return "#f5f5f5";
|
||||||
@@ -120,6 +217,6 @@ public class OptionsController {
|
|||||||
}
|
}
|
||||||
if (lower.contains("purple") || lower.contains("viola")) return "#7b1fa2";
|
if (lower.contains("purple") || lower.contains("viola")) return "#7b1fa2";
|
||||||
if (lower.contains("yellow") || lower.contains("giallo")) return "#fbc02d";
|
if (lower.contains("yellow") || lower.contains("giallo")) return "#fbc02d";
|
||||||
return "#9e9e9e"; // Default grey
|
return "#9e9e9e";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package com.printcalculator.controller;
|
package com.printcalculator.controller;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.FilamentMaterialType;
|
||||||
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
import com.printcalculator.entity.PrinterMachine;
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
import com.printcalculator.entity.QuoteLineItem;
|
import com.printcalculator.entity.QuoteLineItem;
|
||||||
import com.printcalculator.entity.QuoteSession;
|
import com.printcalculator.entity.QuoteSession;
|
||||||
import com.printcalculator.model.ModelDimensions;
|
import com.printcalculator.model.ModelDimensions;
|
||||||
import com.printcalculator.model.PrintStats;
|
import com.printcalculator.model.PrintStats;
|
||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
|
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||||
|
import com.printcalculator.repository.FilamentVariantRepository;
|
||||||
import com.printcalculator.repository.PrinterMachineRepository;
|
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.QuoteCalculator;
|
import com.printcalculator.service.QuoteCalculator;
|
||||||
import com.printcalculator.service.SlicerService;
|
import com.printcalculator.service.SlicerService;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -43,18 +48,20 @@ public class QuoteSessionController {
|
|||||||
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 FilamentMaterialTypeRepository materialRepo;
|
||||||
|
private final FilamentVariantRepository variantRepo;
|
||||||
|
private final OrcaProfileResolver orcaProfileResolver;
|
||||||
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
||||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||||
|
|
||||||
// Defaults
|
|
||||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
|
||||||
private static final String DEFAULT_PROCESS = "standard";
|
|
||||||
|
|
||||||
public QuoteSessionController(QuoteSessionRepository sessionRepo,
|
public QuoteSessionController(QuoteSessionRepository sessionRepo,
|
||||||
QuoteLineItemRepository lineItemRepo,
|
QuoteLineItemRepository lineItemRepo,
|
||||||
SlicerService slicerService,
|
SlicerService slicerService,
|
||||||
QuoteCalculator quoteCalculator,
|
QuoteCalculator quoteCalculator,
|
||||||
PrinterMachineRepository machineRepo,
|
PrinterMachineRepository machineRepo,
|
||||||
|
FilamentMaterialTypeRepository materialRepo,
|
||||||
|
FilamentVariantRepository variantRepo,
|
||||||
|
OrcaProfileResolver orcaProfileResolver,
|
||||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
||||||
com.printcalculator.service.ClamAVService clamAVService) {
|
com.printcalculator.service.ClamAVService clamAVService) {
|
||||||
this.sessionRepo = sessionRepo;
|
this.sessionRepo = sessionRepo;
|
||||||
@@ -62,6 +69,9 @@ public class QuoteSessionController {
|
|||||||
this.slicerService = slicerService;
|
this.slicerService = slicerService;
|
||||||
this.quoteCalculator = quoteCalculator;
|
this.quoteCalculator = quoteCalculator;
|
||||||
this.machineRepo = machineRepo;
|
this.machineRepo = machineRepo;
|
||||||
|
this.materialRepo = materialRepo;
|
||||||
|
this.variantRepo = variantRepo;
|
||||||
|
this.orcaProfileResolver = orcaProfileResolver;
|
||||||
this.pricingRepo = pricingRepo;
|
this.pricingRepo = pricingRepo;
|
||||||
this.clamAVService = clamAVService;
|
this.clamAVService = clamAVService;
|
||||||
}
|
}
|
||||||
@@ -129,45 +139,31 @@ public class QuoteSessionController {
|
|||||||
// Apply Basic/Advanced Logic
|
// Apply Basic/Advanced Logic
|
||||||
applyPrintSettings(settings);
|
applyPrintSettings(settings);
|
||||||
|
|
||||||
|
BigDecimal nozzleDiameter = BigDecimal.valueOf(settings.getNozzleDiameter() != null ? settings.getNozzleDiameter() : 0.4);
|
||||||
|
|
||||||
|
// Pick machine (selected machine if provided, otherwise first active)
|
||||||
|
PrinterMachine machine = resolvePrinterMachine(settings.getPrinterMachineId());
|
||||||
|
|
||||||
|
// Resolve selected filament variant
|
||||||
|
FilamentVariant selectedVariant = resolveFilamentVariant(settings);
|
||||||
|
|
||||||
// Update session global settings from the most recent item added
|
// Update session global settings from the most recent item added
|
||||||
session.setMaterialCode(settings.getMaterial());
|
session.setMaterialCode(selectedVariant.getFilamentMaterialType().getMaterialCode());
|
||||||
session.setNozzleDiameterMm(BigDecimal.valueOf(settings.getNozzleDiameter() != null ? settings.getNozzleDiameter() : 0.4));
|
session.setNozzleDiameterMm(nozzleDiameter);
|
||||||
session.setLayerHeightMm(BigDecimal.valueOf(settings.getLayerHeight() != null ? settings.getLayerHeight() : 0.2));
|
session.setLayerHeightMm(BigDecimal.valueOf(settings.getLayerHeight() != null ? settings.getLayerHeight() : 0.2));
|
||||||
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);
|
||||||
sessionRepo.save(session);
|
sessionRepo.save(session);
|
||||||
|
|
||||||
// REAL SLICING
|
OrcaProfileResolver.ResolvedProfiles profiles = orcaProfileResolver.resolve(machine, nozzleDiameter, selectedVariant);
|
||||||
// 1. Pick Machine (default to first active or specific)
|
String machineProfile = profiles.machineProfileName();
|
||||||
PrinterMachine machine = machineRepo.findFirstByIsActiveTrue()
|
String filamentProfile = profiles.filamentProfileName();
|
||||||
.orElseThrow(() -> new RuntimeException("No active printer found"));
|
|
||||||
|
|
||||||
// 2. Pick Profiles
|
String processProfile = "standard";
|
||||||
String machineProfile = machine.getPrinterDisplayName(); // e.g. "Bambu Lab A1 0.4 nozzle"
|
|
||||||
// If the display name doesn't match the json profile name, we might need a mapping key in DB.
|
|
||||||
// For now assuming display name works or we use a tough default
|
|
||||||
machineProfile = "Bambu Lab A1 0.4 nozzle"; // Force known good for now? Or use DB field if exists.
|
|
||||||
// Ideally: machine.getSlicerProfileName();
|
|
||||||
|
|
||||||
String filamentProfile = "Generic " + (settings.getMaterial() != null ? settings.getMaterial().toUpperCase() : "PLA");
|
|
||||||
// Mapping: "pla_basic" -> "Generic PLA", "petg_basic" -> "Generic PETG"
|
|
||||||
if (settings.getMaterial() != null) {
|
|
||||||
if (settings.getMaterial().toLowerCase().contains("pla")) filamentProfile = "Generic PLA";
|
|
||||||
else if (settings.getMaterial().toLowerCase().contains("petg")) filamentProfile = "Generic PETG";
|
|
||||||
else if (settings.getMaterial().toLowerCase().contains("tpu")) filamentProfile = "Generic TPU";
|
|
||||||
else if (settings.getMaterial().toLowerCase().contains("abs")) filamentProfile = "Generic ABS";
|
|
||||||
}
|
|
||||||
|
|
||||||
String processProfile = "0.20mm Standard @BBL A1";
|
|
||||||
// Mapping quality to process
|
|
||||||
// "standard" -> "0.20mm Standard @BBL A1"
|
|
||||||
// "draft" -> "0.28mm Extra Draft @BBL A1"
|
|
||||||
// "high" -> "0.12mm Fine @BBL A1" (approx names, need to be exact for Orca)
|
|
||||||
// Let's use robust defaults or simple overrides
|
|
||||||
if (settings.getLayerHeight() != null) {
|
if (settings.getLayerHeight() != null) {
|
||||||
if (settings.getLayerHeight() >= 0.28) processProfile = "0.28mm Extra Draft @BBL A1";
|
if (settings.getLayerHeight() >= 0.28) processProfile = "draft";
|
||||||
else if (settings.getLayerHeight() <= 0.12) processProfile = "0.12mm Fine @BBL A1";
|
else if (settings.getLayerHeight() <= 0.12) processProfile = "extra_fine";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build overrides map from settings
|
// Build overrides map from settings
|
||||||
@@ -189,7 +185,7 @@ public class QuoteSessionController {
|
|||||||
Optional<ModelDimensions> modelDimensions = slicerService.inspectModelDimensions(persistentPath.toFile());
|
Optional<ModelDimensions> modelDimensions = slicerService.inspectModelDimensions(persistentPath.toFile());
|
||||||
|
|
||||||
// 4. Calculate Quote
|
// 4. Calculate Quote
|
||||||
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile);
|
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), selectedVariant);
|
||||||
|
|
||||||
// 5. Create Line Item
|
// 5. Create Line Item
|
||||||
QuoteLineItem item = new QuoteLineItem();
|
QuoteLineItem item = new QuoteLineItem();
|
||||||
@@ -197,7 +193,8 @@ public class QuoteSessionController {
|
|||||||
item.setOriginalFilename(file.getOriginalFilename());
|
item.setOriginalFilename(file.getOriginalFilename());
|
||||||
item.setStoredPath(persistentPath.toString()); // SAVE PATH
|
item.setStoredPath(persistentPath.toString()); // SAVE PATH
|
||||||
item.setQuantity(1);
|
item.setQuantity(1);
|
||||||
item.setColorCode(settings.getColor() != null ? settings.getColor() : "#FFFFFF");
|
item.setColorCode(selectedVariant.getColorName());
|
||||||
|
item.setFilamentVariant(selectedVariant);
|
||||||
item.setStatus("READY"); // or CALCULATED
|
item.setStatus("READY"); // or CALCULATED
|
||||||
|
|
||||||
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
|
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
|
||||||
@@ -264,6 +261,50 @@ public class QuoteSessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PrinterMachine resolvePrinterMachine(Long printerMachineId) {
|
||||||
|
if (printerMachineId != null) {
|
||||||
|
PrinterMachine selected = machineRepo.findById(printerMachineId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Printer machine not found: " + printerMachineId));
|
||||||
|
if (!Boolean.TRUE.equals(selected.getIsActive())) {
|
||||||
|
throw new RuntimeException("Selected printer machine is not active");
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineRepo.findFirstByIsActiveTrue()
|
||||||
|
.orElseThrow(() -> new RuntimeException("No active printer found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilamentVariant resolveFilamentVariant(com.printcalculator.dto.PrintSettingsDto settings) {
|
||||||
|
if (settings.getFilamentVariantId() != null) {
|
||||||
|
FilamentVariant variant = variantRepo.findById(settings.getFilamentVariantId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Filament variant not found: " + settings.getFilamentVariantId()));
|
||||||
|
if (!Boolean.TRUE.equals(variant.getIsActive())) {
|
||||||
|
throw new RuntimeException("Selected filament variant is not active");
|
||||||
|
}
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
String requestedMaterialCode = settings.getMaterial() != null
|
||||||
|
? settings.getMaterial().trim().toUpperCase()
|
||||||
|
: "PLA";
|
||||||
|
|
||||||
|
FilamentMaterialType materialType = materialRepo.findByMaterialCode(requestedMaterialCode)
|
||||||
|
.orElseGet(() -> materialRepo.findByMaterialCode("PLA")
|
||||||
|
.orElseThrow(() -> new RuntimeException("Fallback material PLA not configured")));
|
||||||
|
|
||||||
|
String requestedColor = settings.getColor() != null ? settings.getColor().trim() : null;
|
||||||
|
if (requestedColor != null && !requestedColor.isBlank()) {
|
||||||
|
Optional<FilamentVariant> byColor = variantRepo.findByFilamentMaterialTypeAndColorName(materialType, requestedColor);
|
||||||
|
if (byColor.isPresent() && Boolean.TRUE.equals(byColor.get().getIsActive())) {
|
||||||
|
return byColor.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantRepo.findFirstByFilamentMaterialTypeAndIsActiveTrue(materialType)
|
||||||
|
.orElseThrow(() -> new RuntimeException("No active variant for material: " + requestedMaterialCode));
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Update Line Item
|
// 3. Update Line Item
|
||||||
@PatchMapping("/line-items/{lineItemId}")
|
@PatchMapping("/line-items/{lineItemId}")
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -344,6 +385,7 @@ public class QuoteSessionController {
|
|||||||
dto.put("printTimeSeconds", item.getPrintTimeSeconds());
|
dto.put("printTimeSeconds", item.getPrintTimeSeconds());
|
||||||
dto.put("materialGrams", item.getMaterialGrams());
|
dto.put("materialGrams", item.getMaterialGrams());
|
||||||
dto.put("colorCode", item.getColorCode());
|
dto.put("colorCode", item.getColorCode());
|
||||||
|
dto.put("filamentVariantId", item.getFilamentVariant() != null ? item.getFilamentVariant().getId() : null);
|
||||||
dto.put("status", item.getStatus());
|
dto.put("status", item.getStatus());
|
||||||
|
|
||||||
BigDecimal unitPrice = item.getUnitPriceChf();
|
BigDecimal unitPrice = item.getUnitPriceChf();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.printcalculator.entity.FilamentMaterialType;
|
|||||||
import com.printcalculator.entity.FilamentVariant;
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||||
import com.printcalculator.repository.FilamentVariantRepository;
|
import com.printcalculator.repository.FilamentVariantRepository;
|
||||||
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
|
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -17,8 +19,12 @@ import java.math.BigDecimal;
|
|||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -26,16 +32,26 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public class AdminFilamentController {
|
public class AdminFilamentController {
|
||||||
private static final BigDecimal MAX_NUMERIC_6_3 = new BigDecimal("999.999");
|
private static final BigDecimal MAX_NUMERIC_6_3 = new BigDecimal("999.999");
|
||||||
|
private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("^#[0-9A-Fa-f]{6}$");
|
||||||
|
private static final Set<String> ALLOWED_FINISH_TYPES = Set.of(
|
||||||
|
"GLOSSY", "MATTE", "MARBLE", "SILK", "TRANSLUCENT", "SPECIAL"
|
||||||
|
);
|
||||||
|
|
||||||
private final FilamentMaterialTypeRepository materialRepo;
|
private final FilamentMaterialTypeRepository materialRepo;
|
||||||
private final FilamentVariantRepository variantRepo;
|
private final FilamentVariantRepository variantRepo;
|
||||||
|
private final QuoteLineItemRepository quoteLineItemRepo;
|
||||||
|
private final OrderItemRepository orderItemRepo;
|
||||||
|
|
||||||
public AdminFilamentController(
|
public AdminFilamentController(
|
||||||
FilamentMaterialTypeRepository materialRepo,
|
FilamentMaterialTypeRepository materialRepo,
|
||||||
FilamentVariantRepository variantRepo
|
FilamentVariantRepository variantRepo,
|
||||||
|
QuoteLineItemRepository quoteLineItemRepo,
|
||||||
|
OrderItemRepository orderItemRepo
|
||||||
) {
|
) {
|
||||||
this.materialRepo = materialRepo;
|
this.materialRepo = materialRepo;
|
||||||
this.variantRepo = variantRepo;
|
this.variantRepo = variantRepo;
|
||||||
|
this.quoteLineItemRepo = quoteLineItemRepo;
|
||||||
|
this.orderItemRepo = orderItemRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/materials")
|
@GetMapping("/materials")
|
||||||
@@ -130,6 +146,20 @@ public class AdminFilamentController {
|
|||||||
return ResponseEntity.ok(toVariantDto(saved));
|
return ResponseEntity.ok(toVariantDto(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/variants/{variantId}")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> deleteVariant(@PathVariable Long variantId) {
|
||||||
|
FilamentVariant variant = variantRepo.findById(variantId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Filament variant not found"));
|
||||||
|
|
||||||
|
if (quoteLineItemRepo.existsByFilamentVariant_Id(variantId) || orderItemRepo.existsByFilamentVariant_Id(variantId)) {
|
||||||
|
throw new ResponseStatusException(CONFLICT, "Variant is already used in quotes/orders and cannot be deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
variantRepo.delete(variant);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
private void applyMaterialPayload(
|
private void applyMaterialPayload(
|
||||||
FilamentMaterialType material,
|
FilamentMaterialType material,
|
||||||
AdminUpsertFilamentMaterialTypeRequest payload,
|
AdminUpsertFilamentMaterialTypeRequest payload,
|
||||||
@@ -156,10 +186,17 @@ public class AdminFilamentController {
|
|||||||
String normalizedDisplayName,
|
String normalizedDisplayName,
|
||||||
String normalizedColorName
|
String normalizedColorName
|
||||||
) {
|
) {
|
||||||
|
String normalizedColorHex = normalizeAndValidateColorHex(payload.getColorHex());
|
||||||
|
String normalizedFinishType = normalizeAndValidateFinishType(payload.getFinishType(), payload.getIsMatte());
|
||||||
|
String normalizedBrand = normalizeOptional(payload.getBrand());
|
||||||
|
|
||||||
variant.setFilamentMaterialType(material);
|
variant.setFilamentMaterialType(material);
|
||||||
variant.setVariantDisplayName(normalizedDisplayName);
|
variant.setVariantDisplayName(normalizedDisplayName);
|
||||||
variant.setColorName(normalizedColorName);
|
variant.setColorName(normalizedColorName);
|
||||||
variant.setIsMatte(Boolean.TRUE.equals(payload.getIsMatte()));
|
variant.setColorHex(normalizedColorHex);
|
||||||
|
variant.setFinishType(normalizedFinishType);
|
||||||
|
variant.setBrand(normalizedBrand);
|
||||||
|
variant.setIsMatte(Boolean.TRUE.equals(payload.getIsMatte()) || "MATTE".equals(normalizedFinishType));
|
||||||
variant.setIsSpecial(Boolean.TRUE.equals(payload.getIsSpecial()));
|
variant.setIsSpecial(Boolean.TRUE.equals(payload.getIsSpecial()));
|
||||||
variant.setCostChfPerKg(payload.getCostChfPerKg());
|
variant.setCostChfPerKg(payload.getCostChfPerKg());
|
||||||
variant.setStockSpools(payload.getStockSpools());
|
variant.setStockSpools(payload.getStockSpools());
|
||||||
@@ -188,6 +225,35 @@ public class AdminFilamentController {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeAndValidateColorHex(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim();
|
||||||
|
if (!HEX_COLOR_PATTERN.matcher(normalized).matches()) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Color hex must be in format #RRGGBB");
|
||||||
|
}
|
||||||
|
return normalized.toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeAndValidateFinishType(String finishType, Boolean isMatte) {
|
||||||
|
String normalized = finishType == null || finishType.isBlank()
|
||||||
|
? (Boolean.TRUE.equals(isMatte) ? "MATTE" : "GLOSSY")
|
||||||
|
: finishType.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (!ALLOWED_FINISH_TYPES.contains(normalized)) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Invalid finish type");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOptional(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.isBlank() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
private FilamentMaterialType validateAndResolveMaterial(AdminUpsertFilamentVariantRequest payload) {
|
private FilamentMaterialType validateAndResolveMaterial(AdminUpsertFilamentVariantRequest payload) {
|
||||||
if (payload == null || payload.getMaterialTypeId() == null) {
|
if (payload == null || payload.getMaterialTypeId() == null) {
|
||||||
throw new ResponseStatusException(BAD_REQUEST, "Material type id is required");
|
throw new ResponseStatusException(BAD_REQUEST, "Material type id is required");
|
||||||
@@ -268,16 +334,20 @@ public class AdminFilamentController {
|
|||||||
|
|
||||||
dto.setVariantDisplayName(variant.getVariantDisplayName());
|
dto.setVariantDisplayName(variant.getVariantDisplayName());
|
||||||
dto.setColorName(variant.getColorName());
|
dto.setColorName(variant.getColorName());
|
||||||
|
dto.setColorHex(variant.getColorHex());
|
||||||
|
dto.setFinishType(variant.getFinishType());
|
||||||
|
dto.setBrand(variant.getBrand());
|
||||||
dto.setIsMatte(variant.getIsMatte());
|
dto.setIsMatte(variant.getIsMatte());
|
||||||
dto.setIsSpecial(variant.getIsSpecial());
|
dto.setIsSpecial(variant.getIsSpecial());
|
||||||
dto.setCostChfPerKg(variant.getCostChfPerKg());
|
dto.setCostChfPerKg(variant.getCostChfPerKg());
|
||||||
dto.setStockSpools(variant.getStockSpools());
|
dto.setStockSpools(variant.getStockSpools());
|
||||||
dto.setSpoolNetKg(variant.getSpoolNetKg());
|
dto.setSpoolNetKg(variant.getSpoolNetKg());
|
||||||
|
BigDecimal stockKg = BigDecimal.ZERO;
|
||||||
if (variant.getStockSpools() != null && variant.getSpoolNetKg() != null) {
|
if (variant.getStockSpools() != null && variant.getSpoolNetKg() != null) {
|
||||||
dto.setStockKg(variant.getStockSpools().multiply(variant.getSpoolNetKg()));
|
stockKg = variant.getStockSpools().multiply(variant.getSpoolNetKg());
|
||||||
} else {
|
|
||||||
dto.setStockKg(BigDecimal.ZERO);
|
|
||||||
}
|
}
|
||||||
|
dto.setStockKg(stockKg);
|
||||||
|
dto.setStockFilamentGrams(stockKg.multiply(BigDecimal.valueOf(1000)));
|
||||||
dto.setIsActive(variant.getIsActive());
|
dto.setIsActive(variant.getIsActive());
|
||||||
dto.setCreatedAt(variant.getCreatedAt());
|
dto.setCreatedAt(variant.getCreatedAt());
|
||||||
return dto;
|
return dto;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -105,6 +106,10 @@ public class AdminOperationsController {
|
|||||||
dto.setStockSpools(stock.getStockSpools());
|
dto.setStockSpools(stock.getStockSpools());
|
||||||
dto.setSpoolNetKg(stock.getSpoolNetKg());
|
dto.setSpoolNetKg(stock.getSpoolNetKg());
|
||||||
dto.setStockKg(stock.getStockKg());
|
dto.setStockKg(stock.getStockKg());
|
||||||
|
BigDecimal grams = stock.getStockKg() != null
|
||||||
|
? stock.getStockKg().multiply(BigDecimal.valueOf(1000))
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
dto.setStockFilamentGrams(grams);
|
||||||
|
|
||||||
if (variant != null) {
|
if (variant != null) {
|
||||||
dto.setMaterialCode(
|
dto.setMaterialCode(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class AdminFilamentStockDto {
|
|||||||
private BigDecimal stockSpools;
|
private BigDecimal stockSpools;
|
||||||
private BigDecimal spoolNetKg;
|
private BigDecimal spoolNetKg;
|
||||||
private BigDecimal stockKg;
|
private BigDecimal stockKg;
|
||||||
|
private BigDecimal stockFilamentGrams;
|
||||||
private Boolean active;
|
private Boolean active;
|
||||||
|
|
||||||
public Long getFilamentVariantId() {
|
public Long getFilamentVariantId() {
|
||||||
@@ -68,6 +69,14 @@ public class AdminFilamentStockDto {
|
|||||||
this.stockKg = stockKg;
|
this.stockKg = stockKg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getStockFilamentGrams() {
|
||||||
|
return stockFilamentGrams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStockFilamentGrams(BigDecimal stockFilamentGrams) {
|
||||||
|
this.stockFilamentGrams = stockFilamentGrams;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getActive() {
|
public Boolean getActive() {
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ public class AdminFilamentVariantDto {
|
|||||||
private String materialTechnicalTypeLabel;
|
private String materialTechnicalTypeLabel;
|
||||||
private String variantDisplayName;
|
private String variantDisplayName;
|
||||||
private String colorName;
|
private String colorName;
|
||||||
|
private String colorHex;
|
||||||
|
private String finishType;
|
||||||
|
private String brand;
|
||||||
private Boolean isMatte;
|
private Boolean isMatte;
|
||||||
private Boolean isSpecial;
|
private Boolean isSpecial;
|
||||||
private BigDecimal costChfPerKg;
|
private BigDecimal costChfPerKg;
|
||||||
private BigDecimal stockSpools;
|
private BigDecimal stockSpools;
|
||||||
private BigDecimal spoolNetKg;
|
private BigDecimal spoolNetKg;
|
||||||
private BigDecimal stockKg;
|
private BigDecimal stockKg;
|
||||||
|
private BigDecimal stockFilamentGrams;
|
||||||
private Boolean isActive;
|
private Boolean isActive;
|
||||||
private OffsetDateTime createdAt;
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
@@ -85,6 +89,30 @@ public class AdminFilamentVariantDto {
|
|||||||
this.colorName = colorName;
|
this.colorName = colorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColorHex() {
|
||||||
|
return colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorHex(String colorHex) {
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFinishType() {
|
||||||
|
return finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFinishType(String finishType) {
|
||||||
|
this.finishType = finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrand() {
|
||||||
|
return brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrand(String brand) {
|
||||||
|
this.brand = brand;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getIsMatte() {
|
public Boolean getIsMatte() {
|
||||||
return isMatte;
|
return isMatte;
|
||||||
}
|
}
|
||||||
@@ -133,6 +161,14 @@ public class AdminFilamentVariantDto {
|
|||||||
this.stockKg = stockKg;
|
this.stockKg = stockKg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getStockFilamentGrams() {
|
||||||
|
return stockFilamentGrams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStockFilamentGrams(BigDecimal stockFilamentGrams) {
|
||||||
|
this.stockFilamentGrams = stockFilamentGrams;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getIsActive() {
|
public Boolean getIsActive() {
|
||||||
return isActive;
|
return isActive;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ public class AdminUpsertFilamentVariantRequest {
|
|||||||
private Long materialTypeId;
|
private Long materialTypeId;
|
||||||
private String variantDisplayName;
|
private String variantDisplayName;
|
||||||
private String colorName;
|
private String colorName;
|
||||||
|
private String colorHex;
|
||||||
|
private String finishType;
|
||||||
|
private String brand;
|
||||||
private Boolean isMatte;
|
private Boolean isMatte;
|
||||||
private Boolean isSpecial;
|
private Boolean isSpecial;
|
||||||
private BigDecimal costChfPerKg;
|
private BigDecimal costChfPerKg;
|
||||||
@@ -37,6 +40,30 @@ public class AdminUpsertFilamentVariantRequest {
|
|||||||
this.colorName = colorName;
|
this.colorName = colorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColorHex() {
|
||||||
|
return colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorHex(String colorHex) {
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFinishType() {
|
||||||
|
return finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFinishType(String finishType) {
|
||||||
|
this.finishType = finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrand() {
|
||||||
|
return brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrand(String brand) {
|
||||||
|
this.brand = brand;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getIsMatte() {
|
public Boolean getIsMatte() {
|
||||||
return isMatte;
|
return isMatte;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,16 @@ public record OptionsResponse(
|
|||||||
List<NozzleOptionDTO> nozzleDiameters
|
List<NozzleOptionDTO> nozzleDiameters
|
||||||
) {
|
) {
|
||||||
public record MaterialOption(String code, String label, List<VariantOption> variants) {}
|
public record MaterialOption(String code, String label, List<VariantOption> variants) {}
|
||||||
public record VariantOption(String name, String colorName, String hexColor, boolean isOutOfStock) {}
|
public record VariantOption(
|
||||||
|
Long id,
|
||||||
|
String name,
|
||||||
|
String colorName,
|
||||||
|
String hexColor,
|
||||||
|
String finishType,
|
||||||
|
Double stockSpools,
|
||||||
|
Double stockFilamentGrams,
|
||||||
|
boolean isOutOfStock
|
||||||
|
) {}
|
||||||
public record QualityOption(String id, String label) {}
|
public record QualityOption(String id, String label) {}
|
||||||
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) {}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public class PrintSettingsDto {
|
|||||||
// Common
|
// Common
|
||||||
private String material; // e.g. "PLA", "PETG"
|
private String material; // e.g. "PLA", "PETG"
|
||||||
private String color; // e.g. "White", "#FFFFFF"
|
private String color; // e.g. "White", "#FFFFFF"
|
||||||
|
private Long filamentVariantId;
|
||||||
|
private Long printerMachineId;
|
||||||
|
|
||||||
// Basic Mode
|
// Basic Mode
|
||||||
private String quality; // "draft", "standard", "high"
|
private String quality; // "draft", "standard", "high"
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ public class FilamentVariant {
|
|||||||
@Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE)
|
@Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE)
|
||||||
private String colorName;
|
private String colorName;
|
||||||
|
|
||||||
|
@Column(name = "color_hex", length = Integer.MAX_VALUE)
|
||||||
|
private String colorHex;
|
||||||
|
|
||||||
|
@ColumnDefault("'GLOSSY'")
|
||||||
|
@Column(name = "finish_type", length = Integer.MAX_VALUE)
|
||||||
|
private String finishType;
|
||||||
|
|
||||||
|
@Column(name = "brand", length = Integer.MAX_VALUE)
|
||||||
|
private String brand;
|
||||||
|
|
||||||
@ColumnDefault("false")
|
@ColumnDefault("false")
|
||||||
@Column(name = "is_matte", nullable = false)
|
@Column(name = "is_matte", nullable = false)
|
||||||
private Boolean isMatte;
|
private Boolean isMatte;
|
||||||
@@ -83,6 +93,30 @@ public class FilamentVariant {
|
|||||||
this.colorName = colorName;
|
this.colorName = colorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColorHex() {
|
||||||
|
return colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorHex(String colorHex) {
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFinishType() {
|
||||||
|
return finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFinishType(String finishType) {
|
||||||
|
this.finishType = finishType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrand() {
|
||||||
|
return brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrand(String brand) {
|
||||||
|
this.brand = brand;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getIsMatte() {
|
public Boolean getIsMatte() {
|
||||||
return isMatte;
|
return isMatte;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.printcalculator.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.ColumnDefault;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "filament_variant_orca_override", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "ux_filament_variant_orca_override_variant_machine", columnNames = {
|
||||||
|
"filament_variant_id", "printer_machine_profile_id"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
public class FilamentVariantOrcaOverride {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "filament_variant_orca_override_id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "filament_variant_id", nullable = false)
|
||||||
|
private FilamentVariant filamentVariant;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "printer_machine_profile_id", nullable = false)
|
||||||
|
private PrinterMachineProfile printerMachineProfile;
|
||||||
|
|
||||||
|
@Column(name = "orca_filament_profile_name", nullable = false, length = Integer.MAX_VALUE)
|
||||||
|
private String orcaFilamentProfileName;
|
||||||
|
|
||||||
|
@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 FilamentVariant getFilamentVariant() {
|
||||||
|
return filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilamentVariant(FilamentVariant filamentVariant) {
|
||||||
|
this.filamentVariant = filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrinterMachineProfile getPrinterMachineProfile() {
|
||||||
|
return printerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrinterMachineProfile(PrinterMachineProfile printerMachineProfile) {
|
||||||
|
this.printerMachineProfile = printerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrcaFilamentProfileName() {
|
||||||
|
return orcaFilamentProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrcaFilamentProfileName(String orcaFilamentProfileName) {
|
||||||
|
this.orcaFilamentProfileName = orcaFilamentProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(Boolean isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.printcalculator.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.ColumnDefault;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "material_orca_profile_map", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "ux_material_orca_profile_map_machine_material", columnNames = {
|
||||||
|
"printer_machine_profile_id", "filament_material_type_id"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
public class MaterialOrcaProfileMap {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "material_orca_profile_map_id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "printer_machine_profile_id", nullable = false)
|
||||||
|
private PrinterMachineProfile printerMachineProfile;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "filament_material_type_id", nullable = false)
|
||||||
|
private FilamentMaterialType filamentMaterialType;
|
||||||
|
|
||||||
|
@Column(name = "orca_filament_profile_name", nullable = false, length = Integer.MAX_VALUE)
|
||||||
|
private String orcaFilamentProfileName;
|
||||||
|
|
||||||
|
@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 PrinterMachineProfile getPrinterMachineProfile() {
|
||||||
|
return printerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrinterMachineProfile(PrinterMachineProfile printerMachineProfile) {
|
||||||
|
this.printerMachineProfile = printerMachineProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilamentMaterialType getFilamentMaterialType() {
|
||||||
|
return filamentMaterialType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilamentMaterialType(FilamentMaterialType filamentMaterialType) {
|
||||||
|
this.filamentMaterialType = filamentMaterialType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrcaFilamentProfileName() {
|
||||||
|
return orcaFilamentProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrcaFilamentProfileName(String orcaFilamentProfileName) {
|
||||||
|
this.orcaFilamentProfileName = orcaFilamentProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(Boolean isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,10 @@ public class OrderItem {
|
|||||||
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
|
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
|
||||||
private String materialCode;
|
private String materialCode;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "filament_variant_id")
|
||||||
|
private FilamentVariant filamentVariant;
|
||||||
|
|
||||||
@Column(name = "color_code", length = Integer.MAX_VALUE)
|
@Column(name = "color_code", length = Integer.MAX_VALUE)
|
||||||
private String colorCode;
|
private String colorCode;
|
||||||
|
|
||||||
@@ -158,6 +162,14 @@ public class OrderItem {
|
|||||||
this.materialCode = materialCode;
|
this.materialCode = materialCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FilamentVariant getFilamentVariant() {
|
||||||
|
return filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilamentVariant(FilamentVariant filamentVariant) {
|
||||||
|
this.filamentVariant = filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
public String getColorCode() {
|
public String getColorCode() {
|
||||||
return colorCode;
|
return colorCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.printcalculator.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.ColumnDefault;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "printer_machine_profile", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "ux_printer_machine_profile_machine_nozzle", columnNames = {
|
||||||
|
"printer_machine_id", "nozzle_diameter_mm"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
public class PrinterMachineProfile {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "printer_machine_profile_id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "printer_machine_id", nullable = false)
|
||||||
|
private PrinterMachine printerMachine;
|
||||||
|
|
||||||
|
@Column(name = "nozzle_diameter_mm", nullable = false, precision = 4, scale = 2)
|
||||||
|
private BigDecimal nozzleDiameterMm;
|
||||||
|
|
||||||
|
@Column(name = "orca_machine_profile_name", nullable = false, length = Integer.MAX_VALUE)
|
||||||
|
private String orcaMachineProfileName;
|
||||||
|
|
||||||
|
@ColumnDefault("false")
|
||||||
|
@Column(name = "is_default", nullable = false)
|
||||||
|
private Boolean isDefault;
|
||||||
|
|
||||||
|
@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 PrinterMachine getPrinterMachine() {
|
||||||
|
return printerMachine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrinterMachine(PrinterMachine printerMachine) {
|
||||||
|
this.printerMachine = printerMachine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getNozzleDiameterMm() {
|
||||||
|
return nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
|
||||||
|
this.nozzleDiameterMm = nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrcaMachineProfileName() {
|
||||||
|
return orcaMachineProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrcaMachineProfileName(String orcaMachineProfileName) {
|
||||||
|
this.orcaMachineProfileName = orcaMachineProfileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsDefault() {
|
||||||
|
return isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsDefault(Boolean isDefault) {
|
||||||
|
this.isDefault = isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(Boolean isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,11 @@ public class QuoteLineItem {
|
|||||||
@Column(name = "color_code", length = Integer.MAX_VALUE)
|
@Column(name = "color_code", length = Integer.MAX_VALUE)
|
||||||
private String colorCode;
|
private String colorCode;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "filament_variant_id")
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
private FilamentVariant filamentVariant;
|
||||||
|
|
||||||
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
|
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
|
||||||
private BigDecimal boundingBoxXMm;
|
private BigDecimal boundingBoxXMm;
|
||||||
|
|
||||||
@@ -124,6 +129,14 @@ public class QuoteLineItem {
|
|||||||
this.colorCode = colorCode;
|
this.colorCode = colorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FilamentVariant getFilamentVariant() {
|
||||||
|
return filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilamentVariant(FilamentVariant filamentVariant) {
|
||||||
|
this.filamentVariant = filamentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getBoundingBoxXMm() {
|
public BigDecimal getBoundingBoxXMm() {
|
||||||
return boundingBoxXMm;
|
return boundingBoxXMm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.printcalculator.repository;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
|
import com.printcalculator.entity.FilamentVariantOrcaOverride;
|
||||||
|
import com.printcalculator.entity.PrinterMachineProfile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface FilamentVariantOrcaOverrideRepository extends JpaRepository<FilamentVariantOrcaOverride, Long> {
|
||||||
|
Optional<FilamentVariantOrcaOverride> findByFilamentVariantAndPrinterMachineProfileAndIsActiveTrue(
|
||||||
|
FilamentVariant filamentVariant,
|
||||||
|
PrinterMachineProfile printerMachineProfile
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.printcalculator.repository;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.FilamentMaterialType;
|
||||||
|
import com.printcalculator.entity.MaterialOrcaProfileMap;
|
||||||
|
import com.printcalculator.entity.PrinterMachineProfile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface MaterialOrcaProfileMapRepository extends JpaRepository<MaterialOrcaProfileMap, Long> {
|
||||||
|
Optional<MaterialOrcaProfileMap> findByPrinterMachineProfileAndFilamentMaterialTypeAndIsActiveTrue(
|
||||||
|
PrinterMachineProfile printerMachineProfile,
|
||||||
|
FilamentMaterialType filamentMaterialType
|
||||||
|
);
|
||||||
|
|
||||||
|
List<MaterialOrcaProfileMap> findByPrinterMachineProfileAndIsActiveTrue(PrinterMachineProfile printerMachineProfile);
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public interface OrderItemRepository extends JpaRepository<OrderItem, UUID> {
|
public interface OrderItemRepository extends JpaRepository<OrderItem, UUID> {
|
||||||
List<OrderItem> findByOrder_Id(UUID orderId);
|
List<OrderItem> findByOrder_Id(UUID orderId);
|
||||||
|
boolean existsByFilamentVariant_Id(Long filamentVariantId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.printcalculator.repository;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
|
import com.printcalculator.entity.PrinterMachineProfile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PrinterMachineProfileRepository extends JpaRepository<PrinterMachineProfile, Long> {
|
||||||
|
Optional<PrinterMachineProfile> findByPrinterMachineAndNozzleDiameterMmAndIsActiveTrue(PrinterMachine printerMachine, BigDecimal nozzleDiameterMm);
|
||||||
|
Optional<PrinterMachineProfile> findFirstByPrinterMachineAndIsDefaultTrueAndIsActiveTrue(PrinterMachine printerMachine);
|
||||||
|
List<PrinterMachineProfile> findByPrinterMachineAndIsActiveTrue(PrinterMachine printerMachine);
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public interface QuoteLineItemRepository extends JpaRepository<QuoteLineItem, UUID> {
|
public interface QuoteLineItemRepository extends JpaRepository<QuoteLineItem, UUID> {
|
||||||
List<QuoteLineItem> findByQuoteSessionId(UUID quoteSessionId);
|
List<QuoteLineItem> findByQuoteSessionId(UUID quoteSessionId);
|
||||||
|
boolean existsByFilamentVariant_Id(Long filamentVariantId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.FilamentMaterialType;
|
||||||
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
|
import com.printcalculator.entity.FilamentVariantOrcaOverride;
|
||||||
|
import com.printcalculator.entity.MaterialOrcaProfileMap;
|
||||||
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
|
import com.printcalculator.entity.PrinterMachineProfile;
|
||||||
|
import com.printcalculator.repository.FilamentVariantOrcaOverrideRepository;
|
||||||
|
import com.printcalculator.repository.MaterialOrcaProfileMapRepository;
|
||||||
|
import com.printcalculator.repository.PrinterMachineProfileRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrcaProfileResolver {
|
||||||
|
|
||||||
|
private final PrinterMachineProfileRepository machineProfileRepo;
|
||||||
|
private final MaterialOrcaProfileMapRepository materialMapRepo;
|
||||||
|
private final FilamentVariantOrcaOverrideRepository variantOverrideRepo;
|
||||||
|
|
||||||
|
public OrcaProfileResolver(
|
||||||
|
PrinterMachineProfileRepository machineProfileRepo,
|
||||||
|
MaterialOrcaProfileMapRepository materialMapRepo,
|
||||||
|
FilamentVariantOrcaOverrideRepository variantOverrideRepo
|
||||||
|
) {
|
||||||
|
this.machineProfileRepo = machineProfileRepo;
|
||||||
|
this.materialMapRepo = materialMapRepo;
|
||||||
|
this.variantOverrideRepo = variantOverrideRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolvedProfiles resolve(PrinterMachine printerMachine, BigDecimal nozzleDiameterMm, FilamentVariant variant) {
|
||||||
|
Optional<PrinterMachineProfile> machineProfileOpt = resolveMachineProfile(printerMachine, nozzleDiameterMm);
|
||||||
|
|
||||||
|
String machineProfileName = machineProfileOpt
|
||||||
|
.map(PrinterMachineProfile::getOrcaMachineProfileName)
|
||||||
|
.orElseGet(() -> fallbackMachineProfile(printerMachine, nozzleDiameterMm));
|
||||||
|
|
||||||
|
String filamentProfileName = machineProfileOpt
|
||||||
|
.map(machineProfile -> resolveFilamentProfileWithMachineProfile(machineProfile, variant)
|
||||||
|
.orElseGet(() -> fallbackFilamentProfile(variant.getFilamentMaterialType())))
|
||||||
|
.orElseGet(() -> fallbackFilamentProfile(variant.getFilamentMaterialType()));
|
||||||
|
|
||||||
|
return new ResolvedProfiles(machineProfileName, filamentProfileName, machineProfileOpt.orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<PrinterMachineProfile> resolveMachineProfile(PrinterMachine machine, BigDecimal nozzleDiameterMm) {
|
||||||
|
if (machine == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal normalizedNozzle = normalizeNozzle(nozzleDiameterMm);
|
||||||
|
if (normalizedNozzle != null) {
|
||||||
|
Optional<PrinterMachineProfile> exact = machineProfileRepo
|
||||||
|
.findByPrinterMachineAndNozzleDiameterMmAndIsActiveTrue(machine, normalizedNozzle);
|
||||||
|
if (exact.isPresent()) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<PrinterMachineProfile> defaultProfile = machineProfileRepo
|
||||||
|
.findFirstByPrinterMachineAndIsDefaultTrueAndIsActiveTrue(machine);
|
||||||
|
if (defaultProfile.isPresent()) {
|
||||||
|
return defaultProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineProfileRepo.findByPrinterMachineAndIsActiveTrue(machine)
|
||||||
|
.stream()
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> resolveFilamentProfileWithMachineProfile(PrinterMachineProfile machineProfile, FilamentVariant variant) {
|
||||||
|
if (machineProfile == null || variant == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FilamentVariantOrcaOverride> override = variantOverrideRepo
|
||||||
|
.findByFilamentVariantAndPrinterMachineProfileAndIsActiveTrue(variant, machineProfile);
|
||||||
|
|
||||||
|
if (override.isPresent()) {
|
||||||
|
return Optional.ofNullable(override.get().getOrcaFilamentProfileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MaterialOrcaProfileMap> map = materialMapRepo
|
||||||
|
.findByPrinterMachineProfileAndFilamentMaterialTypeAndIsActiveTrue(
|
||||||
|
machineProfile,
|
||||||
|
variant.getFilamentMaterialType()
|
||||||
|
);
|
||||||
|
|
||||||
|
return map.map(MaterialOrcaProfileMap::getOrcaFilamentProfileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fallbackMachineProfile(PrinterMachine machine, BigDecimal nozzleDiameterMm) {
|
||||||
|
if (machine == null || machine.getPrinterDisplayName() == null || machine.getPrinterDisplayName().isBlank()) {
|
||||||
|
return "Bambu Lab A1 0.4 nozzle";
|
||||||
|
}
|
||||||
|
|
||||||
|
String displayName = machine.getPrinterDisplayName();
|
||||||
|
if (displayName.toLowerCase().contains("bambulab a1") || displayName.toLowerCase().contains("bambu lab a1")) {
|
||||||
|
BigDecimal normalizedNozzle = normalizeNozzle(nozzleDiameterMm);
|
||||||
|
if (normalizedNozzle == null) {
|
||||||
|
return "Bambu Lab A1 0.4 nozzle";
|
||||||
|
}
|
||||||
|
return "Bambu Lab A1 " + normalizedNozzle.toPlainString() + " nozzle";
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fallbackFilamentProfile(FilamentMaterialType materialType) {
|
||||||
|
String materialCode = materialType != null && materialType.getMaterialCode() != null
|
||||||
|
? materialType.getMaterialCode().trim().toUpperCase()
|
||||||
|
: "PLA";
|
||||||
|
|
||||||
|
return switch (materialCode) {
|
||||||
|
case "PETG" -> "Generic PETG";
|
||||||
|
case "TPU" -> "Generic TPU";
|
||||||
|
case "PC" -> "Generic PC";
|
||||||
|
case "ABS" -> "Generic ABS";
|
||||||
|
default -> "Generic PLA";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal normalizeNozzle(BigDecimal nozzleDiameterMm) {
|
||||||
|
if (nozzleDiameterMm == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nozzleDiameterMm.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ResolvedProfiles(
|
||||||
|
String machineProfileName,
|
||||||
|
String filamentProfileName,
|
||||||
|
PrinterMachineProfile machineProfile
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -203,7 +203,14 @@ public class OrderService {
|
|||||||
oItem.setOriginalFilename(qItem.getOriginalFilename());
|
oItem.setOriginalFilename(qItem.getOriginalFilename());
|
||||||
oItem.setQuantity(qItem.getQuantity());
|
oItem.setQuantity(qItem.getQuantity());
|
||||||
oItem.setColorCode(qItem.getColorCode());
|
oItem.setColorCode(qItem.getColorCode());
|
||||||
|
oItem.setFilamentVariant(qItem.getFilamentVariant());
|
||||||
|
if (qItem.getFilamentVariant() != null
|
||||||
|
&& qItem.getFilamentVariant().getFilamentMaterialType() != null
|
||||||
|
&& qItem.getFilamentVariant().getFilamentMaterialType().getMaterialCode() != null) {
|
||||||
|
oItem.setMaterialCode(qItem.getFilamentVariant().getFilamentMaterialType().getMaterialCode());
|
||||||
|
} else {
|
||||||
oItem.setMaterialCode(session.getMaterialCode());
|
oItem.setMaterialCode(session.getMaterialCode());
|
||||||
|
}
|
||||||
|
|
||||||
BigDecimal distributedUnitPrice = qItem.getUnitPriceChf();
|
BigDecimal distributedUnitPrice = qItem.getUnitPriceChf();
|
||||||
if (totalSeconds.compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
|
if (totalSeconds.compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
|
||||||
|
|||||||
@@ -60,40 +60,49 @@ public class QuoteCalculator {
|
|||||||
.orElseThrow(() -> new RuntimeException("No active printer found"));
|
.orElseThrow(() -> new RuntimeException("No active printer found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fetch Filament Info
|
|
||||||
// filamentProfileName might be "bambu_pla_basic_black" or "Generic PLA"
|
|
||||||
// We try to extract material code (PLA, PETG)
|
|
||||||
String materialCode = detectMaterialCode(filamentProfileName);
|
String materialCode = detectMaterialCode(filamentProfileName);
|
||||||
FilamentMaterialType materialType = materialRepo.findByMaterialCode(materialCode)
|
FilamentMaterialType materialType = materialRepo.findByMaterialCode(materialCode)
|
||||||
.orElseThrow(() -> new RuntimeException("Unknown material type: " + materialCode));
|
.orElseThrow(() -> new RuntimeException("Unknown material type: " + materialCode));
|
||||||
|
|
||||||
// Try to find specific variant (e.g. by color if we could parse it)
|
|
||||||
// For now, get default/first active variant for this material
|
|
||||||
FilamentVariant variant = variantRepo.findFirstByFilamentMaterialTypeAndIsActiveTrue(materialType)
|
FilamentVariant variant = variantRepo.findFirstByFilamentMaterialTypeAndIsActiveTrue(materialType)
|
||||||
.orElseThrow(() -> new RuntimeException("No active variant for material: " + materialCode));
|
.orElseThrow(() -> new RuntimeException("No active variant for material: " + materialCode));
|
||||||
|
|
||||||
|
return calculate(stats, machine, policy, variant);
|
||||||
|
}
|
||||||
|
|
||||||
// --- CALCULATIONS ---
|
public QuoteResult calculate(PrintStats stats, String machineName, FilamentVariant variant) {
|
||||||
|
PricingPolicy policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
|
||||||
|
if (policy == null) {
|
||||||
|
throw new RuntimeException("No active pricing policy found");
|
||||||
|
}
|
||||||
|
|
||||||
// Material Cost: (weight / 1000) * costPerKg
|
PrinterMachine machine = machineRepo.findByPrinterDisplayName(machineName).orElse(null);
|
||||||
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
if (machine == null) {
|
||||||
|
machine = machineRepo.findFirstByIsActiveTrue()
|
||||||
|
.orElseThrow(() -> new RuntimeException("No active printer found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculate(stats, machine, policy, variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QuoteResult calculate(PrintStats stats, PrinterMachine machine, PricingPolicy policy, FilamentVariant variant) {
|
||||||
|
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams())
|
||||||
|
.divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
||||||
|
|
||||||
// We do NOT add tiered machine cost here anymore - it is calculated globally per session
|
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds())
|
||||||
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
.divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
BigDecimal kw = BigDecimal.valueOf(machine.getPowerWatts())
|
||||||
BigDecimal kw = BigDecimal.valueOf(machine.getPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
.divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal kwh = kw.multiply(totalHours);
|
BigDecimal kwh = kw.multiply(totalHours);
|
||||||
BigDecimal energyCost = kwh.multiply(policy.getElectricityCostChfPerKwh());
|
BigDecimal energyCost = kwh.multiply(policy.getElectricityCostChfPerKwh());
|
||||||
|
|
||||||
// Subtotal (Costs without Fixed Fees and without Machine Tiers)
|
|
||||||
BigDecimal subtotal = materialCost.add(energyCost);
|
BigDecimal subtotal = materialCost.add(energyCost);
|
||||||
|
BigDecimal markupFactor = BigDecimal.ONE.add(
|
||||||
// Markup
|
policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)
|
||||||
// Markup is percentage (e.g. 20.0)
|
);
|
||||||
BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
|
||||||
subtotal = subtotal.multiply(markupFactor);
|
subtotal = subtotal.multiply(markupFactor);
|
||||||
|
|
||||||
return new QuoteResult(subtotal.doubleValue(), "CHF", stats);
|
return new QuoteResult(subtotal.doubleValue(), "CHF", stats);
|
||||||
}
|
}
|
||||||
public BigDecimal calculateSessionMachineCost(PricingPolicy policy, BigDecimal hours) {
|
public BigDecimal calculateSessionMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||||
|
|||||||
204
db.sql
204
db.sql
@@ -44,6 +44,10 @@ create table filament_variant
|
|||||||
|
|
||||||
variant_display_name text not null, -- es: "PLA Nero Opaco BrandX"
|
variant_display_name text not null, -- es: "PLA Nero Opaco BrandX"
|
||||||
color_name text not null, -- Nero, Bianco, ecc.
|
color_name text not null, -- Nero, Bianco, ecc.
|
||||||
|
color_hex text,
|
||||||
|
finish_type text not null default 'GLOSSY'
|
||||||
|
check (finish_type in ('GLOSSY', 'MATTE', 'MARBLE', 'SILK', 'TRANSLUCENT', 'SPECIAL')),
|
||||||
|
brand text,
|
||||||
is_matte boolean not null default false,
|
is_matte boolean not null default false,
|
||||||
is_special boolean not null default false,
|
is_special boolean not null default false,
|
||||||
|
|
||||||
@@ -66,6 +70,37 @@ select filament_variant_id,
|
|||||||
(stock_spools * spool_net_kg) as stock_kg
|
(stock_spools * spool_net_kg) as stock_kg
|
||||||
from filament_variant;
|
from filament_variant;
|
||||||
|
|
||||||
|
create table printer_machine_profile
|
||||||
|
(
|
||||||
|
printer_machine_profile_id bigserial primary key,
|
||||||
|
printer_machine_id bigint not null references printer_machine (printer_machine_id) on delete cascade,
|
||||||
|
nozzle_diameter_mm numeric(4, 2) not null check (nozzle_diameter_mm > 0),
|
||||||
|
orca_machine_profile_name text not null,
|
||||||
|
is_default boolean not null default false,
|
||||||
|
is_active boolean not null default true,
|
||||||
|
unique (printer_machine_id, nozzle_diameter_mm)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table material_orca_profile_map
|
||||||
|
(
|
||||||
|
material_orca_profile_map_id bigserial primary key,
|
||||||
|
printer_machine_profile_id bigint not null references printer_machine_profile (printer_machine_profile_id) on delete cascade,
|
||||||
|
filament_material_type_id bigint not null references filament_material_type (filament_material_type_id),
|
||||||
|
orca_filament_profile_name text not null,
|
||||||
|
is_active boolean not null default true,
|
||||||
|
unique (printer_machine_profile_id, filament_material_type_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table filament_variant_orca_override
|
||||||
|
(
|
||||||
|
filament_variant_orca_override_id bigserial primary key,
|
||||||
|
filament_variant_id bigint not null references filament_variant (filament_variant_id) on delete cascade,
|
||||||
|
printer_machine_profile_id bigint not null references printer_machine_profile (printer_machine_profile_id) on delete cascade,
|
||||||
|
orca_filament_profile_name text not null,
|
||||||
|
is_active boolean not null default true,
|
||||||
|
unique (filament_variant_id, printer_machine_profile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
create table pricing_policy
|
create table pricing_policy
|
||||||
@@ -252,6 +287,7 @@ insert into filament_material_type (material_code,
|
|||||||
values ('PLA', false, false, null),
|
values ('PLA', false, false, null),
|
||||||
('PETG', false, false, null),
|
('PETG', false, false, null),
|
||||||
('TPU', true, false, null),
|
('TPU', true, false, null),
|
||||||
|
('PC', false, true, 'engineering'),
|
||||||
('ABS', false, false, null),
|
('ABS', false, false, null),
|
||||||
('Nylon', false, false, null),
|
('Nylon', false, false, null),
|
||||||
('Carbon PLA', false, false, null)
|
('Carbon PLA', false, false, null)
|
||||||
@@ -275,6 +311,9 @@ insert
|
|||||||
into filament_variant (filament_material_type_id,
|
into filament_variant (filament_material_type_id,
|
||||||
variant_display_name,
|
variant_display_name,
|
||||||
color_name,
|
color_name,
|
||||||
|
color_hex,
|
||||||
|
finish_type,
|
||||||
|
brand,
|
||||||
is_matte,
|
is_matte,
|
||||||
is_special,
|
is_special,
|
||||||
cost_chf_per_kg,
|
cost_chf_per_kg,
|
||||||
@@ -284,6 +323,9 @@ into filament_variant (filament_material_type_id,
|
|||||||
select pla.filament_material_type_id,
|
select pla.filament_material_type_id,
|
||||||
v.variant_display_name,
|
v.variant_display_name,
|
||||||
v.color_name,
|
v.color_name,
|
||||||
|
v.color_hex,
|
||||||
|
v.finish_type,
|
||||||
|
null::text as brand,
|
||||||
v.is_matte,
|
v.is_matte,
|
||||||
v.is_special,
|
v.is_special,
|
||||||
18.00, -- PLA da Excel
|
18.00, -- PLA da Excel
|
||||||
@@ -291,17 +333,114 @@ select pla.filament_material_type_id,
|
|||||||
1.000,
|
1.000,
|
||||||
true
|
true
|
||||||
from pla
|
from pla
|
||||||
cross join (values ('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
|
cross join (values ('PLA Bianco', 'Bianco', '#F5F5F5', 'GLOSSY', false, false, 3.000::numeric),
|
||||||
('PLA Nero', 'Nero', false, false, 3.000::numeric),
|
('PLA Nero', 'Nero', '#1A1A1A', 'GLOSSY', false, false, 3.000::numeric),
|
||||||
('PLA Blu', 'Blu', false, false, 1.000::numeric),
|
('PLA Blu', 'Blu', '#1976D2', 'GLOSSY', false, false, 1.000::numeric),
|
||||||
('PLA Arancione', 'Arancione', false, false, 1.000::numeric),
|
('PLA Arancione', 'Arancione', '#FFA726', 'GLOSSY', false, false, 1.000::numeric),
|
||||||
('PLA Grigio', 'Grigio', false, false, 1.000::numeric),
|
('PLA Grigio', 'Grigio', '#BDBDBD', 'GLOSSY', false, false, 1.000::numeric),
|
||||||
('PLA Grigio Scuro', 'Grigio scuro', false, false, 1.000::numeric),
|
('PLA Grigio Scuro', 'Grigio scuro', '#424242', 'MATTE', true, false, 1.000::numeric),
|
||||||
('PLA Grigio Chiaro', 'Grigio chiaro', false, false, 1.000::numeric),
|
('PLA Grigio Chiaro', 'Grigio chiaro', '#D6D6D6', 'MATTE', true, false, 1.000::numeric),
|
||||||
('PLA Viola', 'Viola', false, false,
|
('PLA Viola', 'Viola', '#7B1FA2', 'GLOSSY', false, false,
|
||||||
1.000::numeric)) as v(variant_display_name, color_name, is_matte, is_special, stock_spools)
|
1.000::numeric)) as v(variant_display_name, color_name, color_hex, finish_type, is_matte, is_special, stock_spools)
|
||||||
on conflict (filament_material_type_id, variant_display_name) do update
|
on conflict (filament_material_type_id, variant_display_name) do update
|
||||||
set color_name = excluded.color_name,
|
set color_name = excluded.color_name,
|
||||||
|
color_hex = excluded.color_hex,
|
||||||
|
finish_type = excluded.finish_type,
|
||||||
|
brand = excluded.brand,
|
||||||
|
is_matte = excluded.is_matte,
|
||||||
|
is_special = excluded.is_special,
|
||||||
|
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||||||
|
stock_spools = excluded.stock_spools,
|
||||||
|
spool_net_kg = excluded.spool_net_kg,
|
||||||
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
-- Varianti base per materiali principali del calcolatore
|
||||||
|
with mat as (select filament_material_type_id
|
||||||
|
from filament_material_type
|
||||||
|
where material_code = 'PETG')
|
||||||
|
insert
|
||||||
|
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||||||
|
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||||||
|
select mat.filament_material_type_id,
|
||||||
|
'PETG Nero',
|
||||||
|
'Nero',
|
||||||
|
'#1A1A1A',
|
||||||
|
'GLOSSY',
|
||||||
|
'Bambu',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
24.00,
|
||||||
|
1.000,
|
||||||
|
1.000,
|
||||||
|
true
|
||||||
|
from mat
|
||||||
|
on conflict (filament_material_type_id, variant_display_name) do update
|
||||||
|
set color_name = excluded.color_name,
|
||||||
|
color_hex = excluded.color_hex,
|
||||||
|
finish_type = excluded.finish_type,
|
||||||
|
brand = excluded.brand,
|
||||||
|
is_matte = excluded.is_matte,
|
||||||
|
is_special = excluded.is_special,
|
||||||
|
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||||||
|
stock_spools = excluded.stock_spools,
|
||||||
|
spool_net_kg = excluded.spool_net_kg,
|
||||||
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
with mat as (select filament_material_type_id
|
||||||
|
from filament_material_type
|
||||||
|
where material_code = 'TPU')
|
||||||
|
insert
|
||||||
|
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||||||
|
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||||||
|
select mat.filament_material_type_id,
|
||||||
|
'TPU Nero',
|
||||||
|
'Nero',
|
||||||
|
'#1A1A1A',
|
||||||
|
'GLOSSY',
|
||||||
|
'Bambu',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
42.00,
|
||||||
|
1.000,
|
||||||
|
1.000,
|
||||||
|
true
|
||||||
|
from mat
|
||||||
|
on conflict (filament_material_type_id, variant_display_name) do update
|
||||||
|
set color_name = excluded.color_name,
|
||||||
|
color_hex = excluded.color_hex,
|
||||||
|
finish_type = excluded.finish_type,
|
||||||
|
brand = excluded.brand,
|
||||||
|
is_matte = excluded.is_matte,
|
||||||
|
is_special = excluded.is_special,
|
||||||
|
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||||||
|
stock_spools = excluded.stock_spools,
|
||||||
|
spool_net_kg = excluded.spool_net_kg,
|
||||||
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
with mat as (select filament_material_type_id
|
||||||
|
from filament_material_type
|
||||||
|
where material_code = 'PC')
|
||||||
|
insert
|
||||||
|
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||||||
|
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||||||
|
select mat.filament_material_type_id,
|
||||||
|
'PC Naturale',
|
||||||
|
'Naturale',
|
||||||
|
'#D9D9D9',
|
||||||
|
'TRANSLUCENT',
|
||||||
|
'Generic',
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
48.00,
|
||||||
|
1.000,
|
||||||
|
1.000,
|
||||||
|
true
|
||||||
|
from mat
|
||||||
|
on conflict (filament_material_type_id, variant_display_name) do update
|
||||||
|
set color_name = excluded.color_name,
|
||||||
|
color_hex = excluded.color_hex,
|
||||||
|
finish_type = excluded.finish_type,
|
||||||
|
brand = excluded.brand,
|
||||||
is_matte = excluded.is_matte,
|
is_matte = excluded.is_matte,
|
||||||
is_special = excluded.is_special,
|
is_special = excluded.is_special,
|
||||||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||||||
@@ -325,6 +464,51 @@ on conflict (nozzle_diameter_mm) do update
|
|||||||
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
|
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
|
||||||
is_active = excluded.is_active;
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 5b) Orca machine/material mapping (data-driven)
|
||||||
|
-- =========================================================
|
||||||
|
with a1 as (select printer_machine_id
|
||||||
|
from printer_machine
|
||||||
|
where printer_display_name = 'BambuLab A1')
|
||||||
|
insert
|
||||||
|
into printer_machine_profile (printer_machine_id, nozzle_diameter_mm, orca_machine_profile_name, is_default, is_active)
|
||||||
|
select a1.printer_machine_id, v.nozzle_diameter_mm, v.profile_name, v.is_default, true
|
||||||
|
from a1
|
||||||
|
cross join (values (0.40::numeric, 'Bambu Lab A1 0.4 nozzle', true),
|
||||||
|
(0.20::numeric, 'Bambu Lab A1 0.2 nozzle', false),
|
||||||
|
(0.60::numeric, 'Bambu Lab A1 0.6 nozzle', false),
|
||||||
|
(0.80::numeric, 'Bambu Lab A1 0.8 nozzle', false))
|
||||||
|
as v(nozzle_diameter_mm, profile_name, is_default)
|
||||||
|
on conflict (printer_machine_id, nozzle_diameter_mm) do update
|
||||||
|
set orca_machine_profile_name = excluded.orca_machine_profile_name,
|
||||||
|
is_default = excluded.is_default,
|
||||||
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
with p as (select printer_machine_profile_id
|
||||||
|
from printer_machine_profile pmp
|
||||||
|
join printer_machine pm on pm.printer_machine_id = pmp.printer_machine_id
|
||||||
|
where pm.printer_display_name = 'BambuLab A1'
|
||||||
|
and pmp.nozzle_diameter_mm = 0.40::numeric),
|
||||||
|
m as (select filament_material_type_id, material_code
|
||||||
|
from filament_material_type
|
||||||
|
where material_code in ('PLA', 'PETG', 'TPU', 'PC'))
|
||||||
|
insert
|
||||||
|
into material_orca_profile_map (printer_machine_profile_id, filament_material_type_id, orca_filament_profile_name, is_active)
|
||||||
|
select p.printer_machine_profile_id,
|
||||||
|
m.filament_material_type_id,
|
||||||
|
case m.material_code
|
||||||
|
when 'PLA' then 'Bambu PLA Basic @BBL A1'
|
||||||
|
when 'PETG' then 'Bambu PETG Basic @BBL A1'
|
||||||
|
when 'TPU' then 'Bambu TPU 95A @BBL A1'
|
||||||
|
when 'PC' then 'Generic PC @BBL A1'
|
||||||
|
end,
|
||||||
|
true
|
||||||
|
from p
|
||||||
|
cross join m
|
||||||
|
on conflict (printer_machine_profile_id, filament_material_type_id) do update
|
||||||
|
set orca_filament_profile_name = excluded.orca_filament_profile_name,
|
||||||
|
is_active = excluded.is_active;
|
||||||
|
|
||||||
|
|
||||||
-- =========================================================
|
-- =========================================================
|
||||||
-- 6) Layer heights (opzioni)
|
-- 6) Layer heights (opzioni)
|
||||||
@@ -420,6 +604,7 @@ CREATE TABLE IF NOT EXISTS quote_line_items
|
|||||||
original_filename text NOT NULL,
|
original_filename text NOT NULL,
|
||||||
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
||||||
color_code text, -- es: white/black o codice interno
|
color_code text, -- es: white/black o codice interno
|
||||||
|
filament_variant_id bigint REFERENCES filament_variant (filament_variant_id),
|
||||||
|
|
||||||
-- Output slicing / calcolo
|
-- Output slicing / calcolo
|
||||||
bounding_box_x_mm numeric(10, 3),
|
bounding_box_x_mm numeric(10, 3),
|
||||||
@@ -529,6 +714,7 @@ CREATE TABLE IF NOT EXISTS order_items
|
|||||||
sha256_hex text, -- opzionale, utile anche per dedup interno
|
sha256_hex text, -- opzionale, utile anche per dedup interno
|
||||||
|
|
||||||
material_code text NOT NULL,
|
material_code text NOT NULL,
|
||||||
|
filament_variant_id bigint REFERENCES filament_variant (filament_variant_id),
|
||||||
color_code text,
|
color_code text,
|
||||||
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface ColorOption {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
hex: string;
|
hex: string;
|
||||||
|
variantId?: number;
|
||||||
outOfStock?: boolean;
|
outOfStock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,14 @@
|
|||||||
|
|
||||||
<div class="content" *ngIf="!loading; else loadingTpl">
|
<div class="content" *ngIf="!loading; else loadingTpl">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
<h3>Inserimento rapido</h3>
|
<h3>Inserimento rapido</h3>
|
||||||
|
<button type="button" class="panel-toggle" (click)="toggleQuickInsertCollapsed()">
|
||||||
|
{{ quickInsertCollapsed ? 'Espandi' : 'Collassa' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!quickInsertCollapsed; else quickInsertCollapsedTpl">
|
||||||
<div class="create-grid">
|
<div class="create-grid">
|
||||||
<section class="subpanel">
|
<section class="subpanel">
|
||||||
<h4>Nuovo materiale</h4>
|
<h4>Nuovo materiale</h4>
|
||||||
@@ -69,6 +76,25 @@
|
|||||||
<span>Colore</span>
|
<span>Colore</span>
|
||||||
<input type="text" [(ngModel)]="newVariant.colorName" placeholder="Nero, Bianco..." />
|
<input type="text" [(ngModel)]="newVariant.colorName" placeholder="Nero, Bianco..." />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Hex colore</span>
|
||||||
|
<input type="text" [(ngModel)]="newVariant.colorHex" placeholder="#1A1A1A" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Finitura</span>
|
||||||
|
<select [(ngModel)]="newVariant.finishType">
|
||||||
|
<option value="GLOSSY">GLOSSY</option>
|
||||||
|
<option value="MATTE">MATTE</option>
|
||||||
|
<option value="MARBLE">MARBLE</option>
|
||||||
|
<option value="SILK">SILK</option>
|
||||||
|
<option value="TRANSLUCENT">TRANSLUCENT</option>
|
||||||
|
<option value="SPECIAL">SPECIAL</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Brand</span>
|
||||||
|
<input type="text" [(ngModel)]="newVariant.brand" placeholder="Bambu, SUNLU..." />
|
||||||
|
</label>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>Costo CHF/kg</span>
|
<span>Costo CHF/kg</span>
|
||||||
<input type="number" step="0.01" min="0" [(ngModel)]="newVariant.costChfPerKg" />
|
<input type="number" step="0.01" min="0" [(ngModel)]="newVariant.costChfPerKg" />
|
||||||
@@ -99,7 +125,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="variant-meta">
|
<p class="variant-meta">
|
||||||
Stock stimato: <strong>{{ computeStockKg(newVariant.stockSpools, newVariant.spoolNetKg) | number:'1.0-3' }} kg</strong>
|
Stock spools: <strong>{{ newVariant.stockSpools | number:'1.0-3' }}</strong> |
|
||||||
|
Filamento totale: <strong>{{ computeStockFilamentGrams(newVariant.stockSpools, newVariant.spoolNetKg) | number:'1.0-0' }} g</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button type="button" (click)="createVariant()" [disabled]="creatingVariant || !materials.length">
|
<button type="button" (click)="createVariant()" [disabled]="creatingVariant || !materials.length">
|
||||||
@@ -107,6 +134,127 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h3>Varianti filamento</h3>
|
||||||
|
<div class="variant-list">
|
||||||
|
<article class="variant-row" *ngFor="let variant of variants; trackBy: trackById">
|
||||||
|
<div class="variant-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="expand-toggle"
|
||||||
|
(click)="toggleVariantExpanded(variant.id)"
|
||||||
|
[attr.aria-expanded]="isVariantExpanded(variant.id)">
|
||||||
|
{{ isVariantExpanded(variant.id) ? '▾' : '▸' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="variant-head-main">
|
||||||
|
<strong>{{ variant.variantDisplayName }}</strong>
|
||||||
|
<div class="variant-collapsed-summary" *ngIf="!isVariantExpanded(variant.id)">
|
||||||
|
<span class="color-summary">
|
||||||
|
<span class="color-dot" [style.background-color]="getVariantColorHex(variant)"></span>
|
||||||
|
{{ variant.colorName || 'N/D' }}
|
||||||
|
</span>
|
||||||
|
<span>Stock spools: {{ variant.stockSpools | number:'1.0-3' }}</span>
|
||||||
|
<span>Filamento: {{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variant-head-actions">
|
||||||
|
<span class="badge low" *ngIf="isLowStock(variant)">Stock basso</span>
|
||||||
|
<span class="badge ok" *ngIf="!isLowStock(variant)">Stock ok</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-delete"
|
||||||
|
(click)="openDeleteVariant(variant)"
|
||||||
|
[disabled]="deletingVariantIds.has(variant.id)">
|
||||||
|
{{ deletingVariantIds.has(variant.id) ? 'Eliminazione...' : 'Elimina' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid" *ngIf="isVariantExpanded(variant.id)">
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Materiale</span>
|
||||||
|
<select [(ngModel)]="variant.materialTypeId">
|
||||||
|
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
|
||||||
|
{{ material.materialCode }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Nome variante</span>
|
||||||
|
<input type="text" [(ngModel)]="variant.variantDisplayName" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Colore</span>
|
||||||
|
<input type="text" [(ngModel)]="variant.colorName" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Hex colore</span>
|
||||||
|
<input type="text" [(ngModel)]="variant.colorHex" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Finitura</span>
|
||||||
|
<select [(ngModel)]="variant.finishType">
|
||||||
|
<option value="GLOSSY">GLOSSY</option>
|
||||||
|
<option value="MATTE">MATTE</option>
|
||||||
|
<option value="MARBLE">MARBLE</option>
|
||||||
|
<option value="SILK">SILK</option>
|
||||||
|
<option value="TRANSLUCENT">TRANSLUCENT</option>
|
||||||
|
<option value="SPECIAL">SPECIAL</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Brand</span>
|
||||||
|
<input type="text" [(ngModel)]="variant.brand" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Costo CHF/kg</span>
|
||||||
|
<input type="number" step="0.01" min="0" [(ngModel)]="variant.costChfPerKg" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Stock spool</span>
|
||||||
|
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="variant.stockSpools" />
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Spool netto kg</span>
|
||||||
|
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="variant.spoolNetKg" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-group" *ngIf="isVariantExpanded(variant.id)">
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" [(ngModel)]="variant.isMatte" />
|
||||||
|
<span>Matte</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" [(ngModel)]="variant.isSpecial" />
|
||||||
|
<span>Special</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" [(ngModel)]="variant.isActive" />
|
||||||
|
<span>Attiva</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="variant-meta" *ngIf="isVariantExpanded(variant.id)">
|
||||||
|
Stock spools: <strong>{{ variant.stockSpools | number:'1.0-3' }}</strong> |
|
||||||
|
Filamento totale: <strong>{{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
*ngIf="isVariantExpanded(variant.id)"
|
||||||
|
(click)="saveVariant(variant)"
|
||||||
|
[disabled]="savingVariantIds.has(variant.id)">
|
||||||
|
{{ savingVariantIds.has(variant.id) ? 'Salvataggio...' : 'Salva variante' }}
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<p class="muted" *ngIf="variants.length === 0">Nessuna variante configurata.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -150,74 +298,6 @@
|
|||||||
<p class="muted" *ngIf="materials.length === 0">Nessun materiale configurato.</p>
|
<p class="muted" *ngIf="materials.length === 0">Nessun materiale configurato.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h3>Varianti filamento</h3>
|
|
||||||
<div class="variant-grid">
|
|
||||||
<article class="variant-card" *ngFor="let variant of variants; trackBy: trackById">
|
|
||||||
<div class="variant-header">
|
|
||||||
<strong>{{ variant.variantDisplayName }}</strong>
|
|
||||||
<span class="badge low" *ngIf="isLowStock(variant)">Stock basso</span>
|
|
||||||
<span class="badge ok" *ngIf="!isLowStock(variant)">Stock ok</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-grid">
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Materiale</span>
|
|
||||||
<select [(ngModel)]="variant.materialTypeId">
|
|
||||||
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
|
|
||||||
{{ material.materialCode }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Nome variante</span>
|
|
||||||
<input type="text" [(ngModel)]="variant.variantDisplayName" />
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Colore</span>
|
|
||||||
<input type="text" [(ngModel)]="variant.colorName" />
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Costo CHF/kg</span>
|
|
||||||
<input type="number" step="0.01" min="0" [(ngModel)]="variant.costChfPerKg" />
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Stock spool</span>
|
|
||||||
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="variant.stockSpools" />
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Spool netto kg</span>
|
|
||||||
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="variant.spoolNetKg" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toggle-group">
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" [(ngModel)]="variant.isMatte" />
|
|
||||||
<span>Matte</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" [(ngModel)]="variant.isSpecial" />
|
|
||||||
<span>Special</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" [(ngModel)]="variant.isActive" />
|
|
||||||
<span>Attiva</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="variant-meta">
|
|
||||||
Totale stimato: <strong>{{ computeStockKg(variant.stockSpools, variant.spoolNetKg) | number:'1.0-3' }} kg</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button type="button" (click)="saveVariant(variant)" [disabled]="savingVariantIds.has(variant.id)">
|
|
||||||
{{ savingVariantIds.has(variant.id) ? 'Salvataggio...' : 'Salva variante' }}
|
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<p class="muted" *ngIf="variants.length === 0">Nessuna variante configurata.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -228,3 +308,24 @@
|
|||||||
<ng-template #materialsCollapsedTpl>
|
<ng-template #materialsCollapsedTpl>
|
||||||
<p class="muted">Sezione collassata ({{ materials.length }} materiali).</p>
|
<p class="muted">Sezione collassata ({{ materials.length }} materiali).</p>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #quickInsertCollapsedTpl>
|
||||||
|
<p class="muted">Sezione collassata.</p>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop" *ngIf="variantToDelete" (click)="closeDeleteVariantDialog()"></div>
|
||||||
|
<div class="confirm-dialog" *ngIf="variantToDelete">
|
||||||
|
<h4>Sei sicuro?</h4>
|
||||||
|
<p>Vuoi eliminare la variante <strong>{{ variantToDelete?.variantDisplayName }}</strong>?</p>
|
||||||
|
<p class="muted">L'operazione non è reversibile.</p>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" class="btn-secondary" (click)="closeDeleteVariantDialog()">Annulla</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-delete"
|
||||||
|
(click)="confirmDeleteVariant()"
|
||||||
|
[disabled]="variantToDelete && deletingVariantIds.has(variantToDelete.id)">
|
||||||
|
{{ variantToDelete && deletingVariantIds.has(variantToDelete.id) ? 'Eliminazione...' : 'Conferma elimina' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -141,24 +141,30 @@ select:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.material-grid,
|
.material-grid,
|
||||||
.variant-grid {
|
.variant-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-card,
|
.material-card,
|
||||||
.variant-card {
|
.variant-row {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--color-neutral-100);
|
background: var(--color-neutral-100);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.material-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.variant-header {
|
.variant-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: flex-start;
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
@@ -167,6 +173,57 @@ select:disabled {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.variant-head-main {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-collapsed-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-head-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-toggle {
|
||||||
|
min-width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-toggle:hover:not(:disabled) {
|
||||||
|
background: var(--color-neutral-100);
|
||||||
|
}
|
||||||
|
|
||||||
.variant-meta {
|
.variant-meta {
|
||||||
margin: 0 0 var(--space-3);
|
margin: 0 0 var(--space-3);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -203,6 +260,25 @@ button:disabled {
|
|||||||
background: var(--color-neutral-100);
|
background: var(--color-neutral-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover:not(:disabled) {
|
||||||
|
background: #bb2d3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: var(--color-neutral-100);
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -236,6 +312,43 @@ button:disabled {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.32);
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: min(460px, calc(100vw - 2rem));
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
padding: var(--space-4);
|
||||||
|
z-index: 1101;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.create-grid {
|
.create-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
AdminUpsertFilamentVariantPayload
|
AdminUpsertFilamentVariantPayload
|
||||||
} from '../services/admin-operations.service';
|
} from '../services/admin-operations.service';
|
||||||
import { forkJoin } from 'rxjs';
|
import { forkJoin } from 'rxjs';
|
||||||
|
import { getColorHex } from '../../../core/constants/colors.const';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-filament-stock',
|
selector: 'app-admin-filament-stock',
|
||||||
@@ -23,11 +24,15 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
materials: AdminFilamentMaterialType[] = [];
|
materials: AdminFilamentMaterialType[] = [];
|
||||||
variants: AdminFilamentVariant[] = [];
|
variants: AdminFilamentVariant[] = [];
|
||||||
loading = false;
|
loading = false;
|
||||||
|
quickInsertCollapsed = false;
|
||||||
materialsCollapsed = true;
|
materialsCollapsed = true;
|
||||||
creatingMaterial = false;
|
creatingMaterial = false;
|
||||||
creatingVariant = false;
|
creatingVariant = false;
|
||||||
savingMaterialIds = new Set<number>();
|
savingMaterialIds = new Set<number>();
|
||||||
savingVariantIds = new Set<number>();
|
savingVariantIds = new Set<number>();
|
||||||
|
deletingVariantIds = new Set<number>();
|
||||||
|
expandedVariantIds = new Set<number>();
|
||||||
|
variantToDelete: AdminFilamentVariant | null = null;
|
||||||
errorMessage: string | null = null;
|
errorMessage: string | null = null;
|
||||||
successMessage: string | null = null;
|
successMessage: string | null = null;
|
||||||
|
|
||||||
@@ -42,6 +47,9 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
materialTypeId: 0,
|
materialTypeId: 0,
|
||||||
variantDisplayName: '',
|
variantDisplayName: '',
|
||||||
colorName: '',
|
colorName: '',
|
||||||
|
colorHex: '',
|
||||||
|
finishType: 'GLOSSY',
|
||||||
|
brand: '',
|
||||||
isMatte: false,
|
isMatte: false,
|
||||||
isSpecial: false,
|
isSpecial: false,
|
||||||
costChfPerKg: 0,
|
costChfPerKg: 0,
|
||||||
@@ -66,6 +74,12 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
next: ({ materials, variants }) => {
|
next: ({ materials, variants }) => {
|
||||||
this.materials = this.sortMaterials(materials);
|
this.materials = this.sortMaterials(materials);
|
||||||
this.variants = this.sortVariants(variants);
|
this.variants = this.sortVariants(variants);
|
||||||
|
const existingIds = new Set(this.variants.map(v => v.id));
|
||||||
|
this.expandedVariantIds.forEach(id => {
|
||||||
|
if (!existingIds.has(id)) {
|
||||||
|
this.expandedVariantIds.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (!this.newVariant.materialTypeId && this.materials.length > 0) {
|
if (!this.newVariant.materialTypeId && this.materials.length > 0) {
|
||||||
this.newVariant.materialTypeId = this.materials[0].id;
|
this.newVariant.materialTypeId = this.materials[0].id;
|
||||||
}
|
}
|
||||||
@@ -178,6 +192,9 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
materialTypeId: this.newVariant.materialTypeId || this.materials[0]?.id || 0,
|
materialTypeId: this.newVariant.materialTypeId || this.materials[0]?.id || 0,
|
||||||
variantDisplayName: '',
|
variantDisplayName: '',
|
||||||
colorName: '',
|
colorName: '',
|
||||||
|
colorHex: '',
|
||||||
|
finishType: 'GLOSSY',
|
||||||
|
brand: '',
|
||||||
isMatte: false,
|
isMatte: false,
|
||||||
isSpecial: false,
|
isSpecial: false,
|
||||||
costChfPerKg: 0,
|
costChfPerKg: 0,
|
||||||
@@ -221,7 +238,7 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLowStock(variant: AdminFilamentVariant): boolean {
|
isLowStock(variant: AdminFilamentVariant): boolean {
|
||||||
return this.computeStockKg(variant.stockSpools, variant.spoolNetKg) < 1;
|
return this.computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) < 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
computeStockKg(stockSpools?: number, spoolNetKg?: number): number {
|
computeStockKg(stockSpools?: number, spoolNetKg?: number): number {
|
||||||
@@ -234,19 +251,82 @@ export class AdminFilamentStockComponent implements OnInit {
|
|||||||
return spools * netKg;
|
return spools * netKg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeStockFilamentGrams(stockSpools?: number, spoolNetKg?: number): number {
|
||||||
|
return this.computeStockKg(stockSpools, spoolNetKg) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
trackById(index: number, item: { id: number }): number {
|
trackById(index: number, item: { id: number }): number {
|
||||||
return item.id;
|
return item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isVariantExpanded(variantId: number): boolean {
|
||||||
|
return this.expandedVariantIds.has(variantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVariantExpanded(variantId: number): void {
|
||||||
|
if (this.expandedVariantIds.has(variantId)) {
|
||||||
|
this.expandedVariantIds.delete(variantId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.expandedVariantIds.add(variantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariantColorHex(variant: AdminFilamentVariant): string {
|
||||||
|
if (variant.colorHex && variant.colorHex.trim().length > 0) {
|
||||||
|
return variant.colorHex;
|
||||||
|
}
|
||||||
|
return getColorHex(variant.colorName || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteVariant(variant: AdminFilamentVariant): void {
|
||||||
|
this.variantToDelete = variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDeleteVariantDialog(): void {
|
||||||
|
this.variantToDelete = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteVariant(): void {
|
||||||
|
const variant = this.variantToDelete;
|
||||||
|
if (!variant || this.deletingVariantIds.has(variant.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.successMessage = null;
|
||||||
|
this.deletingVariantIds.add(variant.id);
|
||||||
|
|
||||||
|
this.adminOperationsService.deleteFilamentVariant(variant.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.variants = this.variants.filter(v => v.id !== variant.id);
|
||||||
|
this.expandedVariantIds.delete(variant.id);
|
||||||
|
this.deletingVariantIds.delete(variant.id);
|
||||||
|
this.variantToDelete = null;
|
||||||
|
this.successMessage = 'Variante eliminata.';
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.deletingVariantIds.delete(variant.id);
|
||||||
|
this.errorMessage = this.extractErrorMessage(err, 'Eliminazione variante non riuscita.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggleMaterialsCollapsed(): void {
|
toggleMaterialsCollapsed(): void {
|
||||||
this.materialsCollapsed = !this.materialsCollapsed;
|
this.materialsCollapsed = !this.materialsCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleQuickInsertCollapsed(): void {
|
||||||
|
this.quickInsertCollapsed = !this.quickInsertCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
private toVariantPayload(source: AdminUpsertFilamentVariantPayload | AdminFilamentVariant): AdminUpsertFilamentVariantPayload {
|
private toVariantPayload(source: AdminUpsertFilamentVariantPayload | AdminFilamentVariant): AdminUpsertFilamentVariantPayload {
|
||||||
return {
|
return {
|
||||||
materialTypeId: Number(source.materialTypeId),
|
materialTypeId: Number(source.materialTypeId),
|
||||||
variantDisplayName: (source.variantDisplayName || '').trim(),
|
variantDisplayName: (source.variantDisplayName || '').trim(),
|
||||||
colorName: (source.colorName || '').trim(),
|
colorName: (source.colorName || '').trim(),
|
||||||
|
colorHex: (source.colorHex || '').trim() || undefined,
|
||||||
|
finishType: (source.finishType || 'GLOSSY').trim().toUpperCase(),
|
||||||
|
brand: (source.brand || '').trim() || undefined,
|
||||||
isMatte: !!source.isMatte,
|
isMatte: !!source.isMatte,
|
||||||
isSpecial: !!source.isSpecial,
|
isSpecial: !!source.isSpecial,
|
||||||
costChfPerKg: Number(source.costChfPerKg ?? 0),
|
costChfPerKg: Number(source.costChfPerKg ?? 0),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface AdminFilamentStockRow {
|
|||||||
stockSpools: number;
|
stockSpools: number;
|
||||||
spoolNetKg: number;
|
spoolNetKg: number;
|
||||||
stockKg: number;
|
stockKg: number;
|
||||||
|
stockFilamentGrams: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +32,16 @@ export interface AdminFilamentVariant {
|
|||||||
materialTechnicalTypeLabel?: string;
|
materialTechnicalTypeLabel?: string;
|
||||||
variantDisplayName: string;
|
variantDisplayName: string;
|
||||||
colorName: string;
|
colorName: string;
|
||||||
|
colorHex?: string;
|
||||||
|
finishType?: string;
|
||||||
|
brand?: string;
|
||||||
isMatte: boolean;
|
isMatte: boolean;
|
||||||
isSpecial: boolean;
|
isSpecial: boolean;
|
||||||
costChfPerKg: number;
|
costChfPerKg: number;
|
||||||
stockSpools: number;
|
stockSpools: number;
|
||||||
spoolNetKg: number;
|
spoolNetKg: number;
|
||||||
stockKg: number;
|
stockKg: number;
|
||||||
|
stockFilamentGrams: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -52,6 +57,9 @@ export interface AdminUpsertFilamentVariantPayload {
|
|||||||
materialTypeId: number;
|
materialTypeId: number;
|
||||||
variantDisplayName: string;
|
variantDisplayName: string;
|
||||||
colorName: string;
|
colorName: string;
|
||||||
|
colorHex?: string;
|
||||||
|
finishType?: string;
|
||||||
|
brand?: string;
|
||||||
isMatte: boolean;
|
isMatte: boolean;
|
||||||
isSpecial: boolean;
|
isSpecial: boolean;
|
||||||
costChfPerKg: number;
|
costChfPerKg: number;
|
||||||
@@ -167,6 +175,10 @@ export class AdminOperationsService {
|
|||||||
return this.http.put<AdminFilamentVariant>(`${this.baseUrl}/filaments/variants/${variantId}`, payload, { withCredentials: true });
|
return this.http.put<AdminFilamentVariant>(`${this.baseUrl}/filaments/variants/${variantId}`, payload, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteFilamentVariant(variantId: number): Observable<void> {
|
||||||
|
return this.http.delete<void>(`${this.baseUrl}/filaments/variants/${variantId}`, { withCredentials: true });
|
||||||
|
}
|
||||||
|
|
||||||
getContactRequests(): Observable<AdminContactRequest[]> {
|
getContactRequests(): Observable<AdminContactRequest[]> {
|
||||||
return this.http.get<AdminContactRequest[]>(`${this.baseUrl}/contact-requests`, { withCredentials: true });
|
return this.http.get<AdminContactRequest[]>(`${this.baseUrl}/contact-requests`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,10 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
// Assuming index matches.
|
// Assuming index matches.
|
||||||
// Need to be careful if items order changed, but usually ID sort or insert order.
|
// Need to be careful if items order changed, but usually ID sort or insert order.
|
||||||
if (item.colorCode) {
|
if (item.colorCode) {
|
||||||
this.uploadForm.updateItemColor(index, item.colorCode);
|
this.uploadForm.updateItemColor(index, {
|
||||||
|
colorName: item.colorCode,
|
||||||
|
filamentVariantId: item.filamentVariantId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<label>{{ 'CALC.COLOR_LABEL' | translate }}</label>
|
<label>{{ 'CALC.COLOR_LABEL' | translate }}</label>
|
||||||
<app-color-selector
|
<app-color-selector
|
||||||
[selectedColor]="item.color"
|
[selectedColor]="item.color"
|
||||||
|
[selectedVariantId]="item.filamentVariantId ?? null"
|
||||||
[variants]="currentMaterialVariants()"
|
[variants]="currentMaterialVariants()"
|
||||||
(colorSelected)="updateItemColor(i, $event)">
|
(colorSelected)="updateItemColor(i, $event)">
|
||||||
</app-color-selector>
|
</app-color-selector>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface FormItem {
|
|||||||
file: File;
|
file: File;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
color: string;
|
color: string;
|
||||||
|
filamentVariantId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,6 +59,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (matCode && this.fullMaterialOptions.length > 0) {
|
if (matCode && this.fullMaterialOptions.length > 0) {
|
||||||
const found = this.fullMaterialOptions.find(m => m.code === matCode);
|
const found = this.fullMaterialOptions.find(m => m.code === matCode);
|
||||||
this.currentMaterialVariants.set(found ? found.variants : []);
|
this.currentMaterialVariants.set(found ? found.variants : []);
|
||||||
|
this.syncItemVariantSelections();
|
||||||
} else {
|
} else {
|
||||||
this.currentMaterialVariants.set([]);
|
this.currentMaterialVariants.set([]);
|
||||||
}
|
}
|
||||||
@@ -166,8 +168,13 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (file.size > MAX_SIZE) {
|
if (file.size > MAX_SIZE) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
} else {
|
} else {
|
||||||
// Default color is Black
|
const defaultSelection = this.getDefaultVariantSelection();
|
||||||
validItems.push({ file, quantity: 1, color: 'Black' });
|
validItems.push({
|
||||||
|
file,
|
||||||
|
quantity: 1,
|
||||||
|
color: defaultSelection.colorName,
|
||||||
|
filamentVariantId: defaultSelection.filamentVariantId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +227,9 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (item) {
|
if (item) {
|
||||||
const vars = this.currentMaterialVariants();
|
const vars = this.currentMaterialVariants();
|
||||||
if (vars && vars.length > 0) {
|
if (vars && vars.length > 0) {
|
||||||
const found = vars.find(v => v.colorName === item.color);
|
const found = item.filamentVariantId
|
||||||
|
? vars.find(v => v.id === item.filamentVariantId)
|
||||||
|
: vars.find(v => v.colorName === item.color);
|
||||||
if (found) return found.hexColor;
|
if (found) return found.hexColor;
|
||||||
}
|
}
|
||||||
return getColorHex(item.color);
|
return getColorHex(item.color);
|
||||||
@@ -240,10 +249,12 @@ export class UploadFormComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateItemColor(index: number, newColor: string) {
|
updateItemColor(index: number, newSelection: string | { colorName: string; filamentVariantId?: number }) {
|
||||||
|
const colorName = typeof newSelection === 'string' ? newSelection : newSelection.colorName;
|
||||||
|
const filamentVariantId = typeof newSelection === 'string' ? undefined : newSelection.filamentVariantId;
|
||||||
this.items.update(current => {
|
this.items.update(current => {
|
||||||
const updated = [...current];
|
const updated = [...current];
|
||||||
updated[index] = { ...updated[index], color: newColor };
|
updated[index] = { ...updated[index], color: colorName, filamentVariantId };
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -261,9 +272,14 @@ export class UploadFormComponent implements OnInit {
|
|||||||
|
|
||||||
setFiles(files: File[]) {
|
setFiles(files: File[]) {
|
||||||
const validItems: FormItem[] = [];
|
const validItems: FormItem[] = [];
|
||||||
|
const defaultSelection = this.getDefaultVariantSelection();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Default color is Black or derive from somewhere if possible, but here we just init
|
validItems.push({
|
||||||
validItems.push({ file, quantity: 1, color: 'Black' });
|
file,
|
||||||
|
quantity: 1,
|
||||||
|
color: defaultSelection.colorName,
|
||||||
|
filamentVariantId: defaultSelection.filamentVariantId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validItems.length > 0) {
|
if (validItems.length > 0) {
|
||||||
@@ -274,6 +290,39 @@ export class UploadFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultVariantSelection(): { colorName: string; filamentVariantId?: number } {
|
||||||
|
const vars = this.currentMaterialVariants();
|
||||||
|
if (vars && vars.length > 0) {
|
||||||
|
const preferred = vars.find(v => !v.isOutOfStock) || vars[0];
|
||||||
|
return {
|
||||||
|
colorName: preferred.colorName,
|
||||||
|
filamentVariantId: preferred.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { colorName: 'Black' };
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncItemVariantSelections(): void {
|
||||||
|
const vars = this.currentMaterialVariants();
|
||||||
|
if (!vars || vars.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = vars.find(v => !v.isOutOfStock) || vars[0];
|
||||||
|
this.items.update(current => current.map(item => {
|
||||||
|
const byId = item.filamentVariantId != null
|
||||||
|
? vars.find(v => v.id === item.filamentVariantId)
|
||||||
|
: null;
|
||||||
|
const byColor = vars.find(v => v.colorName === item.color);
|
||||||
|
const selected = byId || byColor || fallback;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
color: selected.colorName,
|
||||||
|
filamentVariantId: selected.id
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
patchSettings(settings: any) {
|
patchSettings(settings: any) {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
// settings object matches keys in our form?
|
// settings object matches keys in our form?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, catchError, tap } from 'rxjs/operators';
|
|||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
export interface QuoteRequest {
|
export interface QuoteRequest {
|
||||||
items: { file: File, quantity: number, color?: string }[];
|
items: { file: File, quantity: number, color?: string, filamentVariantId?: number }[];
|
||||||
material: string;
|
material: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -26,6 +26,7 @@ export interface QuoteItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
material?: string;
|
material?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
filamentVariantId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuoteResult {
|
export interface QuoteResult {
|
||||||
@@ -72,9 +73,13 @@ export interface MaterialOption {
|
|||||||
variants: VariantOption[];
|
variants: VariantOption[];
|
||||||
}
|
}
|
||||||
export interface VariantOption {
|
export interface VariantOption {
|
||||||
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
colorName: string;
|
colorName: string;
|
||||||
hexColor: string;
|
hexColor: string;
|
||||||
|
finishType: string;
|
||||||
|
stockSpools: number;
|
||||||
|
stockFilamentGrams: number;
|
||||||
isOutOfStock: boolean;
|
isOutOfStock: boolean;
|
||||||
}
|
}
|
||||||
export interface QualityOption {
|
export interface QualityOption {
|
||||||
@@ -250,6 +255,7 @@ export class QuoteEstimatorService {
|
|||||||
const settings = {
|
const settings = {
|
||||||
complexityMode: request.mode === 'easy' ? 'ADVANCED' : request.mode.toUpperCase(),
|
complexityMode: request.mode === 'easy' ? 'ADVANCED' : request.mode.toUpperCase(),
|
||||||
material: request.material,
|
material: request.material,
|
||||||
|
filamentVariantId: item.filamentVariantId,
|
||||||
quality: easyPreset ? easyPreset.quality : request.quality,
|
quality: easyPreset ? easyPreset.quality : request.quality,
|
||||||
supportsEnabled: request.supportEnabled,
|
supportsEnabled: request.supportEnabled,
|
||||||
color: item.color || '#FFFFFF',
|
color: item.color || '#FFFFFF',
|
||||||
@@ -351,7 +357,8 @@ export class QuoteEstimatorService {
|
|||||||
material: session.materialCode, // Assumption: session has one material for all? or items have it?
|
material: session.materialCode, // Assumption: session has one material for all? or items have it?
|
||||||
// Backend model QuoteSession has materialCode.
|
// Backend model QuoteSession has materialCode.
|
||||||
// But line items might have different colors.
|
// But line items might have different colors.
|
||||||
color: item.colorCode
|
color: item.colorCode,
|
||||||
|
filamentVariantId: item.filamentVariantId
|
||||||
})),
|
})),
|
||||||
setupCost: session.setupCostChf || 0,
|
setupCost: session.setupCostChf || 0,
|
||||||
globalMachineCost: sessionData.globalMachineCostChf || 0,
|
globalMachineCost: sessionData.globalMachineCostChf || 0,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
[class.disabled]="color.outOfStock">
|
[class.disabled]="color.outOfStock">
|
||||||
|
|
||||||
<div class="selection-ring"
|
<div class="selection-ring"
|
||||||
[class.active]="selectedColor() === color.value"
|
[class.active]="selectedVariantId() ? selectedVariantId() === color.variantId : selectedColor() === color.value"
|
||||||
[class.out-of-stock]="color.outOfStock">
|
[class.out-of-stock]="color.outOfStock">
|
||||||
<div class="color-circle small" [style.background-color]="color.hex"></div>
|
<div class="color-circle small" [style.background-color]="color.hex"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,25 +13,33 @@ import { VariantOption } from '../../../features/calculator/services/quote-estim
|
|||||||
})
|
})
|
||||||
export class ColorSelectorComponent {
|
export class ColorSelectorComponent {
|
||||||
selectedColor = input<string>('Black');
|
selectedColor = input<string>('Black');
|
||||||
|
selectedVariantId = input<number | null>(null);
|
||||||
variants = input<VariantOption[]>([]);
|
variants = input<VariantOption[]>([]);
|
||||||
colorSelected = output<string>();
|
colorSelected = output<{ colorName: string; filamentVariantId?: number }>();
|
||||||
|
|
||||||
isOpen = signal(false);
|
isOpen = signal(false);
|
||||||
|
|
||||||
categories = computed(() => {
|
categories = computed(() => {
|
||||||
const vars = this.variants();
|
const vars = this.variants();
|
||||||
if (vars && vars.length > 0) {
|
if (vars && vars.length > 0) {
|
||||||
// Flatten variants into a single category for now
|
const byFinish = new Map<string, ColorOption[]>();
|
||||||
// We could try to group by extracting words, but "Colors" is fine.
|
vars.forEach(v => {
|
||||||
return [{
|
const finish = v.finishType || 'AVAILABLE_COLORS';
|
||||||
name: 'COLOR.AVAILABLE_COLORS',
|
const bucket = byFinish.get(finish) || [];
|
||||||
colors: vars.map(v => ({
|
bucket.push({
|
||||||
label: v.colorName, // Display "Red"
|
label: v.colorName,
|
||||||
value: v.colorName, // Send "Red" to backend
|
value: v.colorName,
|
||||||
hex: v.hexColor,
|
hex: v.hexColor,
|
||||||
|
variantId: v.id,
|
||||||
outOfStock: v.isOutOfStock
|
outOfStock: v.isOutOfStock
|
||||||
}))
|
});
|
||||||
}] as ColorCategory[];
|
byFinish.set(finish, bucket);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(byFinish.entries()).map(([finish, colors]) => ({
|
||||||
|
name: finish,
|
||||||
|
colors
|
||||||
|
})) as ColorCategory[];
|
||||||
}
|
}
|
||||||
return PRODUCT_COLORS;
|
return PRODUCT_COLORS;
|
||||||
});
|
});
|
||||||
@@ -43,7 +51,10 @@ export class ColorSelectorComponent {
|
|||||||
selectColor(color: ColorOption) {
|
selectColor(color: ColorOption) {
|
||||||
if (color.outOfStock) return;
|
if (color.outOfStock) return;
|
||||||
|
|
||||||
this.colorSelected.emit(color.value);
|
this.colorSelected.emit({
|
||||||
|
colorName: color.value,
|
||||||
|
filamentVariantId: color.variantId
|
||||||
|
});
|
||||||
this.isOpen.set(false);
|
this.isOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,8 +133,6 @@
|
|||||||
"TITLE": "Über uns",
|
"TITLE": "Über uns",
|
||||||
"EYEBROW": "3D-Druck-Labor",
|
"EYEBROW": "3D-Druck-Labor",
|
||||||
"SUBTITLE": "Wir sind zwei Studenten mit viel Motivation und Lernbereitschaft.",
|
"SUBTITLE": "Wir sind zwei Studenten mit viel Motivation und Lernbereitschaft.",
|
||||||
"HOW_TEXT": "3D Fab entstand aus Matteos anfänglichem Interesse am 3D-Druck. Er kaufte einen Drucker und begann ernsthaft zu experimentieren.\nIrgendwann kamen die ersten Anfragen: ein gebrochenes Teil zum Ersetzen, ein Ersatzteil, das man nicht findet, ein praktischer Adapter. Die Anfragen nahmen zu und wir sagten uns: okay, machen wir es richtig.\nSpäter haben wir einen Rechner entwickelt, um die Kosten im Voraus zu verstehen: das war einer der ersten Schritte vom „wir machen ein paar Teile“ zu einem echten Projekt – gemeinsam.",
|
|
||||||
"PASSIONS_TITLE": "Unsere Leidenschaften",
|
|
||||||
"PASSION_BIKE_TRIAL": "Bike Trial",
|
"PASSION_BIKE_TRIAL": "Bike Trial",
|
||||||
"PASSION_MOUNTAIN": "Berge",
|
"PASSION_MOUNTAIN": "Berge",
|
||||||
"PASSION_SKI": "Ski",
|
"PASSION_SKI": "Ski",
|
||||||
|
|||||||
@@ -133,8 +133,6 @@
|
|||||||
"TITLE": "About Us",
|
"TITLE": "About Us",
|
||||||
"EYEBROW": "3D Printing Lab",
|
"EYEBROW": "3D Printing Lab",
|
||||||
"SUBTITLE": "We are two students with a strong desire to build and learn.",
|
"SUBTITLE": "We are two students with a strong desire to build and learn.",
|
||||||
"HOW_TEXT": "3D Fab was born from Matteo's initial interest in 3D printing. He bought a printer and started experimenting seriously. \n At a certain point, the first requests arrived: a broken part to replace, a spare part that cannot be found, a handy adapter to have. The requests increased and we said: okay, let's do it properly.\nLater we created a calculator to understand the cost in advance: it was one of the first steps that took us from \"let's make a few parts\" to a real project, together.",
|
|
||||||
"PASSIONS_TITLE": "Our passions",
|
|
||||||
"PASSION_BIKE_TRIAL": "Bike trial",
|
"PASSION_BIKE_TRIAL": "Bike trial",
|
||||||
"PASSION_MOUNTAIN": "Mountain",
|
"PASSION_MOUNTAIN": "Mountain",
|
||||||
"PASSION_SKI": "Ski",
|
"PASSION_SKI": "Ski",
|
||||||
|
|||||||
@@ -190,8 +190,6 @@
|
|||||||
"TITLE": "Qui sommes-nous",
|
"TITLE": "Qui sommes-nous",
|
||||||
"EYEBROW": "Atelier d'impression 3D",
|
"EYEBROW": "Atelier d'impression 3D",
|
||||||
"SUBTITLE": "Nous sommes deux étudiants avec beaucoup d'envie de faire et d'apprendre.",
|
"SUBTITLE": "Nous sommes deux étudiants avec beaucoup d'envie de faire et d'apprendre.",
|
||||||
"HOW_TEXT": "3D Fab est né de l'intérêt initial de Matteo pour l'impression 3D. Il a acheté une imprimante et a commencé à expérimenter sérieusement. \n À un certain moment, les premières demandes sont arrivées : une pièce cassée à remplacer, une pièce de rechange introuvable, un adaptateur pratique à avoir. Les demandes ont augmenté et nous nous sommes dit : d'accord, faisons-le bien.\nEnsuite, nous avons créé un calculateur pour connaître le coût à l'avance : cela a été l'un des premiers pas qui nous a fait passer de « on fait quelques pièces » à un vrai projet, ensemble.",
|
|
||||||
"PASSIONS_TITLE": "Nos passions",
|
|
||||||
"PASSION_BIKE_TRIAL": "Bike trial",
|
"PASSION_BIKE_TRIAL": "Bike trial",
|
||||||
"PASSION_MOUNTAIN": "Montagne",
|
"PASSION_MOUNTAIN": "Montagne",
|
||||||
"PASSION_SKI": "Ski",
|
"PASSION_SKI": "Ski",
|
||||||
|
|||||||
@@ -190,8 +190,8 @@
|
|||||||
"TITLE": "Chi Siamo",
|
"TITLE": "Chi Siamo",
|
||||||
"EYEBROW": "Laboratorio di stampa 3D",
|
"EYEBROW": "Laboratorio di stampa 3D",
|
||||||
"SUBTITLE": "Siamo due studenti con tanta voglia di fare e di imparare.",
|
"SUBTITLE": "Siamo due studenti con tanta voglia di fare e di imparare.",
|
||||||
"HOW_TEXT": "3D Fab nasce dall’interesse iniziale di Matteo per la stampa 3D. Ha comprato una stampante e ha iniziato a sperimentare sul serio. \n A un certo punto sono arrivate le prime richieste: un pezzo rotto da sostituire, un ricambio che non si trova, un adattatore comodo da avere. Le richieste sono aumentate e ci siamo detti: ok, facciamolo bene.\nIn seguito abbiamo creato un calcolatore per capire il costo in anticipo: è stato uno dei primi passi che ci ha fatto passare dal “facciamo qualche pezzo” a un progetto vero, insieme.",
|
"HOW_TEXT": "3D Fab nasce per trasformare le potenzialità della stampa 3D in soluzioni quotidiane. Siamo partiti dalla curiosità tecnica e siamo arrivati alla produzione di ricambi, prodotti e prototipi su misura. Per passare da un'idea a un progetto concreto abbiamo lanciato il nostro calcolatore automatico: preventivi chiari in un clic per garantirti un servizio professionale e senza sorprese sul prezzo.",
|
||||||
"PASSIONS_TITLE": "Le nostre passioni",
|
"PASSIONS_TITLE": "I nostri interessi",
|
||||||
"PASSION_BIKE_TRIAL": "Bike trial",
|
"PASSION_BIKE_TRIAL": "Bike trial",
|
||||||
"PASSION_MOUNTAIN": "Montagna",
|
"PASSION_MOUNTAIN": "Montagna",
|
||||||
"PASSION_SKI": "Ski",
|
"PASSION_SKI": "Ski",
|
||||||
|
|||||||
Reference in New Issue
Block a user