feat(back-end): db connections and other stuff
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
package com.printcalculator.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "pricing")
|
||||
public class AppProperties {
|
||||
|
||||
private double filamentCostPerKg;
|
||||
private double machineCostPerHour;
|
||||
private double energyCostPerKwh;
|
||||
private double printerPowerWatts;
|
||||
private double markupPercent;
|
||||
|
||||
private String slicerPath;
|
||||
private String profilesRoot;
|
||||
|
||||
// Getters and Setters needed for Spring binding
|
||||
|
||||
public double getFilamentCostPerKg() { return filamentCostPerKg; }
|
||||
public void setFilamentCostPerKg(double filamentCostPerKg) { this.filamentCostPerKg = filamentCostPerKg; }
|
||||
|
||||
public double getMachineCostPerHour() { return machineCostPerHour; }
|
||||
public void setMachineCostPerHour(double machineCostPerHour) { this.machineCostPerHour = machineCostPerHour; }
|
||||
|
||||
public double getEnergyCostPerKwh() { return energyCostPerKwh; }
|
||||
public void setEnergyCostPerKwh(double energyCostPerKwh) { this.energyCostPerKwh = energyCostPerKwh; }
|
||||
|
||||
public double getPrinterPowerWatts() { return printerPowerWatts; }
|
||||
public void setPrinterPowerWatts(double printerPowerWatts) { this.printerPowerWatts = printerPowerWatts; }
|
||||
|
||||
public double getMarkupPercent() { return markupPercent; }
|
||||
public void setMarkupPercent(double markupPercent) { this.markupPercent = markupPercent; }
|
||||
|
||||
// Slicer props are not under "pricing" prefix in properties file?
|
||||
// Wait, in application.properties I put them at root level/custom.
|
||||
// Let's fix this class to map correctly or change prefix.
|
||||
// I'll make a separate section or just bind manually.
|
||||
// Actually, I'll just add @Value in services for simplicity or fix the prefix structure.
|
||||
// Let's stick to standard @Value for simple paths if this is messy.
|
||||
// Or better, creating a dedicated SlicerProperties.
|
||||
}
|
||||
@@ -6,15 +6,14 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@Profile("local")
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedOrigins("http://localhost", "http://localhost:4200", "http://localhost:80", "http://127.0.0.1")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(false);
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.printcalculator.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "")
|
||||
// Hack: standard prefix is usually required. I'll use @Value in service or correct this.
|
||||
// Better: make SlicerConfig class.
|
||||
public class SlicerConfig {
|
||||
// Intentionally empty, will use @Value in service for simplicity
|
||||
// or fix in next step.
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.dto.OptionsResponse;
|
||||
import com.printcalculator.entity.FilamentMaterialType;
|
||||
import com.printcalculator.entity.FilamentVariant;
|
||||
import com.printcalculator.entity.*; // This line replaces specific entity imports
|
||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||
import com.printcalculator.repository.FilamentVariantRepository;
|
||||
import com.printcalculator.repository.LayerHeightOptionRepository;
|
||||
import com.printcalculator.repository.NozzleOptionRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
public class OptionsController {
|
||||
|
||||
private final FilamentMaterialTypeRepository materialRepo;
|
||||
private final FilamentVariantRepository variantRepo;
|
||||
private final LayerHeightOptionRepository layerHeightRepo;
|
||||
private final NozzleOptionRepository nozzleRepo;
|
||||
|
||||
public OptionsController(FilamentMaterialTypeRepository materialRepo,
|
||||
FilamentVariantRepository variantRepo,
|
||||
LayerHeightOptionRepository layerHeightRepo,
|
||||
NozzleOptionRepository nozzleRepo) {
|
||||
this.materialRepo = materialRepo;
|
||||
this.variantRepo = variantRepo;
|
||||
this.layerHeightRepo = layerHeightRepo;
|
||||
this.nozzleRepo = nozzleRepo;
|
||||
}
|
||||
|
||||
@GetMapping("/api/calculator/options")
|
||||
public ResponseEntity<OptionsResponse> getOptions() {
|
||||
// 1. Materials & Variants
|
||||
List<FilamentMaterialType> types = materialRepo.findAll();
|
||||
List<FilamentVariant> allVariants = variantRepo.findAll();
|
||||
|
||||
List<OptionsResponse.MaterialOption> materialOptions = types.stream()
|
||||
.map(type -> {
|
||||
List<OptionsResponse.VariantOption> variants = allVariants.stream()
|
||||
.filter(v -> v.getFilamentMaterialType().getId().equals(type.getId()) && v.getIsActive())
|
||||
.map(v -> new OptionsResponse.VariantOption(
|
||||
v.getVariantDisplayName(),
|
||||
v.getColorName(),
|
||||
getColorHex(v.getColorName()), // Need helper or store hex in DB
|
||||
v.getStockSpools().doubleValue() <= 0
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Only include material if it has active variants
|
||||
if (variants.isEmpty()) return null;
|
||||
|
||||
return new OptionsResponse.MaterialOption(
|
||||
type.getMaterialCode(),
|
||||
type.getMaterialCode() + (type.getIsFlexible() ? " (Flexible)" : " (Standard)"),
|
||||
variants
|
||||
);
|
||||
})
|
||||
.filter(m -> m != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 2. Qualities (Static as per user request)
|
||||
List<OptionsResponse.QualityOption> qualities = List.of(
|
||||
new OptionsResponse.QualityOption("draft", "Draft"),
|
||||
new OptionsResponse.QualityOption("standard", "Standard"),
|
||||
new OptionsResponse.QualityOption("extra_fine", "High Definition")
|
||||
);
|
||||
|
||||
// 3. Infill Patterns (Static as per user request)
|
||||
List<OptionsResponse.InfillPatternOption> patterns = List.of(
|
||||
new OptionsResponse.InfillPatternOption("grid", "Grid"),
|
||||
new OptionsResponse.InfillPatternOption("gyroid", "Gyroid"),
|
||||
new OptionsResponse.InfillPatternOption("cubic", "Cubic")
|
||||
);
|
||||
|
||||
// 4. Layer Heights
|
||||
List<OptionsResponse.LayerHeightOptionDTO> layers = layerHeightRepo.findAll().stream()
|
||||
.filter(l -> l.getIsActive())
|
||||
.sorted(Comparator.comparing(LayerHeightOption::getLayerHeightMm))
|
||||
.map(l -> new OptionsResponse.LayerHeightOptionDTO(
|
||||
l.getLayerHeightMm().doubleValue(),
|
||||
String.format("%.2f mm", l.getLayerHeightMm())
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 5. Nozzles
|
||||
List<OptionsResponse.NozzleOptionDTO> nozzles = nozzleRepo.findAll().stream()
|
||||
.filter(n -> n.getIsActive())
|
||||
.sorted(Comparator.comparing(NozzleOption::getNozzleDiameterMm))
|
||||
.map(n -> new OptionsResponse.NozzleOptionDTO(
|
||||
n.getNozzleDiameterMm().doubleValue(),
|
||||
String.format("%.1f mm%s", n.getNozzleDiameterMm(),
|
||||
n.getExtraNozzleChangeFeeChf().doubleValue() > 0
|
||||
? String.format(" (+ %.2f CHF)", n.getExtraNozzleChangeFeeChf())
|
||||
: " (Standard)")
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(new OptionsResponse(materialOptions, qualities, patterns, layers, nozzles));
|
||||
}
|
||||
|
||||
// Temporary helper until we add hex to DB
|
||||
private String getColorHex(String colorName) {
|
||||
String lower = colorName.toLowerCase();
|
||||
if (lower.contains("black") || lower.contains("nero")) return "#1a1a1a";
|
||||
if (lower.contains("white") || lower.contains("bianco")) return "#f5f5f5";
|
||||
if (lower.contains("blue") || lower.contains("blu")) return "#1976d2";
|
||||
if (lower.contains("red") || lower.contains("rosso")) return "#d32f2f";
|
||||
if (lower.contains("green") || lower.contains("verde")) return "#388e3c";
|
||||
if (lower.contains("orange") || lower.contains("arancione")) return "#ffa726";
|
||||
if (lower.contains("grey") || lower.contains("gray") || lower.contains("grigio")) {
|
||||
if (lower.contains("dark") || lower.contains("scuro")) return "#424242";
|
||||
return "#bdbdbd";
|
||||
}
|
||||
if (lower.contains("purple") || lower.contains("viola")) return "#7b1fa2";
|
||||
if (lower.contains("yellow") || lower.contains("giallo")) return "#fbc02d";
|
||||
return "#9e9e9e"; // Default grey
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.entity.PrinterMachine;
|
||||
import com.printcalculator.model.PrintStats;
|
||||
import com.printcalculator.model.QuoteResult;
|
||||
import com.printcalculator.repository.PrinterMachineRepository;
|
||||
import com.printcalculator.service.QuoteCalculator;
|
||||
import com.printcalculator.service.SlicerService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -17,28 +21,33 @@ public class QuoteController {
|
||||
|
||||
private final SlicerService slicerService;
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
|
||||
// Defaults (using aliases defined in ProfileManager)
|
||||
private static final String DEFAULT_MACHINE = "bambu_a1";
|
||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||
private static final String DEFAULT_PROCESS = "standard";
|
||||
|
||||
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator) {
|
||||
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) {
|
||||
this.slicerService = slicerService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.machineRepo = machineRepo;
|
||||
}
|
||||
|
||||
@PostMapping("/api/quote")
|
||||
public ResponseEntity<QuoteResult> calculateQuote(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "machine", required = false, defaultValue = DEFAULT_MACHINE) String machine,
|
||||
@RequestParam(value = "filament", required = false, defaultValue = DEFAULT_FILAMENT) String filament,
|
||||
@RequestParam(value = "process", required = false) String process,
|
||||
@RequestParam(value = "quality", required = false) String quality
|
||||
@RequestParam(value = "quality", required = false) String quality,
|
||||
// Advanced Options
|
||||
@RequestParam(value = "infill_density", required = false) Integer infillDensity,
|
||||
@RequestParam(value = "infill_pattern", required = false) String infillPattern,
|
||||
@RequestParam(value = "layer_height", required = false) Double layerHeight,
|
||||
@RequestParam(value = "nozzle_diameter", required = false) Double nozzleDiameter,
|
||||
@RequestParam(value = "support_enabled", required = false) Boolean supportEnabled
|
||||
) throws IOException {
|
||||
|
||||
// Frontend sends 'quality', backend expects 'process'.
|
||||
// If process is missing, try quality. If both missing, use default.
|
||||
// ... process selection logic ...
|
||||
String actualProcess = process;
|
||||
if (actualProcess == null || actualProcess.isEmpty()) {
|
||||
if (quality != null && !quality.isEmpty()) {
|
||||
@@ -48,7 +57,31 @@ public class QuoteController {
|
||||
}
|
||||
}
|
||||
|
||||
return processRequest(file, machine, filament, actualProcess);
|
||||
// Prepare Overrides
|
||||
Map<String, String> processOverrides = new HashMap<>();
|
||||
Map<String, String> machineOverrides = new HashMap<>();
|
||||
|
||||
if (infillDensity != null) {
|
||||
processOverrides.put("sparse_infill_density", infillDensity + "%");
|
||||
}
|
||||
if (infillPattern != null && !infillPattern.isEmpty()) {
|
||||
processOverrides.put("sparse_infill_pattern", infillPattern);
|
||||
}
|
||||
if (layerHeight != null) {
|
||||
processOverrides.put("layer_height", String.valueOf(layerHeight));
|
||||
}
|
||||
if (supportEnabled != null) {
|
||||
processOverrides.put("enable_support", supportEnabled ? "1" : "0");
|
||||
}
|
||||
|
||||
if (nozzleDiameter != null) {
|
||||
machineOverrides.put("nozzle_diameter", String.valueOf(nozzleDiameter));
|
||||
// Also need to ensure the printer profile is compatible or just override?
|
||||
// Usually nozzle diameter changes require a different printer profile or deep overrides.
|
||||
// For now, we trust the override key works on the base profile.
|
||||
}
|
||||
|
||||
return processRequest(file, filament, actualProcess, machineOverrides, processOverrides);
|
||||
}
|
||||
|
||||
@PostMapping("/calculate/stl")
|
||||
@@ -56,30 +89,37 @@ public class QuoteController {
|
||||
@RequestParam("file") MultipartFile file
|
||||
) throws IOException {
|
||||
// Legacy endpoint uses defaults
|
||||
return processRequest(file, DEFAULT_MACHINE, DEFAULT_FILAMENT, DEFAULT_PROCESS);
|
||||
return processRequest(file, DEFAULT_FILAMENT, DEFAULT_PROCESS, null, null);
|
||||
}
|
||||
|
||||
private ResponseEntity<QuoteResult> processRequest(MultipartFile file, String machine, String filament, String process) throws IOException {
|
||||
private ResponseEntity<QuoteResult> processRequest(MultipartFile file, String filament, String process,
|
||||
Map<String, String> machineOverrides,
|
||||
Map<String, String> processOverrides) throws IOException {
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Fetch Default Active Machine
|
||||
PrinterMachine machine = machineRepo.findFirstByIsActiveTrue()
|
||||
.orElseThrow(() -> new IOException("No active printer found in database"));
|
||||
|
||||
// Save uploaded file temporarily
|
||||
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||
try {
|
||||
file.transferTo(tempInput.toFile());
|
||||
|
||||
// Slice
|
||||
PrintStats stats = slicerService.slice(tempInput.toFile(), machine, filament, process);
|
||||
String slicerMachineProfile = "bambu_a1"; // TODO: Add to PrinterMachine entity
|
||||
|
||||
// Calculate Quote
|
||||
QuoteResult result = quoteCalculator.calculate(stats);
|
||||
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
||||
|
||||
// Calculate Quote (Pass machine display name for pricing lookup)
|
||||
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.internalServerError().build(); // Simplify error handling for now
|
||||
return ResponseEntity.internalServerError().build();
|
||||
} finally {
|
||||
Files.deleteIfExists(tempInput);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.printcalculator.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record OptionsResponse(
|
||||
List<MaterialOption> materials,
|
||||
List<QualityOption> qualities,
|
||||
List<InfillPatternOption> infillPatterns,
|
||||
List<LayerHeightOptionDTO> layerHeights,
|
||||
List<NozzleOptionDTO> nozzleDiameters
|
||||
) {
|
||||
public record MaterialOption(String code, String label, List<VariantOption> variants) {}
|
||||
public record VariantOption(String name, String colorName, String hexColor, boolean isOutOfStock) {}
|
||||
public record QualityOption(String id, String label) {}
|
||||
public record InfillPatternOption(String id, String label) {}
|
||||
public record LayerHeightOptionDTO(double value, String label) {}
|
||||
public record NozzleOptionDTO(double value, String label) {}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
@Entity
|
||||
@Table(name = "filament_material_type")
|
||||
public class FilamentMaterialType {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "filament_material_type_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String materialCode;
|
||||
|
||||
@ColumnDefault("false")
|
||||
@Column(name = "is_flexible", nullable = false)
|
||||
private Boolean isFlexible;
|
||||
|
||||
@ColumnDefault("false")
|
||||
@Column(name = "is_technical", nullable = false)
|
||||
private Boolean isTechnical;
|
||||
|
||||
@Column(name = "technical_type_label", length = Integer.MAX_VALUE)
|
||||
private String technicalTypeLabel;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMaterialCode() {
|
||||
return materialCode;
|
||||
}
|
||||
|
||||
public void setMaterialCode(String materialCode) {
|
||||
this.materialCode = materialCode;
|
||||
}
|
||||
|
||||
public Boolean getIsFlexible() {
|
||||
return isFlexible;
|
||||
}
|
||||
|
||||
public void setIsFlexible(Boolean isFlexible) {
|
||||
this.isFlexible = isFlexible;
|
||||
}
|
||||
|
||||
public Boolean getIsTechnical() {
|
||||
return isTechnical;
|
||||
}
|
||||
|
||||
public void setIsTechnical(Boolean isTechnical) {
|
||||
this.isTechnical = isTechnical;
|
||||
}
|
||||
|
||||
public String getTechnicalTypeLabel() {
|
||||
return technicalTypeLabel;
|
||||
}
|
||||
|
||||
public void setTechnicalTypeLabel(String technicalTypeLabel) {
|
||||
this.technicalTypeLabel = technicalTypeLabel;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "filament_variant")
|
||||
public class FilamentVariant {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "filament_variant_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "filament_material_type_id", nullable = false)
|
||||
private FilamentMaterialType filamentMaterialType;
|
||||
|
||||
@Column(name = "variant_display_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String variantDisplayName;
|
||||
|
||||
@Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String colorName;
|
||||
|
||||
@ColumnDefault("false")
|
||||
@Column(name = "is_matte", nullable = false)
|
||||
private Boolean isMatte;
|
||||
|
||||
@ColumnDefault("false")
|
||||
@Column(name = "is_special", nullable = false)
|
||||
private Boolean isSpecial;
|
||||
|
||||
@Column(name = "cost_chf_per_kg", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal costChfPerKg;
|
||||
|
||||
@ColumnDefault("0.000")
|
||||
@Column(name = "stock_spools", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal stockSpools;
|
||||
|
||||
@ColumnDefault("1.000")
|
||||
@Column(name = "spool_net_kg", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal spoolNetKg;
|
||||
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public FilamentMaterialType getFilamentMaterialType() {
|
||||
return filamentMaterialType;
|
||||
}
|
||||
|
||||
public void setFilamentMaterialType(FilamentMaterialType filamentMaterialType) {
|
||||
this.filamentMaterialType = filamentMaterialType;
|
||||
}
|
||||
|
||||
public String getVariantDisplayName() {
|
||||
return variantDisplayName;
|
||||
}
|
||||
|
||||
public void setVariantDisplayName(String variantDisplayName) {
|
||||
this.variantDisplayName = variantDisplayName;
|
||||
}
|
||||
|
||||
public String getColorName() {
|
||||
return colorName;
|
||||
}
|
||||
|
||||
public void setColorName(String colorName) {
|
||||
this.colorName = colorName;
|
||||
}
|
||||
|
||||
public Boolean getIsMatte() {
|
||||
return isMatte;
|
||||
}
|
||||
|
||||
public void setIsMatte(Boolean isMatte) {
|
||||
this.isMatte = isMatte;
|
||||
}
|
||||
|
||||
public Boolean getIsSpecial() {
|
||||
return isSpecial;
|
||||
}
|
||||
|
||||
public void setIsSpecial(Boolean isSpecial) {
|
||||
this.isSpecial = isSpecial;
|
||||
}
|
||||
|
||||
public BigDecimal getCostChfPerKg() {
|
||||
return costChfPerKg;
|
||||
}
|
||||
|
||||
public void setCostChfPerKg(BigDecimal costChfPerKg) {
|
||||
this.costChfPerKg = costChfPerKg;
|
||||
}
|
||||
|
||||
public BigDecimal getStockSpools() {
|
||||
return stockSpools;
|
||||
}
|
||||
|
||||
public void setStockSpools(BigDecimal stockSpools) {
|
||||
this.stockSpools = stockSpools;
|
||||
}
|
||||
|
||||
public BigDecimal getSpoolNetKg() {
|
||||
return spoolNetKg;
|
||||
}
|
||||
|
||||
public void setSpoolNetKg(BigDecimal spoolNetKg) {
|
||||
this.spoolNetKg = spoolNetKg;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.Immutable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Immutable
|
||||
@Table(name = "filament_variant_stock_kg")
|
||||
public class FilamentVariantStockKg {
|
||||
@Id
|
||||
@Column(name = "filament_variant_id")
|
||||
private Long filamentVariantId;
|
||||
|
||||
@Column(name = "stock_spools", precision = 6, scale = 3)
|
||||
private BigDecimal stockSpools;
|
||||
|
||||
@Column(name = "spool_net_kg", precision = 6, scale = 3)
|
||||
private BigDecimal spoolNetKg;
|
||||
|
||||
@Column(name = "stock_kg")
|
||||
private BigDecimal stockKg;
|
||||
|
||||
public Long getFilamentVariantId() {
|
||||
return filamentVariantId;
|
||||
}
|
||||
|
||||
public BigDecimal getStockSpools() {
|
||||
return stockSpools;
|
||||
}
|
||||
|
||||
public BigDecimal getSpoolNetKg() {
|
||||
return spoolNetKg;
|
||||
}
|
||||
|
||||
public BigDecimal getStockKg() {
|
||||
return stockKg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
@Entity
|
||||
@Table(name = "infill_pattern")
|
||||
public class InfillPattern {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "infill_pattern_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "pattern_code", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String patternCode;
|
||||
|
||||
@Column(name = "display_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String displayName;
|
||||
|
||||
@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 String getPatternCode() {
|
||||
return patternCode;
|
||||
}
|
||||
|
||||
public void setPatternCode(String patternCode) {
|
||||
this.patternCode = patternCode;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "layer_height_option")
|
||||
public class LayerHeightOption {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "layer_height_option_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "layer_height_mm", nullable = false, precision = 5, scale = 3)
|
||||
private BigDecimal layerHeightMm;
|
||||
|
||||
@ColumnDefault("1.000")
|
||||
@Column(name = "time_multiplier", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal timeMultiplier;
|
||||
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigDecimal getLayerHeightMm() {
|
||||
return layerHeightMm;
|
||||
}
|
||||
|
||||
public void setLayerHeightMm(BigDecimal layerHeightMm) {
|
||||
this.layerHeightMm = layerHeightMm;
|
||||
}
|
||||
|
||||
public BigDecimal getTimeMultiplier() {
|
||||
return timeMultiplier;
|
||||
}
|
||||
|
||||
public void setTimeMultiplier(BigDecimal timeMultiplier) {
|
||||
this.timeMultiplier = timeMultiplier;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "layer_height_profile")
|
||||
public class LayerHeightProfile {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "layer_height_profile_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "profile_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String profileName;
|
||||
|
||||
@Column(name = "min_layer_height_mm", nullable = false, precision = 5, scale = 3)
|
||||
private BigDecimal minLayerHeightMm;
|
||||
|
||||
@Column(name = "max_layer_height_mm", nullable = false, precision = 5, scale = 3)
|
||||
private BigDecimal maxLayerHeightMm;
|
||||
|
||||
@Column(name = "default_layer_height_mm", nullable = false, precision = 5, scale = 3)
|
||||
private BigDecimal defaultLayerHeightMm;
|
||||
|
||||
@ColumnDefault("1.000")
|
||||
@Column(name = "time_multiplier", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal timeMultiplier;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
public void setProfileName(String profileName) {
|
||||
this.profileName = profileName;
|
||||
}
|
||||
|
||||
public BigDecimal getMinLayerHeightMm() {
|
||||
return minLayerHeightMm;
|
||||
}
|
||||
|
||||
public void setMinLayerHeightMm(BigDecimal minLayerHeightMm) {
|
||||
this.minLayerHeightMm = minLayerHeightMm;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxLayerHeightMm() {
|
||||
return maxLayerHeightMm;
|
||||
}
|
||||
|
||||
public void setMaxLayerHeightMm(BigDecimal maxLayerHeightMm) {
|
||||
this.maxLayerHeightMm = maxLayerHeightMm;
|
||||
}
|
||||
|
||||
public BigDecimal getDefaultLayerHeightMm() {
|
||||
return defaultLayerHeightMm;
|
||||
}
|
||||
|
||||
public void setDefaultLayerHeightMm(BigDecimal defaultLayerHeightMm) {
|
||||
this.defaultLayerHeightMm = defaultLayerHeightMm;
|
||||
}
|
||||
|
||||
public BigDecimal getTimeMultiplier() {
|
||||
return timeMultiplier;
|
||||
}
|
||||
|
||||
public void setTimeMultiplier(BigDecimal timeMultiplier) {
|
||||
this.timeMultiplier = timeMultiplier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "nozzle_option")
|
||||
public class NozzleOption {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "nozzle_option_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "nozzle_diameter_mm", nullable = false, precision = 4, scale = 2)
|
||||
private BigDecimal nozzleDiameterMm;
|
||||
|
||||
@ColumnDefault("0")
|
||||
@Column(name = "owned_quantity", nullable = false)
|
||||
private Integer ownedQuantity;
|
||||
|
||||
@ColumnDefault("0.00")
|
||||
@Column(name = "extra_nozzle_change_fee_chf", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal extraNozzleChangeFeeChf;
|
||||
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigDecimal getNozzleDiameterMm() {
|
||||
return nozzleDiameterMm;
|
||||
}
|
||||
|
||||
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
|
||||
this.nozzleDiameterMm = nozzleDiameterMm;
|
||||
}
|
||||
|
||||
public Integer getOwnedQuantity() {
|
||||
return ownedQuantity;
|
||||
}
|
||||
|
||||
public void setOwnedQuantity(Integer ownedQuantity) {
|
||||
this.ownedQuantity = ownedQuantity;
|
||||
}
|
||||
|
||||
public BigDecimal getExtraNozzleChangeFeeChf() {
|
||||
return extraNozzleChangeFeeChf;
|
||||
}
|
||||
|
||||
public void setExtraNozzleChangeFeeChf(BigDecimal extraNozzleChangeFeeChf) {
|
||||
this.extraNozzleChangeFeeChf = extraNozzleChangeFeeChf;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pricing_policy")
|
||||
public class PricingPolicy {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "pricing_policy_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "policy_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String policyName;
|
||||
|
||||
@Column(name = "valid_from", nullable = false)
|
||||
private OffsetDateTime validFrom;
|
||||
|
||||
@Column(name = "valid_to")
|
||||
private OffsetDateTime validTo;
|
||||
|
||||
@Column(name = "electricity_cost_chf_per_kwh", nullable = false, precision = 10, scale = 6)
|
||||
private BigDecimal electricityCostChfPerKwh;
|
||||
|
||||
@ColumnDefault("20.000")
|
||||
@Column(name = "markup_percent", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal markupPercent;
|
||||
|
||||
@ColumnDefault("0.00")
|
||||
@Column(name = "fixed_job_fee_chf", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal fixedJobFeeChf;
|
||||
|
||||
@ColumnDefault("0.00")
|
||||
@Column(name = "nozzle_change_base_fee_chf", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal nozzleChangeBaseFeeChf;
|
||||
|
||||
@ColumnDefault("0.00")
|
||||
@Column(name = "cad_cost_chf_per_hour", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal cadCostChfPerHour;
|
||||
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getPolicyName() {
|
||||
return policyName;
|
||||
}
|
||||
|
||||
public void setPolicyName(String policyName) {
|
||||
this.policyName = policyName;
|
||||
}
|
||||
|
||||
public OffsetDateTime getValidFrom() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public void setValidFrom(OffsetDateTime validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public OffsetDateTime getValidTo() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
public void setValidTo(OffsetDateTime validTo) {
|
||||
this.validTo = validTo;
|
||||
}
|
||||
|
||||
public BigDecimal getElectricityCostChfPerKwh() {
|
||||
return electricityCostChfPerKwh;
|
||||
}
|
||||
|
||||
public void setElectricityCostChfPerKwh(BigDecimal electricityCostChfPerKwh) {
|
||||
this.electricityCostChfPerKwh = electricityCostChfPerKwh;
|
||||
}
|
||||
|
||||
public BigDecimal getMarkupPercent() {
|
||||
return markupPercent;
|
||||
}
|
||||
|
||||
public void setMarkupPercent(BigDecimal markupPercent) {
|
||||
this.markupPercent = markupPercent;
|
||||
}
|
||||
|
||||
public BigDecimal getFixedJobFeeChf() {
|
||||
return fixedJobFeeChf;
|
||||
}
|
||||
|
||||
public void setFixedJobFeeChf(BigDecimal fixedJobFeeChf) {
|
||||
this.fixedJobFeeChf = fixedJobFeeChf;
|
||||
}
|
||||
|
||||
public BigDecimal getNozzleChangeBaseFeeChf() {
|
||||
return nozzleChangeBaseFeeChf;
|
||||
}
|
||||
|
||||
public void setNozzleChangeBaseFeeChf(BigDecimal nozzleChangeBaseFeeChf) {
|
||||
this.nozzleChangeBaseFeeChf = nozzleChangeBaseFeeChf;
|
||||
}
|
||||
|
||||
public BigDecimal getCadCostChfPerHour() {
|
||||
return cadCostChfPerHour;
|
||||
}
|
||||
|
||||
public void setCadCostChfPerHour(BigDecimal cadCostChfPerHour) {
|
||||
this.cadCostChfPerHour = cadCostChfPerHour;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pricing_policy_machine_hour_tier")
|
||||
public class PricingPolicyMachineHourTier {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "pricing_policy_machine_hour_tier_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "pricing_policy_id", nullable = false)
|
||||
private PricingPolicy pricingPolicy;
|
||||
|
||||
@Column(name = "tier_start_hours", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal tierStartHours;
|
||||
|
||||
@Column(name = "tier_end_hours", precision = 10, scale = 2)
|
||||
private BigDecimal tierEndHours;
|
||||
|
||||
@Column(name = "machine_cost_chf_per_hour", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal machineCostChfPerHour;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PricingPolicy getPricingPolicy() {
|
||||
return pricingPolicy;
|
||||
}
|
||||
|
||||
public void setPricingPolicy(PricingPolicy pricingPolicy) {
|
||||
this.pricingPolicy = pricingPolicy;
|
||||
}
|
||||
|
||||
public BigDecimal getTierStartHours() {
|
||||
return tierStartHours;
|
||||
}
|
||||
|
||||
public void setTierStartHours(BigDecimal tierStartHours) {
|
||||
this.tierStartHours = tierStartHours;
|
||||
}
|
||||
|
||||
public BigDecimal getTierEndHours() {
|
||||
return tierEndHours;
|
||||
}
|
||||
|
||||
public void setTierEndHours(BigDecimal tierEndHours) {
|
||||
this.tierEndHours = tierEndHours;
|
||||
}
|
||||
|
||||
public BigDecimal getMachineCostChfPerHour() {
|
||||
return machineCostChfPerHour;
|
||||
}
|
||||
|
||||
public void setMachineCostChfPerHour(BigDecimal machineCostChfPerHour) {
|
||||
this.machineCostChfPerHour = machineCostChfPerHour;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.Immutable;
|
||||
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
@Immutable
|
||||
@Table(name = "printer_fleet_current")
|
||||
public class PrinterFleetCurrent {
|
||||
@Id
|
||||
@Column(name = "fleet_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "weighted_average_power_watts")
|
||||
private Integer weightedAveragePowerWatts;
|
||||
|
||||
@Column(name = "fleet_max_build_x_mm")
|
||||
private Integer fleetMaxBuildXMm;
|
||||
|
||||
@Column(name = "fleet_max_build_y_mm")
|
||||
private Integer fleetMaxBuildYMm;
|
||||
|
||||
@Column(name = "fleet_max_build_z_mm")
|
||||
private Integer fleetMaxBuildZMm;
|
||||
|
||||
public Integer getWeightedAveragePowerWatts() {
|
||||
return weightedAveragePowerWatts;
|
||||
}
|
||||
|
||||
public Integer getFleetMaxBuildXMm() {
|
||||
return fleetMaxBuildXMm;
|
||||
}
|
||||
|
||||
public Integer getFleetMaxBuildYMm() {
|
||||
return fleetMaxBuildYMm;
|
||||
}
|
||||
|
||||
public Integer getFleetMaxBuildZMm() {
|
||||
return fleetMaxBuildZMm;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "printer_machine")
|
||||
public class PrinterMachine {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "printer_machine_id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "printer_display_name", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String printerDisplayName;
|
||||
|
||||
@Column(name = "build_volume_x_mm", nullable = false)
|
||||
private Integer buildVolumeXMm;
|
||||
|
||||
@Column(name = "build_volume_y_mm", nullable = false)
|
||||
private Integer buildVolumeYMm;
|
||||
|
||||
@Column(name = "build_volume_z_mm", nullable = false)
|
||||
private Integer buildVolumeZMm;
|
||||
|
||||
@Column(name = "power_watts", nullable = false)
|
||||
private Integer powerWatts;
|
||||
|
||||
@ColumnDefault("1.000")
|
||||
@Column(name = "fleet_weight", nullable = false, precision = 6, scale = 3)
|
||||
private BigDecimal fleetWeight;
|
||||
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getPrinterDisplayName() {
|
||||
return printerDisplayName;
|
||||
}
|
||||
|
||||
public void setPrinterDisplayName(String printerDisplayName) {
|
||||
this.printerDisplayName = printerDisplayName;
|
||||
}
|
||||
|
||||
public Integer getBuildVolumeXMm() {
|
||||
return buildVolumeXMm;
|
||||
}
|
||||
|
||||
public void setBuildVolumeXMm(Integer buildVolumeXMm) {
|
||||
this.buildVolumeXMm = buildVolumeXMm;
|
||||
}
|
||||
|
||||
public Integer getBuildVolumeYMm() {
|
||||
return buildVolumeYMm;
|
||||
}
|
||||
|
||||
public void setBuildVolumeYMm(Integer buildVolumeYMm) {
|
||||
this.buildVolumeYMm = buildVolumeYMm;
|
||||
}
|
||||
|
||||
public Integer getBuildVolumeZMm() {
|
||||
return buildVolumeZMm;
|
||||
}
|
||||
|
||||
public void setBuildVolumeZMm(Integer buildVolumeZMm) {
|
||||
this.buildVolumeZMm = buildVolumeZMm;
|
||||
}
|
||||
|
||||
public Integer getPowerWatts() {
|
||||
return powerWatts;
|
||||
}
|
||||
|
||||
public void setPowerWatts(Integer powerWatts) {
|
||||
this.powerWatts = powerWatts;
|
||||
}
|
||||
|
||||
public BigDecimal getFleetWeight() {
|
||||
return fleetWeight;
|
||||
}
|
||||
|
||||
public void setFleetWeight(BigDecimal fleetWeight) {
|
||||
this.fleetWeight = fleetWeight;
|
||||
}
|
||||
|
||||
public Boolean getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,5 +7,5 @@ public record CostBreakdown(
|
||||
BigDecimal machineCost,
|
||||
BigDecimal energyCost,
|
||||
BigDecimal subtotal,
|
||||
BigDecimal markupAmount
|
||||
BigDecimal markup
|
||||
) {}
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
package com.printcalculator.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import java.util.List;
|
||||
|
||||
public record QuoteResult(
|
||||
BigDecimal totalPrice,
|
||||
String currency,
|
||||
PrintStats stats,
|
||||
CostBreakdown breakdown,
|
||||
List<String> notes
|
||||
) {}
|
||||
public class QuoteResult {
|
||||
private double totalPrice;
|
||||
private String currency;
|
||||
private PrintStats stats;
|
||||
|
||||
@JsonIgnore
|
||||
private CostBreakdown breakdown;
|
||||
|
||||
@JsonIgnore
|
||||
private List<String> notes;
|
||||
|
||||
private double setupCost;
|
||||
|
||||
public QuoteResult(double totalPrice, String currency, PrintStats stats, CostBreakdown breakdown, List<String> notes, double setupCost) {
|
||||
this.totalPrice = totalPrice;
|
||||
this.currency = currency;
|
||||
this.stats = stats;
|
||||
this.breakdown = breakdown;
|
||||
this.notes = notes;
|
||||
this.setupCost = setupCost;
|
||||
}
|
||||
|
||||
public double getTotalPrice() {
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public PrintStats getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public CostBreakdown getBreakdown() {
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
public List<String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public double getSetupCost() {
|
||||
return setupCost;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.FilamentMaterialType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FilamentMaterialTypeRepository extends JpaRepository<FilamentMaterialType, Long> {
|
||||
Optional<FilamentMaterialType> findByMaterialCode(String materialCode);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.FilamentVariant;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.printcalculator.entity.FilamentMaterialType;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FilamentVariantRepository extends JpaRepository<FilamentVariant, Long> {
|
||||
// We try to match by color name if possible, or get first active
|
||||
Optional<FilamentVariant> findByFilamentMaterialTypeAndColorName(FilamentMaterialType type, String colorName);
|
||||
Optional<FilamentVariant> findFirstByFilamentMaterialTypeAndIsActiveTrue(FilamentMaterialType type);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.InfillPattern;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InfillPatternRepository extends JpaRepository<InfillPattern, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.LayerHeightOption;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LayerHeightOptionRepository extends JpaRepository<LayerHeightOption, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.LayerHeightProfile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LayerHeightProfileRepository extends JpaRepository<LayerHeightProfile, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.NozzleOption;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface NozzleOptionRepository extends JpaRepository<NozzleOption, Long> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.PricingPolicyMachineHourTier;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import java.util.List;
|
||||
|
||||
public interface PricingPolicyMachineHourTierRepository extends JpaRepository<PricingPolicyMachineHourTier, Long> {
|
||||
List<PricingPolicyMachineHourTier> findAllByPricingPolicyOrderByTierStartHoursAsc(PricingPolicy pricingPolicy);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PricingPolicyRepository extends JpaRepository<PricingPolicy, Long> {
|
||||
PricingPolicy findFirstByIsActiveTrueOrderByValidFromDesc();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.printcalculator.repository;
|
||||
|
||||
import com.printcalculator.entity.PrinterMachine;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PrinterMachineRepository extends JpaRepository<PrinterMachine, Long> {
|
||||
Optional<PrinterMachine> findByPrinterDisplayName(String printerDisplayName);
|
||||
Optional<PrinterMachine> findFirstByIsActiveTrue();
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class GCodeParser {
|
||||
// ; estimated printing time = 1h 2m 3s
|
||||
// ; filament used [g] = 12.34
|
||||
// ; filament used [mm] = 1234.56
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile(";\\s*estimated printing time\\s*=\\s*(.*)");
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile(";\\s*estimated printing time.*=\\s*(.*)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*(.*)");
|
||||
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)");
|
||||
|
||||
@@ -29,25 +29,32 @@ public class GCodeParser {
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) {
|
||||
String line;
|
||||
// Scan first 5000 lines for efficiency (metadata might be further down)
|
||||
int count = 0;
|
||||
while ((line = reader.readLine()) != null && count < 5000) {
|
||||
|
||||
// Scan entire file as metadata is often at the end
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
// OrcaSlicer comments start with ;
|
||||
if (!line.startsWith(";")) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.toLowerCase().contains("estimated printing time")) {
|
||||
System.out.println("DEBUG: Found potential time line: '" + line + "'");
|
||||
}
|
||||
|
||||
Matcher timeMatcher = TIME_PATTERN.matcher(line);
|
||||
if (timeMatcher.find()) {
|
||||
timeFormatted = timeMatcher.group(1).trim();
|
||||
seconds = parseTimeString(timeFormatted);
|
||||
System.out.println("GCodeParser: Found time: " + timeFormatted + " (" + seconds + "s)");
|
||||
}
|
||||
|
||||
Matcher weightMatcher = FILAMENT_G_PATTERN.matcher(line);
|
||||
if (weightMatcher.find()) {
|
||||
try {
|
||||
weightG = Double.parseDouble(weightMatcher.group(1).trim());
|
||||
System.out.println("GCodeParser: Found weight: " + weightG + "g");
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
@@ -55,9 +62,9 @@ public class GCodeParser {
|
||||
if (lengthMatcher.find()) {
|
||||
try {
|
||||
lengthMm = Double.parseDouble(lengthMatcher.group(1).trim());
|
||||
System.out.println("GCodeParser: Found length: " + lengthMm + "mm");
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,104 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import com.printcalculator.config.AppProperties;
|
||||
|
||||
import com.printcalculator.entity.FilamentMaterialType;
|
||||
import com.printcalculator.entity.FilamentVariant;
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import com.printcalculator.entity.PricingPolicyMachineHourTier;
|
||||
import com.printcalculator.entity.PrinterMachine;
|
||||
import com.printcalculator.model.CostBreakdown;
|
||||
import com.printcalculator.model.PrintStats;
|
||||
import com.printcalculator.model.QuoteResult;
|
||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||
import com.printcalculator.repository.FilamentVariantRepository;
|
||||
import com.printcalculator.repository.PricingPolicyMachineHourTierRepository;
|
||||
import com.printcalculator.repository.PricingPolicyRepository;
|
||||
import com.printcalculator.repository.PrinterMachineRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class QuoteCalculator {
|
||||
|
||||
private final AppProperties props;
|
||||
private final PricingPolicyRepository pricingRepo;
|
||||
private final PricingPolicyMachineHourTierRepository tierRepo;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
private final FilamentMaterialTypeRepository materialRepo;
|
||||
private final FilamentVariantRepository variantRepo;
|
||||
|
||||
public QuoteCalculator(AppProperties props) {
|
||||
this.props = props;
|
||||
public QuoteCalculator(PricingPolicyRepository pricingRepo,
|
||||
PricingPolicyMachineHourTierRepository tierRepo,
|
||||
PrinterMachineRepository machineRepo,
|
||||
FilamentMaterialTypeRepository materialRepo,
|
||||
FilamentVariantRepository variantRepo) {
|
||||
this.pricingRepo = pricingRepo;
|
||||
this.tierRepo = tierRepo;
|
||||
this.machineRepo = machineRepo;
|
||||
this.materialRepo = materialRepo;
|
||||
this.variantRepo = variantRepo;
|
||||
}
|
||||
|
||||
public QuoteResult calculate(PrintStats stats) {
|
||||
public QuoteResult calculate(PrintStats stats, String machineName, String filamentProfileName) {
|
||||
// 1. Fetch Active Policy
|
||||
PricingPolicy policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
|
||||
if (policy == null) {
|
||||
throw new RuntimeException("No active pricing policy found");
|
||||
}
|
||||
|
||||
// 2. Fetch Machine Info
|
||||
// Map "bambu_a1" -> "BambuLab A1" or similar?
|
||||
// Ideally we should use the display name from DB.
|
||||
// For now, if machineName is a code, we might need a mapping or just fuzzy search.
|
||||
// Let's assume machineName is mapped or we search by display name.
|
||||
// If not found, fallback to first active.
|
||||
PrinterMachine machine = machineRepo.findByPrinterDisplayName(machineName).orElse(null);
|
||||
if (machine == null) {
|
||||
// Try "BambuLab A1" if code was "bambu_a1" logic or just get first active
|
||||
machine = machineRepo.findFirstByIsActiveTrue()
|
||||
.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);
|
||||
FilamentMaterialType materialType = materialRepo.findByMaterialCode(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)
|
||||
.orElseThrow(() -> new RuntimeException("No active variant for material: " + materialCode));
|
||||
|
||||
|
||||
// --- CALCULATIONS ---
|
||||
|
||||
// Material Cost: (weight / 1000) * costPerKg
|
||||
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal materialCost = weightKg.multiply(BigDecimal.valueOf(props.getFilamentCostPerKg()));
|
||||
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
||||
|
||||
// Machine Cost: (seconds / 3600) * costPerHour
|
||||
BigDecimal hours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal machineCost = hours.multiply(BigDecimal.valueOf(props.getMachineCostPerHour()));
|
||||
// Machine Cost: Tiered
|
||||
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal machineCost = calculateMachineCost(policy, totalHours);
|
||||
|
||||
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
||||
BigDecimal kw = BigDecimal.valueOf(props.getPrinterPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal kwh = kw.multiply(hours);
|
||||
BigDecimal energyCost = kwh.multiply(BigDecimal.valueOf(props.getEnergyCostPerKwh()));
|
||||
BigDecimal kw = BigDecimal.valueOf(machine.getPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal kwh = kw.multiply(totalHours);
|
||||
BigDecimal energyCost = kwh.multiply(policy.getElectricityCostChfPerKwh());
|
||||
|
||||
// Subtotal
|
||||
BigDecimal subtotal = materialCost.add(machineCost).add(energyCost);
|
||||
// Subtotal (Costs + Fixed Fees)
|
||||
BigDecimal fixedFee = policy.getFixedJobFeeChf();
|
||||
BigDecimal subtotal = materialCost.add(machineCost).add(energyCost).add(fixedFee);
|
||||
|
||||
// Markup
|
||||
BigDecimal markupFactor = BigDecimal.valueOf(1.0 + (props.getMarkupPercent() / 100.0));
|
||||
// Markup is percentage (e.g. 20.0)
|
||||
BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
||||
BigDecimal totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
BigDecimal markupAmount = totalPrice.subtract(subtotal);
|
||||
@@ -52,8 +112,66 @@ public class QuoteCalculator {
|
||||
);
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
// notes.add("Generated via Dynamic Slicer (Java Backend)");
|
||||
notes.add("Policy: " + policy.getPolicyName());
|
||||
notes.add("Machine: " + machine.getPrinterDisplayName());
|
||||
notes.add("Material: " + variant.getVariantDisplayName());
|
||||
|
||||
return new QuoteResult(totalPrice, "CHF", stats, breakdown, notes);
|
||||
return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, breakdown, notes, fixedFee.doubleValue());
|
||||
}
|
||||
|
||||
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||
List<PricingPolicyMachineHourTier> tiers = tierRepo.findAllByPricingPolicyOrderByTierStartHoursAsc(policy);
|
||||
if (tiers.isEmpty()) {
|
||||
return BigDecimal.ZERO; // Should not happen if DB is correct
|
||||
}
|
||||
|
||||
BigDecimal remainingHours = hours;
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
BigDecimal processedHours = BigDecimal.ZERO;
|
||||
|
||||
for (PricingPolicyMachineHourTier tier : tiers) {
|
||||
if (remainingHours.compareTo(BigDecimal.ZERO) <= 0) break;
|
||||
|
||||
BigDecimal tierStart = tier.getTierStartHours();
|
||||
BigDecimal tierEnd = tier.getTierEndHours(); // can be null for infinity
|
||||
|
||||
// Determine duration in this tier
|
||||
// Valid duration in this tier = (min(tierEnd, totalHours) - tierStart)
|
||||
// But logic is simpler: we consume hours sequentially?
|
||||
// "0-10h @ 2CHF, 10-20h @ 1.5CHF" implies:
|
||||
// 5h job -> 5 * 2
|
||||
// 15h job -> 10 * 2 + 5 * 1.5
|
||||
|
||||
BigDecimal tierDuration;
|
||||
|
||||
// Max hours applicable in this tier relative to 0
|
||||
BigDecimal tierLimit = (tierEnd != null) ? tierEnd : BigDecimal.valueOf(Long.MAX_VALUE);
|
||||
|
||||
// The amount of hours falling into this bucket
|
||||
// Upper bound for this calculation is min(totalHours, tierLimit)
|
||||
// Lower bound is tierStart
|
||||
// So hours in this bucket = max(0, min(totalHours, tierLimit) - tierStart)
|
||||
|
||||
BigDecimal upper = hours.min(tierLimit);
|
||||
BigDecimal lower = tierStart;
|
||||
|
||||
if (upper.compareTo(lower) > 0) {
|
||||
BigDecimal hoursInTier = upper.subtract(lower);
|
||||
totalCost = totalCost.add(hoursInTier.multiply(tier.getMachineCostChfPerHour()));
|
||||
}
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
private String detectMaterialCode(String profileName) {
|
||||
String lower = profileName.toLowerCase();
|
||||
if (lower.contains("petg")) return "PETG";
|
||||
if (lower.contains("tpu")) return "TPU";
|
||||
if (lower.contains("abs")) return "ABS";
|
||||
if (lower.contains("nylon")) return "Nylon";
|
||||
if (lower.contains("asa")) return "ASA";
|
||||
// Default to PLA
|
||||
return "PLA";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -36,12 +37,21 @@ public class SlicerService {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public PrintStats slice(File inputStl, String machineName, String filamentName, String processName) throws IOException {
|
||||
public PrintStats slice(File inputStl, String machineName, String filamentName, String processName,
|
||||
Map<String, String> machineOverrides, Map<String, String> processOverrides) throws IOException {
|
||||
// 1. Prepare Profiles
|
||||
ObjectNode machineProfile = profileManager.getMergedProfile(machineName, "machine");
|
||||
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
|
||||
ObjectNode processProfile = profileManager.getMergedProfile(processName, "process");
|
||||
|
||||
// Apply Overrides
|
||||
if (machineOverrides != null) {
|
||||
machineOverrides.forEach(machineProfile::put);
|
||||
}
|
||||
if (processOverrides != null) {
|
||||
processOverrides.forEach(processProfile::put);
|
||||
}
|
||||
|
||||
// 2. Create Temp Dir
|
||||
Path tempDir = Files.createTempDirectory("slicer_job_");
|
||||
try {
|
||||
|
||||
@@ -14,13 +14,6 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
slicer.path=${SLICER_PATH:/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer}
|
||||
profiles.root=${PROFILES_DIR:profiles}
|
||||
|
||||
# Pricing Configuration
|
||||
# Mapped to legacy environment variables for Docker compatibility
|
||||
pricing.filament-cost-per-kg=${FILAMENT_COST_PER_KG:25.0}
|
||||
pricing.machine-cost-per-hour=${MACHINE_COST_PER_HOUR:2.0}
|
||||
pricing.energy-cost-per-kwh=${ENERGY_COST_PER_KWH:0.30}
|
||||
pricing.printer-power-watts=${PRINTER_POWER_WATTS:150.0}
|
||||
pricing.markup-percent=${MARKUP_PERCENT:20.0}
|
||||
|
||||
# File Upload Limits
|
||||
spring.servlet.multipart.max-file-size=200MB
|
||||
|
||||
@@ -52,6 +52,26 @@ class GCodeParserTest {
|
||||
assertEquals(750, stats.printTimeSeconds()); // 12*60 + 30
|
||||
assertEquals(5.0, stats.filamentWeightGrams(), 0.001);
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
@Test
|
||||
void parse_withExtraTextInTimeLine_returnsCorrectStats() throws IOException {
|
||||
// Arrange
|
||||
File tempFile = File.createTempFile("test_extra", ".gcode");
|
||||
try (FileWriter writer = new FileWriter(tempFile)) {
|
||||
writer.write("; generated by OrcaSlicer\n");
|
||||
// Simulate the variation that was causing issues
|
||||
writer.write("; estimated printing time (normal mode) = 1h 2m 3s\n");
|
||||
writer.write("; filament used [g] = 10.5\n");
|
||||
writer.write("; filament used [mm] = 3000.0\n");
|
||||
}
|
||||
|
||||
GCodeParser parser = new GCodeParser();
|
||||
PrintStats stats = parser.parse(tempFile);
|
||||
|
||||
assertEquals(3723L, stats.printTimeSeconds());
|
||||
assertEquals("1h 2m 3s", stats.printTimeFormatted());
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
366
db.sql
Normal file
366
db.sql
Normal file
@@ -0,0 +1,366 @@
|
||||
create table printer_machine
|
||||
(
|
||||
printer_machine_id bigserial primary key,
|
||||
printer_display_name text not null unique,
|
||||
|
||||
build_volume_x_mm integer not null check (build_volume_x_mm > 0),
|
||||
build_volume_y_mm integer not null check (build_volume_y_mm > 0),
|
||||
build_volume_z_mm integer not null check (build_volume_z_mm > 0),
|
||||
|
||||
power_watts integer not null check (power_watts > 0),
|
||||
|
||||
fleet_weight numeric(6, 3) not null default 1.000,
|
||||
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create view printer_fleet_current as
|
||||
select 1 as fleet_id,
|
||||
case
|
||||
when sum(fleet_weight) = 0 then null
|
||||
else round(sum(power_watts * fleet_weight) / sum(fleet_weight))::integer
|
||||
end as weighted_average_power_watts,
|
||||
max(build_volume_x_mm) as fleet_max_build_x_mm,
|
||||
max(build_volume_y_mm) as fleet_max_build_y_mm,
|
||||
max(build_volume_z_mm) as fleet_max_build_z_mm
|
||||
from printer_machine
|
||||
where is_active = true;
|
||||
|
||||
|
||||
|
||||
create table filament_material_type
|
||||
(
|
||||
filament_material_type_id bigserial primary key,
|
||||
material_code text not null unique, -- PLA, PETG, TPU, ASA...
|
||||
is_flexible boolean not null default false, -- sì/no
|
||||
is_technical boolean not null default false, -- sì/no
|
||||
technical_type_label text -- es: "alta temperatura", "rinforzato", ecc.
|
||||
);
|
||||
|
||||
create table filament_variant
|
||||
(
|
||||
filament_variant_id bigserial primary key,
|
||||
filament_material_type_id bigint not null references filament_material_type (filament_material_type_id),
|
||||
|
||||
variant_display_name text not null, -- es: "PLA Nero Opaco BrandX"
|
||||
color_name text not null, -- Nero, Bianco, ecc.
|
||||
is_matte boolean not null default false,
|
||||
is_special boolean not null default false,
|
||||
|
||||
cost_chf_per_kg numeric(10, 2) not null,
|
||||
|
||||
-- Stock espresso in rotoli anche frazionati
|
||||
stock_spools numeric(6, 3) not null default 0.000,
|
||||
spool_net_kg numeric(6, 3) not null default 1.000,
|
||||
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
|
||||
unique (filament_material_type_id, variant_display_name)
|
||||
);
|
||||
|
||||
-- (opzionale) kg disponibili calcolati
|
||||
create view filament_variant_stock_kg as
|
||||
select filament_variant_id,
|
||||
stock_spools,
|
||||
spool_net_kg,
|
||||
(stock_spools * spool_net_kg) as stock_kg
|
||||
from filament_variant;
|
||||
|
||||
|
||||
|
||||
create table pricing_policy
|
||||
(
|
||||
pricing_policy_id bigserial primary key,
|
||||
|
||||
policy_name text not null, -- es: "2026 Q1", "Default", ecc.
|
||||
|
||||
-- validità temporale (consiglio: valid_to esclusiva)
|
||||
valid_from timestamptz not null,
|
||||
valid_to timestamptz,
|
||||
|
||||
electricity_cost_chf_per_kwh numeric(10, 6) not null,
|
||||
markup_percent numeric(6, 3) not null default 20.000,
|
||||
|
||||
fixed_job_fee_chf numeric(10, 2) not null default 0.00, -- "costo fisso"
|
||||
nozzle_change_base_fee_chf numeric(10, 2) not null default 0.00, -- base cambio ugello, se vuoi
|
||||
cad_cost_chf_per_hour numeric(10, 2) not null default 0.00,
|
||||
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table pricing_policy_machine_hour_tier
|
||||
(
|
||||
pricing_policy_machine_hour_tier_id bigserial primary key,
|
||||
pricing_policy_id bigint not null references pricing_policy (pricing_policy_id),
|
||||
|
||||
tier_start_hours numeric(10, 2) not null,
|
||||
tier_end_hours numeric(10, 2), -- null = infinito
|
||||
machine_cost_chf_per_hour numeric(10, 2) not null,
|
||||
|
||||
constraint chk_tier_start_non_negative check (tier_start_hours >= 0),
|
||||
constraint chk_tier_end_gt_start check (tier_end_hours is null or tier_end_hours > tier_start_hours)
|
||||
);
|
||||
|
||||
create index idx_pricing_policy_validity
|
||||
on pricing_policy (valid_from, valid_to);
|
||||
|
||||
create index idx_pricing_tier_lookup
|
||||
on pricing_policy_machine_hour_tier (pricing_policy_id, tier_start_hours);
|
||||
|
||||
|
||||
create table nozzle_option
|
||||
(
|
||||
nozzle_option_id bigserial primary key,
|
||||
nozzle_diameter_mm numeric(4, 2) not null unique, -- 0.4, 0.6, 0.8...
|
||||
|
||||
owned_quantity integer not null default 0 check (owned_quantity >= 0),
|
||||
|
||||
-- extra costo specifico oltre ad eventuale base fee della pricing_policy
|
||||
extra_nozzle_change_fee_chf numeric(10, 2) not null default 0.00,
|
||||
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
|
||||
create table layer_height_option
|
||||
(
|
||||
layer_height_option_id bigserial primary key,
|
||||
layer_height_mm numeric(5, 3) not null unique, -- 0.12, 0.20, 0.28...
|
||||
|
||||
-- opzionale: moltiplicatore costo/tempo (es: 0.12 costa di più)
|
||||
time_multiplier numeric(6, 3) not null default 1.000,
|
||||
|
||||
is_active boolean not null default true
|
||||
);
|
||||
|
||||
create table layer_height_profile
|
||||
(
|
||||
layer_height_profile_id bigserial primary key,
|
||||
profile_name text not null unique, -- "Standard", "Fine", ecc.
|
||||
|
||||
min_layer_height_mm numeric(5, 3) not null,
|
||||
max_layer_height_mm numeric(5, 3) not null,
|
||||
default_layer_height_mm numeric(5, 3) not null,
|
||||
|
||||
time_multiplier numeric(6, 3) not null default 1.000,
|
||||
|
||||
constraint chk_layer_range check (max_layer_height_mm >= min_layer_height_mm)
|
||||
);
|
||||
|
||||
|
||||
begin;
|
||||
|
||||
set timezone = 'Europe/Zurich';
|
||||
|
||||
is_active = excluded.is_active;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 1) Pricing policy (valori ESATTI da Excel)
|
||||
-- Valid from: 2026-01-01, valid_to: NULL
|
||||
-- =========================================================
|
||||
insert into pricing_policy (
|
||||
policy_name,
|
||||
valid_from,
|
||||
valid_to,
|
||||
electricity_cost_chf_per_kwh,
|
||||
markup_percent,
|
||||
fixed_job_fee_chf,
|
||||
nozzle_change_base_fee_chf,
|
||||
cad_cost_chf_per_hour,
|
||||
is_active
|
||||
) values (
|
||||
'Excel Tariffe 2026-01-01',
|
||||
'2026-01-01 00:00:00+01'::timestamptz,
|
||||
null,
|
||||
0.156, -- Costo elettricità CHF/kWh (Excel)
|
||||
0.000, -- Markup non specificato -> 0 (puoi cambiarlo dopo)
|
||||
1.00, -- Costo fisso macchina CHF (Excel)
|
||||
0.00, -- Base cambio ugello: non specificato -> 0
|
||||
25.00, -- Tariffa CAD CHF/h (Excel)
|
||||
true
|
||||
)
|
||||
on conflict do nothing;
|
||||
|
||||
-- scaglioni tariffa stampa (Excel)
|
||||
insert into pricing_policy_machine_hour_tier (
|
||||
pricing_policy_id,
|
||||
tier_start_hours,
|
||||
tier_end_hours,
|
||||
machine_cost_chf_per_hour
|
||||
)
|
||||
select
|
||||
p.pricing_policy_id,
|
||||
tiers.tier_start_hours,
|
||||
tiers.tier_end_hours,
|
||||
tiers.machine_cost_chf_per_hour
|
||||
from pricing_policy p
|
||||
cross join (
|
||||
values
|
||||
(0.00::numeric, 10.00::numeric, 2.00::numeric), -- 0–10 h
|
||||
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 10–20 h
|
||||
(20.00::numeric, null::numeric, 0.50::numeric) -- >20 h
|
||||
) as tiers(tier_start_hours, tier_end_hours, machine_cost_chf_per_hour)
|
||||
where p.policy_name = 'Excel Tariffe 2026-01-01'
|
||||
on conflict do nothing;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 2) Stampante: BambuLab A1
|
||||
-- =========================================================
|
||||
insert into printer_machine (
|
||||
printer_display_name,
|
||||
build_volume_x_mm,
|
||||
build_volume_y_mm,
|
||||
build_volume_z_mm,
|
||||
power_watts,
|
||||
fleet_weight,
|
||||
is_active
|
||||
) values (
|
||||
'BambuLab A1',
|
||||
256,
|
||||
256,
|
||||
256,
|
||||
150, -- hai detto "150, 140": qui ho messo 150
|
||||
1.000,
|
||||
true
|
||||
)
|
||||
on conflict (printer_display_name) do update
|
||||
set
|
||||
build_volume_x_mm = excluded.build_volume_x_mm,
|
||||
build_volume_y_mm = excluded.build_volume_y_mm,
|
||||
build_volume_z_mm = excluded.build_volume_z_mm,
|
||||
power_watts = excluded.power_watts,
|
||||
fleet_weight = excluded.fleet_weight,
|
||||
is_active = excluded.is_active;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 3) Material types (da Excel) - per ora niente technical
|
||||
-- =========================================================
|
||||
insert into filament_material_type (
|
||||
material_code,
|
||||
is_flexible,
|
||||
is_technical,
|
||||
technical_type_label
|
||||
) values
|
||||
('PLA', false, false, null),
|
||||
('PETG', false, false, null),
|
||||
('TPU', true, false, null),
|
||||
('ABS', false, false, null),
|
||||
('Nylon', false, false, null),
|
||||
('Carbon PLA', false, false, null)
|
||||
on conflict (material_code) do update
|
||||
set
|
||||
is_flexible = excluded.is_flexible,
|
||||
is_technical = excluded.is_technical,
|
||||
technical_type_label = excluded.technical_type_label;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 4) Filament variants (PLA colori) - costi da Excel
|
||||
-- Excel: PLA = 18 CHF/kg, TPU = 42 CHF/kg (non inserito perché quantità non chiara)
|
||||
-- Stock in "rotoli" (3 = 3 kg se spool_net_kg=1)
|
||||
-- =========================================================
|
||||
|
||||
-- helper: ID PLA
|
||||
with pla as (
|
||||
select filament_material_type_id
|
||||
from filament_material_type
|
||||
where material_code = 'PLA'
|
||||
)
|
||||
insert into filament_variant (
|
||||
filament_material_type_id,
|
||||
variant_display_name,
|
||||
color_name,
|
||||
is_matte,
|
||||
is_special,
|
||||
cost_chf_per_kg,
|
||||
stock_spools,
|
||||
spool_net_kg,
|
||||
is_active
|
||||
)
|
||||
select
|
||||
pla.filament_material_type_id,
|
||||
v.variant_display_name,
|
||||
v.color_name,
|
||||
v.is_matte,
|
||||
v.is_special,
|
||||
18.00, -- PLA da Excel
|
||||
v.stock_spools,
|
||||
1.000,
|
||||
true
|
||||
from pla
|
||||
cross join (
|
||||
values
|
||||
('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
|
||||
('PLA Nero', 'Nero', false, false, 3.000::numeric),
|
||||
('PLA Blu', 'Blu', false, false, 1.000::numeric),
|
||||
('PLA Arancione', 'Arancione', false, false, 1.000::numeric),
|
||||
('PLA Grigio', 'Grigio', false, false, 1.000::numeric),
|
||||
('PLA Grigio Scuro', 'Grigio scuro', false, false, 1.000::numeric),
|
||||
('PLA Grigio Chiaro', 'Grigio chiaro', false, false, 1.000::numeric),
|
||||
('PLA Viola', 'Viola', false, false, 1.000::numeric)
|
||||
) as v(variant_display_name, color_name, is_matte, is_special, stock_spools)
|
||||
on conflict (filament_material_type_id, variant_display_name) do update
|
||||
set
|
||||
color_name = excluded.color_name,
|
||||
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;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 5) Ugelli
|
||||
-- 0.4 standard (0 extra), 0.6 con attivazione 50 CHF
|
||||
-- =========================================================
|
||||
insert into nozzle_option (
|
||||
nozzle_diameter_mm,
|
||||
owned_quantity,
|
||||
extra_nozzle_change_fee_chf,
|
||||
is_active
|
||||
) values
|
||||
(0.40, 1, 0.00, true),
|
||||
(0.60, 1, 50.00, true)
|
||||
on conflict (nozzle_diameter_mm) do update
|
||||
set
|
||||
owned_quantity = excluded.owned_quantity,
|
||||
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
|
||||
is_active = excluded.is_active;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 6) Layer heights (opzioni)
|
||||
-- =========================================================
|
||||
insert into layer_height_option (
|
||||
layer_height_mm,
|
||||
time_multiplier,
|
||||
is_active
|
||||
) values
|
||||
(0.080, 1.000, true),
|
||||
(0.120, 1.000, true),
|
||||
(0.160, 1.000, true),
|
||||
(0.200, 1.000, true),
|
||||
(0.240, 1.000, true),
|
||||
(0.280, 1.000, true)
|
||||
on conflict (layer_height_mm) do update
|
||||
set
|
||||
time_multiplier = excluded.time_multiplier,
|
||||
is_active = excluded.is_active;
|
||||
|
||||
commit;
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sostituisci __MULTIPLIER__ con il tuo valore (es. 1.10)
|
||||
update layer_height_option
|
||||
set time_multiplier = 0.1
|
||||
where layer_height_mm = 0.080;
|
||||
@@ -9,6 +9,10 @@ services:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- DB_URL=jdbc:postgresql://db:5432/printcalc
|
||||
- DB_USERNAME=printcalc
|
||||
- DB_PASSWORD=printcalc_secret
|
||||
- SPRING_PROFILES_ACTIVE=local
|
||||
- FILAMENT_COST_PER_KG=22.0
|
||||
- MACHINE_COST_PER_HOUR=2.50
|
||||
- ENERGY_COST_PER_KWH=0.30
|
||||
@@ -16,10 +20,14 @@ services:
|
||||
- MARKUP_PERCENT=20
|
||||
- TEMP_DIR=/app/temp
|
||||
- PROFILES_DIR=/app/profiles
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: print-calculator-frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
15
frontend/Dockerfile.dev
Normal file
15
frontend/Dockerfile.dev
Normal file
@@ -0,0 +1,15 @@
|
||||
# Stage 1: Build
|
||||
FROM node:20 as build
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
# Use development configuration to pick up environment.ts (localhost)
|
||||
RUN npm run build -- --configuration=development
|
||||
|
||||
# Stage 2: Serve
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
@@ -48,6 +48,7 @@
|
||||
<label>COLORE</label>
|
||||
<app-color-selector
|
||||
[selectedColor]="item.color"
|
||||
[variants]="currentMaterialVariants()"
|
||||
(colorSelected)="updateItemColor(i, $event)">
|
||||
</app-color-selector>
|
||||
</div>
|
||||
@@ -80,20 +81,20 @@
|
||||
<app-select
|
||||
formControlName="material"
|
||||
[label]="'CALC.MATERIAL' | translate"
|
||||
[options]="materials"
|
||||
[options]="materials()"
|
||||
></app-select>
|
||||
|
||||
@if (mode() === 'easy') {
|
||||
<app-select
|
||||
formControlName="quality"
|
||||
[label]="'CALC.QUALITY' | translate"
|
||||
[options]="qualities"
|
||||
[options]="qualities()"
|
||||
></app-select>
|
||||
} @else {
|
||||
<app-select
|
||||
formControlName="nozzleDiameter"
|
||||
[label]="'CALC.NOZZLE' | translate"
|
||||
[options]="nozzleDiameters"
|
||||
[options]="nozzleDiameters()"
|
||||
></app-select>
|
||||
}
|
||||
</div>
|
||||
@@ -105,13 +106,13 @@
|
||||
<app-select
|
||||
formControlName="infillPattern"
|
||||
[label]="'CALC.PATTERN' | translate"
|
||||
[options]="infillPatterns"
|
||||
[options]="infillPatterns()"
|
||||
></app-select>
|
||||
|
||||
<app-select
|
||||
formControlName="layerHeight"
|
||||
[label]="'CALC.LAYER_HEIGHT' | translate"
|
||||
[options]="layerHeights"
|
||||
[options]="layerHeights()"
|
||||
></app-select>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +148,7 @@
|
||||
|
||||
<app-button
|
||||
type="submit"
|
||||
[disabled]="form.invalid || items().length === 0 || loading()"
|
||||
[disabled]="items().length === 0 || loading()"
|
||||
[fullWidth]="true">
|
||||
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
|
||||
</app-button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, input, output, signal, effect } from '@angular/core';
|
||||
import { Component, input, output, signal, effect, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -8,7 +8,7 @@ import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone
|
||||
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
|
||||
import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component';
|
||||
import { QuoteRequest } from '../../services/quote-estimator.service';
|
||||
import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, MaterialOption, VariantOption } from '../../services/quote-estimator.service';
|
||||
import { getColorHex } from '../../../../core/constants/colors.const';
|
||||
|
||||
interface FormItem {
|
||||
@@ -24,69 +24,110 @@ interface FormItem {
|
||||
templateUrl: './upload-form.component.html',
|
||||
styleUrl: './upload-form.component.scss'
|
||||
})
|
||||
export class UploadFormComponent {
|
||||
export class UploadFormComponent implements OnInit {
|
||||
mode = input<'easy' | 'advanced'>('easy');
|
||||
loading = input<boolean>(false);
|
||||
uploadProgress = input<number>(0);
|
||||
submitRequest = output<QuoteRequest>();
|
||||
|
||||
private estimator = inject(QuoteEstimatorService);
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
items = signal<FormItem[]>([]);
|
||||
selectedFile = signal<File | null>(null);
|
||||
|
||||
materials = [
|
||||
{ label: 'PLA (Standard)', value: 'PLA' },
|
||||
{ label: 'PETG (Resistente)', value: 'PETG' },
|
||||
{ label: 'TPU (Flessibile)', value: 'TPU' }
|
||||
];
|
||||
// Dynamic Options
|
||||
materials = signal<SimpleOption[]>([]);
|
||||
qualities = signal<SimpleOption[]>([]);
|
||||
nozzleDiameters = signal<SimpleOption[]>([]);
|
||||
infillPatterns = signal<SimpleOption[]>([]);
|
||||
layerHeights = signal<SimpleOption[]>([]);
|
||||
|
||||
qualities = [
|
||||
{ label: 'Bozza (Fast)', value: 'Draft' },
|
||||
{ label: 'Standard', value: 'Standard' },
|
||||
{ label: 'Alta definizione', value: 'High' }
|
||||
];
|
||||
// Store full material options to lookup variants/colors if needed later
|
||||
private fullMaterialOptions: MaterialOption[] = [];
|
||||
|
||||
nozzleDiameters = [
|
||||
{ label: '0.2 mm (+2 CHF)', value: 0.2 },
|
||||
{ label: '0.4 mm (Standard)', value: 0.4 },
|
||||
{ label: '0.6 mm (+2 CHF)', value: 0.6 },
|
||||
{ label: '0.8 mm (+2 CHF)', value: 0.8 }
|
||||
];
|
||||
// Computed variants for valid material
|
||||
currentMaterialVariants = signal<VariantOption[]>([]);
|
||||
|
||||
infillPatterns = [
|
||||
{ label: 'Grid', value: 'grid' },
|
||||
{ label: 'Gyroid', value: 'gyroid' },
|
||||
{ label: 'Cubic', value: 'cubic' },
|
||||
{ label: 'Triangles', value: 'triangles' }
|
||||
];
|
||||
|
||||
layerHeights = [
|
||||
{ label: '0.08 mm', value: 0.08 },
|
||||
{ label: '0.12 mm (High Quality - Slow)', value: 0.12 },
|
||||
{ label: '0.16 mm', value: 0.16 },
|
||||
{ label: '0.20 mm (Standard)', value: 0.20 },
|
||||
{ label: '0.24 mm', value: 0.24 },
|
||||
{ label: '0.28 mm', value: 0.28 }
|
||||
];
|
||||
private updateVariants() {
|
||||
const matCode = this.form.get('material')?.value;
|
||||
if (matCode && this.fullMaterialOptions.length > 0) {
|
||||
const found = this.fullMaterialOptions.find(m => m.code === matCode);
|
||||
this.currentMaterialVariants.set(found ? found.variants : []);
|
||||
} else {
|
||||
this.currentMaterialVariants.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
acceptedFormats = '.stl,.3mf,.step,.stp,.obj,.amf,.ply,.igs,.iges';
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
constructor() {
|
||||
this.form = this.fb.group({
|
||||
itemsTouched: [false], // Hack to track touched state for custom items list
|
||||
material: ['PLA', Validators.required],
|
||||
quality: ['Standard', Validators.required],
|
||||
// Print Speed removed
|
||||
material: ['', Validators.required],
|
||||
quality: ['', Validators.required],
|
||||
items: [[]], // Track items in form for validation if needed
|
||||
notes: [''],
|
||||
// Advanced fields
|
||||
// Color removed from global form
|
||||
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
|
||||
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
||||
nozzleDiameter: [0.4, Validators.required],
|
||||
infillPattern: ['grid'],
|
||||
supportEnabled: [false]
|
||||
});
|
||||
|
||||
// Listen to material changes to update variants
|
||||
this.form.get('material')?.valueChanges.subscribe(() => {
|
||||
this.updateVariants();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.estimator.getOptions().subscribe({
|
||||
next: (options: OptionsResponse) => {
|
||||
this.fullMaterialOptions = options.materials;
|
||||
this.updateVariants(); // Trigger initial update
|
||||
|
||||
this.materials.set(options.materials.map(m => ({ label: m.label, value: m.code })));
|
||||
this.qualities.set(options.qualities.map(q => ({ label: q.label, value: q.id })));
|
||||
this.infillPatterns.set(options.infillPatterns.map(p => ({ label: p.label, value: p.id })));
|
||||
this.layerHeights.set(options.layerHeights.map(l => ({ label: l.label, value: l.value })));
|
||||
this.nozzleDiameters.set(options.nozzleDiameters.map(n => ({ label: n.label, value: n.value })));
|
||||
|
||||
this.setDefaults();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load options', err);
|
||||
// Fallback for debugging/offline dev
|
||||
this.materials.set([{ label: 'PLA (Fallback)', value: 'PLA' }]);
|
||||
this.qualities.set([{ label: 'Standard', value: 'standard' }]);
|
||||
this.nozzleDiameters.set([{ label: '0.4 mm', value: 0.4 }]);
|
||||
this.setDefaults();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaults() {
|
||||
// Set Defaults if available
|
||||
if (this.materials().length > 0 && !this.form.get('material')?.value) {
|
||||
this.form.get('material')?.setValue(this.materials()[0].value);
|
||||
}
|
||||
if (this.qualities().length > 0 && !this.form.get('quality')?.value) {
|
||||
// Try to find 'standard' or use first
|
||||
const std = this.qualities().find(q => q.value === 'standard');
|
||||
this.form.get('quality')?.setValue(std ? std.value : this.qualities()[0].value);
|
||||
}
|
||||
if (this.nozzleDiameters().length > 0 && !this.form.get('nozzleDiameter')?.value) {
|
||||
this.form.get('nozzleDiameter')?.setValue(0.4); // Prefer 0.4
|
||||
}
|
||||
if (this.layerHeights().length > 0 && !this.form.get('layerHeight')?.value) {
|
||||
this.form.get('layerHeight')?.setValue(0.2); // Prefer 0.2
|
||||
}
|
||||
if (this.infillPatterns().length > 0 && !this.form.get('infillPattern')?.value) {
|
||||
this.form.get('infillPattern')?.setValue(this.infillPatterns()[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
onFilesDropped(newFiles: File[]) {
|
||||
@@ -187,13 +228,25 @@ export class UploadFormComponent {
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
console.log('UploadFormComponent: onSubmit triggered');
|
||||
console.log('Form Valid:', this.form.valid, 'Items:', this.items().length);
|
||||
|
||||
if (this.form.valid && this.items().length > 0) {
|
||||
console.log('UploadFormComponent: Emitting submitRequest', this.form.value);
|
||||
this.submitRequest.emit({
|
||||
items: this.items(), // Pass the items array including colors
|
||||
...this.form.value,
|
||||
items: this.items(), // Pass the items array explicitly AFTER form value to prevent overwrite
|
||||
mode: this.mode()
|
||||
});
|
||||
} else {
|
||||
console.warn('UploadFormComponent: Form Invalid or No Items');
|
||||
console.log('Form Errors:', this.form.errors);
|
||||
Object.keys(this.form.controls).forEach(key => {
|
||||
const control = this.form.get(key);
|
||||
if (control?.invalid) {
|
||||
console.log('Invalid Control:', key, control.errors, 'Value:', control.value);
|
||||
}
|
||||
});
|
||||
this.form.markAllAsTouched();
|
||||
this.form.get('itemsTouched')?.setValue(true);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient, HttpEventType } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { map, catchError, tap } from 'rxjs/operators';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
export interface QuoteRequest {
|
||||
@@ -49,14 +49,70 @@ interface BackendResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Options Interfaces
|
||||
export interface MaterialOption {
|
||||
code: string;
|
||||
label: string;
|
||||
variants: VariantOption[];
|
||||
}
|
||||
export interface VariantOption {
|
||||
name: string;
|
||||
colorName: string;
|
||||
hexColor: string;
|
||||
isOutOfStock: boolean;
|
||||
}
|
||||
export interface QualityOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
export interface InfillOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
export interface NumericOption {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface OptionsResponse {
|
||||
materials: MaterialOption[];
|
||||
qualities: QualityOption[];
|
||||
infillPatterns: InfillOption[];
|
||||
layerHeights: NumericOption[];
|
||||
nozzleDiameters: NumericOption[];
|
||||
}
|
||||
|
||||
// UI Option for Select Component
|
||||
export interface SimpleOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class QuoteEstimatorService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getOptions(): Observable<OptionsResponse> {
|
||||
console.log('QuoteEstimatorService: Requesting options...');
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get<OptionsResponse>(`${environment.apiUrl}/api/calculator/options`, { headers }).pipe(
|
||||
tap({
|
||||
next: (res) => console.log('QuoteEstimatorService: Options loaded', res),
|
||||
error: (err) => console.error('QuoteEstimatorService: Options failed', err)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||
if (request.items.length === 0) return of();
|
||||
console.log('QuoteEstimatorService: Calculating quote...', request);
|
||||
if (request.items.length === 0) {
|
||||
console.warn('QuoteEstimatorService: No items to calculate');
|
||||
return of();
|
||||
}
|
||||
|
||||
return new Observable(observer => {
|
||||
const totalItems = request.items.length;
|
||||
@@ -67,7 +123,24 @@ export class QuoteEstimatorService {
|
||||
const uploads = request.items.map((item, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', item.file);
|
||||
formData.append('machine', 'bambu_a1');
|
||||
// machine param removed - backend uses default active
|
||||
|
||||
// Map material? Or trust frontend to send correct code?
|
||||
// Since we fetch options now, we should send the code directly.
|
||||
// But for backward compat/safety/mapping logic in mapMaterial, let's keep it or update it.
|
||||
// If frontend sends 'PLA', mapMaterial returns 'pla_basic'.
|
||||
// We should check if request.material is already a code from options.
|
||||
// For now, let's assume request.material IS the code if it matches our new options,
|
||||
// or fallback to mapper if it's old legacy string.
|
||||
// Let's keep mapMaterial but update it to be smarter if needed, or rely on UploadForm to send correct codes.
|
||||
// For now, let's use mapMaterial as safety, assuming frontend sends short codes 'PLA'.
|
||||
// Wait, if we use dynamic options, the 'value' in select will be the 'code' from backend (e.g. 'PLA').
|
||||
// Backend expects 'pla_basic' or just 'PLA'?
|
||||
// QuoteController -> processRequest -> SlicerService.slice -> assumes 'filament' is a profile name like 'pla_basic'.
|
||||
// So we MUST map 'PLA' to 'pla_basic' UNLESS backend options return 'pla_basic' as code.
|
||||
// Backend OptionsController returns type.getMaterialCode() which is 'PLA'.
|
||||
// So we still need mapping to slicer profile names.
|
||||
|
||||
formData.append('filament', this.mapMaterial(request.material));
|
||||
formData.append('quality', this.mapQuality(request.quality));
|
||||
|
||||
@@ -104,9 +177,6 @@ export class QuoteEstimatorService {
|
||||
|
||||
if (wrapper.error) {
|
||||
finalResponses[idx] = { success: false, fileName: wrapper.item.file.name };
|
||||
// Even if error, we count as complete
|
||||
// But we need to handle completion logic carefully.
|
||||
// For simplicity, let's treat it as complete but check later.
|
||||
}
|
||||
|
||||
const event = wrapper.event;
|
||||
@@ -159,8 +229,6 @@ export class QuoteEstimatorService {
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
// If at least one failed? Or all?
|
||||
// For now if NO items succeeded, error.
|
||||
observer.error('All calculations failed.');
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +264,6 @@ export class QuoteEstimatorService {
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error in request subscription', err);
|
||||
// Should be caught by inner pipe, but safety net
|
||||
completedRequests++;
|
||||
if (completedRequests === totalItems) {
|
||||
observer.error('Requests failed');
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
@if (isOpen()) {
|
||||
<div class="color-popup">
|
||||
@for (category of categories; track category.name) {
|
||||
@for (category of categories(); track category.name) {
|
||||
<div class="category">
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
<div class="colors-grid">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { Component, input, output, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
|
||||
import { VariantOption } from '../../../features/calculator/services/quote-estimator.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-color-selector',
|
||||
@@ -12,11 +13,28 @@ import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../.
|
||||
})
|
||||
export class ColorSelectorComponent {
|
||||
selectedColor = input<string>('Black');
|
||||
variants = input<VariantOption[]>([]);
|
||||
colorSelected = output<string>();
|
||||
|
||||
isOpen = signal(false);
|
||||
|
||||
categories: ColorCategory[] = PRODUCT_COLORS;
|
||||
categories = computed(() => {
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
// Flatten variants into a single category for now
|
||||
// We could try to group by extracting words, but "Colors" is fine.
|
||||
return [{
|
||||
name: 'Available Colors',
|
||||
colors: vars.map(v => ({
|
||||
label: v.colorName, // Display "Red"
|
||||
value: v.colorName, // Send "Red" to backend
|
||||
hex: v.hexColor,
|
||||
outOfStock: v.isOutOfStock
|
||||
}))
|
||||
}] as ColorCategory[];
|
||||
}
|
||||
return PRODUCT_COLORS;
|
||||
});
|
||||
|
||||
toggleOpen() {
|
||||
this.isOpen.update(v => !v);
|
||||
@@ -31,6 +49,13 @@ export class ColorSelectorComponent {
|
||||
|
||||
// Helper to find hex for the current selected value
|
||||
getCurrentHex(): string {
|
||||
// Check in dynamic variants first
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
const found = vars.find(v => v.colorName === this.selectedColor());
|
||||
if (found) return found.hexColor;
|
||||
}
|
||||
|
||||
return getColorHex(this.selectedColor());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://dev.3d-fab.ch',
|
||||
apiUrl: 'http://localhost:8000',
|
||||
basicAuth: 'fab:0presura' // Format: 'username:password'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user