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;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Profile("local")
|
|
||||||
public class CorsConfig implements WebMvcConfigurer {
|
public class CorsConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOrigins("*")
|
.allowedOrigins("http://localhost", "http://localhost:4200", "http://localhost:80", "http://127.0.0.1")
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
.allowedHeaders("*")
|
.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;
|
package com.printcalculator.controller;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
import com.printcalculator.model.PrintStats;
|
import com.printcalculator.model.PrintStats;
|
||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
|
import com.printcalculator.repository.PrinterMachineRepository;
|
||||||
import com.printcalculator.service.QuoteCalculator;
|
import com.printcalculator.service.QuoteCalculator;
|
||||||
import com.printcalculator.service.SlicerService;
|
import com.printcalculator.service.SlicerService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -17,28 +21,33 @@ public class QuoteController {
|
|||||||
|
|
||||||
private final SlicerService slicerService;
|
private final SlicerService slicerService;
|
||||||
private final QuoteCalculator quoteCalculator;
|
private final QuoteCalculator quoteCalculator;
|
||||||
|
private final PrinterMachineRepository machineRepo;
|
||||||
|
|
||||||
// Defaults (using aliases defined in ProfileManager)
|
// 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_FILAMENT = "pla_basic";
|
||||||
private static final String DEFAULT_PROCESS = "standard";
|
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.slicerService = slicerService;
|
||||||
this.quoteCalculator = quoteCalculator;
|
this.quoteCalculator = quoteCalculator;
|
||||||
|
this.machineRepo = machineRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/quote")
|
@PostMapping("/api/quote")
|
||||||
public ResponseEntity<QuoteResult> calculateQuote(
|
public ResponseEntity<QuoteResult> calculateQuote(
|
||||||
@RequestParam("file") MultipartFile file,
|
@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 = "filament", required = false, defaultValue = DEFAULT_FILAMENT) String filament,
|
||||||
@RequestParam(value = "process", required = false) String process,
|
@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 {
|
) throws IOException {
|
||||||
|
|
||||||
// Frontend sends 'quality', backend expects 'process'.
|
// ... process selection logic ...
|
||||||
// If process is missing, try quality. If both missing, use default.
|
|
||||||
String actualProcess = process;
|
String actualProcess = process;
|
||||||
if (actualProcess == null || actualProcess.isEmpty()) {
|
if (actualProcess == null || actualProcess.isEmpty()) {
|
||||||
if (quality != null && !quality.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")
|
@PostMapping("/calculate/stl")
|
||||||
@@ -56,30 +89,37 @@ public class QuoteController {
|
|||||||
@RequestParam("file") MultipartFile file
|
@RequestParam("file") MultipartFile file
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
// Legacy endpoint uses defaults
|
// 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()) {
|
if (file.isEmpty()) {
|
||||||
return ResponseEntity.badRequest().build();
|
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
|
// Save uploaded file temporarily
|
||||||
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||||
try {
|
try {
|
||||||
file.transferTo(tempInput.toFile());
|
file.transferTo(tempInput.toFile());
|
||||||
|
|
||||||
// Slice
|
String slicerMachineProfile = "bambu_a1"; // TODO: Add to PrinterMachine entity
|
||||||
PrintStats stats = slicerService.slice(tempInput.toFile(), machine, filament, process);
|
|
||||||
|
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
||||||
|
|
||||||
// Calculate Quote
|
// Calculate Quote (Pass machine display name for pricing lookup)
|
||||||
QuoteResult result = quoteCalculator.calculate(stats);
|
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament);
|
||||||
|
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return ResponseEntity.internalServerError().build(); // Simplify error handling for now
|
return ResponseEntity.internalServerError().build();
|
||||||
} finally {
|
} finally {
|
||||||
Files.deleteIfExists(tempInput);
|
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 machineCost,
|
||||||
BigDecimal energyCost,
|
BigDecimal energyCost,
|
||||||
BigDecimal subtotal,
|
BigDecimal subtotal,
|
||||||
BigDecimal markupAmount
|
BigDecimal markup
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -1,12 +1,51 @@
|
|||||||
package com.printcalculator.model;
|
package com.printcalculator.model;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record QuoteResult(
|
public class QuoteResult {
|
||||||
BigDecimal totalPrice,
|
private double totalPrice;
|
||||||
String currency,
|
private String currency;
|
||||||
PrintStats stats,
|
private PrintStats stats;
|
||||||
CostBreakdown breakdown,
|
|
||||||
List<String> notes
|
@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
|
// ; estimated printing time = 1h 2m 3s
|
||||||
// ; filament used [g] = 12.34
|
// ; filament used [g] = 12.34
|
||||||
// ; filament used [mm] = 1234.56
|
// ; 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_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*(.*)");
|
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))) {
|
try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) {
|
||||||
String line;
|
String line;
|
||||||
// Scan first 5000 lines for efficiency (metadata might be further down)
|
|
||||||
int count = 0;
|
// Scan entire file as metadata is often at the end
|
||||||
while ((line = reader.readLine()) != null && count < 5000) {
|
while ((line = reader.readLine()) != null) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
|
|
||||||
|
// OrcaSlicer comments start with ;
|
||||||
if (!line.startsWith(";")) {
|
if (!line.startsWith(";")) {
|
||||||
count++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.toLowerCase().contains("estimated printing time")) {
|
||||||
|
System.out.println("DEBUG: Found potential time line: '" + line + "'");
|
||||||
|
}
|
||||||
|
|
||||||
Matcher timeMatcher = TIME_PATTERN.matcher(line);
|
Matcher timeMatcher = TIME_PATTERN.matcher(line);
|
||||||
if (timeMatcher.find()) {
|
if (timeMatcher.find()) {
|
||||||
timeFormatted = timeMatcher.group(1).trim();
|
timeFormatted = timeMatcher.group(1).trim();
|
||||||
seconds = parseTimeString(timeFormatted);
|
seconds = parseTimeString(timeFormatted);
|
||||||
|
System.out.println("GCodeParser: Found time: " + timeFormatted + " (" + seconds + "s)");
|
||||||
}
|
}
|
||||||
|
|
||||||
Matcher weightMatcher = FILAMENT_G_PATTERN.matcher(line);
|
Matcher weightMatcher = FILAMENT_G_PATTERN.matcher(line);
|
||||||
if (weightMatcher.find()) {
|
if (weightMatcher.find()) {
|
||||||
try {
|
try {
|
||||||
weightG = Double.parseDouble(weightMatcher.group(1).trim());
|
weightG = Double.parseDouble(weightMatcher.group(1).trim());
|
||||||
|
System.out.println("GCodeParser: Found weight: " + weightG + "g");
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +62,9 @@ public class GCodeParser {
|
|||||||
if (lengthMatcher.find()) {
|
if (lengthMatcher.find()) {
|
||||||
try {
|
try {
|
||||||
lengthMm = Double.parseDouble(lengthMatcher.group(1).trim());
|
lengthMm = Double.parseDouble(lengthMatcher.group(1).trim());
|
||||||
|
System.out.println("GCodeParser: Found length: " + lengthMm + "mm");
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,177 @@
|
|||||||
package com.printcalculator.service;
|
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.CostBreakdown;
|
||||||
import com.printcalculator.model.PrintStats;
|
import com.printcalculator.model.PrintStats;
|
||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
|
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||||
|
import com.printcalculator.repository.FilamentVariantRepository;
|
||||||
|
import com.printcalculator.repository.PricingPolicyMachineHourTierRepository;
|
||||||
|
import com.printcalculator.repository.PricingPolicyRepository;
|
||||||
|
import com.printcalculator.repository.PrinterMachineRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class QuoteCalculator {
|
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) {
|
public QuoteCalculator(PricingPolicyRepository pricingRepo,
|
||||||
this.props = props;
|
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
|
// Material Cost: (weight / 1000) * costPerKg
|
||||||
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
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
|
// Machine Cost: Tiered
|
||||||
BigDecimal hours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal machineCost = hours.multiply(BigDecimal.valueOf(props.getMachineCostPerHour()));
|
BigDecimal machineCost = calculateMachineCost(policy, totalHours);
|
||||||
|
|
||||||
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
||||||
BigDecimal kw = BigDecimal.valueOf(props.getPrinterPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
BigDecimal kw = BigDecimal.valueOf(machine.getPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal kwh = kw.multiply(hours);
|
BigDecimal kwh = kw.multiply(totalHours);
|
||||||
BigDecimal energyCost = kwh.multiply(BigDecimal.valueOf(props.getEnergyCostPerKwh()));
|
BigDecimal energyCost = kwh.multiply(policy.getElectricityCostChfPerKwh());
|
||||||
|
|
||||||
// Subtotal
|
// Subtotal (Costs + Fixed Fees)
|
||||||
BigDecimal subtotal = materialCost.add(machineCost).add(energyCost);
|
BigDecimal fixedFee = policy.getFixedJobFeeChf();
|
||||||
|
BigDecimal subtotal = materialCost.add(machineCost).add(energyCost).add(fixedFee);
|
||||||
|
|
||||||
// Markup
|
// 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 totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
BigDecimal markupAmount = totalPrice.subtract(subtotal);
|
BigDecimal markupAmount = totalPrice.subtract(subtotal);
|
||||||
|
|
||||||
CostBreakdown breakdown = new CostBreakdown(
|
CostBreakdown breakdown = new CostBreakdown(
|
||||||
materialCost.setScale(2, RoundingMode.HALF_UP),
|
materialCost.setScale(2, RoundingMode.HALF_UP),
|
||||||
machineCost.setScale(2, RoundingMode.HALF_UP),
|
machineCost.setScale(2, RoundingMode.HALF_UP),
|
||||||
energyCost.setScale(2, RoundingMode.HALF_UP),
|
energyCost.setScale(2, RoundingMode.HALF_UP),
|
||||||
subtotal.setScale(2, RoundingMode.HALF_UP),
|
subtotal.setScale(2, RoundingMode.HALF_UP),
|
||||||
markupAmount.setScale(2, RoundingMode.HALF_UP)
|
markupAmount.setScale(2, RoundingMode.HALF_UP)
|
||||||
);
|
);
|
||||||
|
|
||||||
List<String> notes = new ArrayList<>();
|
|
||||||
// notes.add("Generated via Dynamic Slicer (Java Backend)");
|
|
||||||
|
|
||||||
return new QuoteResult(totalPrice, "CHF", stats, breakdown, notes);
|
List<String> notes = new ArrayList<>();
|
||||||
|
notes.add("Policy: " + policy.getPolicyName());
|
||||||
|
notes.add("Machine: " + machine.getPrinterDisplayName());
|
||||||
|
notes.add("Material: " + variant.getVariantDisplayName());
|
||||||
|
|
||||||
|
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.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@@ -36,12 +37,21 @@ public class SlicerService {
|
|||||||
this.mapper = mapper;
|
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
|
// 1. Prepare Profiles
|
||||||
ObjectNode machineProfile = profileManager.getMergedProfile(machineName, "machine");
|
ObjectNode machineProfile = profileManager.getMergedProfile(machineName, "machine");
|
||||||
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
|
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
|
||||||
ObjectNode processProfile = profileManager.getMergedProfile(processName, "process");
|
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
|
// 2. Create Temp Dir
|
||||||
Path tempDir = Files.createTempDirectory("slicer_job_");
|
Path tempDir = Files.createTempDirectory("slicer_job_");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|||||||
slicer.path=${SLICER_PATH:/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer}
|
slicer.path=${SLICER_PATH:/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer}
|
||||||
profiles.root=${PROFILES_DIR:profiles}
|
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
|
# File Upload Limits
|
||||||
spring.servlet.multipart.max-file-size=200MB
|
spring.servlet.multipart.max-file-size=200MB
|
||||||
|
|||||||
@@ -52,6 +52,26 @@ class GCodeParserTest {
|
|||||||
assertEquals(750, stats.printTimeSeconds()); // 12*60 + 30
|
assertEquals(750, stats.printTimeSeconds()); // 12*60 + 30
|
||||||
assertEquals(5.0, stats.filamentWeightGrams(), 0.001);
|
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();
|
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:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
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
|
- FILAMENT_COST_PER_KG=22.0
|
||||||
- MACHINE_COST_PER_HOUR=2.50
|
- MACHINE_COST_PER_HOUR=2.50
|
||||||
- ENERGY_COST_PER_KWH=0.30
|
- ENERGY_COST_PER_KWH=0.30
|
||||||
@@ -16,10 +20,14 @@ services:
|
|||||||
- MARKUP_PERCENT=20
|
- MARKUP_PERCENT=20
|
||||||
- TEMP_DIR=/app/temp
|
- TEMP_DIR=/app/temp
|
||||||
- PROFILES_DIR=/app/profiles
|
- PROFILES_DIR=/app/profiles
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
container_name: print-calculator-frontend
|
container_name: print-calculator-frontend
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "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>
|
<label>COLORE</label>
|
||||||
<app-color-selector
|
<app-color-selector
|
||||||
[selectedColor]="item.color"
|
[selectedColor]="item.color"
|
||||||
|
[variants]="currentMaterialVariants()"
|
||||||
(colorSelected)="updateItemColor(i, $event)">
|
(colorSelected)="updateItemColor(i, $event)">
|
||||||
</app-color-selector>
|
</app-color-selector>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,20 +81,20 @@
|
|||||||
<app-select
|
<app-select
|
||||||
formControlName="material"
|
formControlName="material"
|
||||||
[label]="'CALC.MATERIAL' | translate"
|
[label]="'CALC.MATERIAL' | translate"
|
||||||
[options]="materials"
|
[options]="materials()"
|
||||||
></app-select>
|
></app-select>
|
||||||
|
|
||||||
@if (mode() === 'easy') {
|
@if (mode() === 'easy') {
|
||||||
<app-select
|
<app-select
|
||||||
formControlName="quality"
|
formControlName="quality"
|
||||||
[label]="'CALC.QUALITY' | translate"
|
[label]="'CALC.QUALITY' | translate"
|
||||||
[options]="qualities"
|
[options]="qualities()"
|
||||||
></app-select>
|
></app-select>
|
||||||
} @else {
|
} @else {
|
||||||
<app-select
|
<app-select
|
||||||
formControlName="nozzleDiameter"
|
formControlName="nozzleDiameter"
|
||||||
[label]="'CALC.NOZZLE' | translate"
|
[label]="'CALC.NOZZLE' | translate"
|
||||||
[options]="nozzleDiameters"
|
[options]="nozzleDiameters()"
|
||||||
></app-select>
|
></app-select>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -105,13 +106,13 @@
|
|||||||
<app-select
|
<app-select
|
||||||
formControlName="infillPattern"
|
formControlName="infillPattern"
|
||||||
[label]="'CALC.PATTERN' | translate"
|
[label]="'CALC.PATTERN' | translate"
|
||||||
[options]="infillPatterns"
|
[options]="infillPatterns()"
|
||||||
></app-select>
|
></app-select>
|
||||||
|
|
||||||
<app-select
|
<app-select
|
||||||
formControlName="layerHeight"
|
formControlName="layerHeight"
|
||||||
[label]="'CALC.LAYER_HEIGHT' | translate"
|
[label]="'CALC.LAYER_HEIGHT' | translate"
|
||||||
[options]="layerHeights"
|
[options]="layerHeights()"
|
||||||
></app-select>
|
></app-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
|
|
||||||
<app-button
|
<app-button
|
||||||
type="submit"
|
type="submit"
|
||||||
[disabled]="form.invalid || items().length === 0 || loading()"
|
[disabled]="items().length === 0 || loading()"
|
||||||
[fullWidth]="true">
|
[fullWidth]="true">
|
||||||
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
|
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
|
||||||
</app-button>
|
</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 { CommonModule } from '@angular/common';
|
||||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
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 { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||||
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
|
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
|
||||||
import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.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';
|
import { getColorHex } from '../../../../core/constants/colors.const';
|
||||||
|
|
||||||
interface FormItem {
|
interface FormItem {
|
||||||
@@ -24,69 +24,110 @@ interface FormItem {
|
|||||||
templateUrl: './upload-form.component.html',
|
templateUrl: './upload-form.component.html',
|
||||||
styleUrl: './upload-form.component.scss'
|
styleUrl: './upload-form.component.scss'
|
||||||
})
|
})
|
||||||
export class UploadFormComponent {
|
export class UploadFormComponent implements OnInit {
|
||||||
mode = input<'easy' | 'advanced'>('easy');
|
mode = input<'easy' | 'advanced'>('easy');
|
||||||
loading = input<boolean>(false);
|
loading = input<boolean>(false);
|
||||||
uploadProgress = input<number>(0);
|
uploadProgress = input<number>(0);
|
||||||
submitRequest = output<QuoteRequest>();
|
submitRequest = output<QuoteRequest>();
|
||||||
|
|
||||||
|
private estimator = inject(QuoteEstimatorService);
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
|
||||||
items = signal<FormItem[]>([]);
|
items = signal<FormItem[]>([]);
|
||||||
selectedFile = signal<File | null>(null);
|
selectedFile = signal<File | null>(null);
|
||||||
|
|
||||||
materials = [
|
// Dynamic Options
|
||||||
{ label: 'PLA (Standard)', value: 'PLA' },
|
materials = signal<SimpleOption[]>([]);
|
||||||
{ label: 'PETG (Resistente)', value: 'PETG' },
|
qualities = signal<SimpleOption[]>([]);
|
||||||
{ label: 'TPU (Flessibile)', value: 'TPU' }
|
nozzleDiameters = signal<SimpleOption[]>([]);
|
||||||
];
|
infillPatterns = signal<SimpleOption[]>([]);
|
||||||
|
layerHeights = signal<SimpleOption[]>([]);
|
||||||
qualities = [
|
|
||||||
{ label: 'Bozza (Fast)', value: 'Draft' },
|
|
||||||
{ label: 'Standard', value: 'Standard' },
|
|
||||||
{ label: 'Alta definizione', value: 'High' }
|
|
||||||
];
|
|
||||||
|
|
||||||
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 }
|
|
||||||
];
|
|
||||||
|
|
||||||
infillPatterns = [
|
// Store full material options to lookup variants/colors if needed later
|
||||||
{ label: 'Grid', value: 'grid' },
|
private fullMaterialOptions: MaterialOption[] = [];
|
||||||
{ 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 }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
// Computed variants for valid material
|
||||||
|
currentMaterialVariants = signal<VariantOption[]>([]);
|
||||||
|
|
||||||
|
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';
|
acceptedFormats = '.stl,.3mf,.step,.stp,.obj,.amf,.ply,.igs,.iges';
|
||||||
|
|
||||||
constructor(private fb: FormBuilder) {
|
constructor() {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
itemsTouched: [false], // Hack to track touched state for custom items list
|
itemsTouched: [false], // Hack to track touched state for custom items list
|
||||||
material: ['PLA', Validators.required],
|
material: ['', Validators.required],
|
||||||
quality: ['Standard', Validators.required],
|
quality: ['', Validators.required],
|
||||||
// Print Speed removed
|
items: [[]], // Track items in form for validation if needed
|
||||||
notes: [''],
|
notes: [''],
|
||||||
// Advanced fields
|
// Advanced fields
|
||||||
// Color removed from global form
|
|
||||||
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
|
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
|
||||||
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
||||||
nozzleDiameter: [0.4, Validators.required],
|
nozzleDiameter: [0.4, Validators.required],
|
||||||
infillPattern: ['grid'],
|
infillPattern: ['grid'],
|
||||||
supportEnabled: [false]
|
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[]) {
|
onFilesDropped(newFiles: File[]) {
|
||||||
@@ -187,13 +228,25 @@ export class UploadFormComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
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) {
|
if (this.form.valid && this.items().length > 0) {
|
||||||
|
console.log('UploadFormComponent: Emitting submitRequest', this.form.value);
|
||||||
this.submitRequest.emit({
|
this.submitRequest.emit({
|
||||||
items: this.items(), // Pass the items array including colors
|
|
||||||
...this.form.value,
|
...this.form.value,
|
||||||
|
items: this.items(), // Pass the items array explicitly AFTER form value to prevent overwrite
|
||||||
mode: this.mode()
|
mode: this.mode()
|
||||||
});
|
});
|
||||||
} else {
|
} 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.markAllAsTouched();
|
||||||
this.form.get('itemsTouched')?.setValue(true);
|
this.form.get('itemsTouched')?.setValue(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
import { HttpClient, HttpEventType } from '@angular/common/http';
|
import { HttpClient, HttpEventType } from '@angular/common/http';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map, catchError } from 'rxjs/operators';
|
import { map, catchError, tap } from 'rxjs/operators';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
export interface QuoteRequest {
|
export interface QuoteRequest {
|
||||||
@@ -49,14 +49,70 @@ interface BackendResponse {
|
|||||||
error?: string;
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class QuoteEstimatorService {
|
export class QuoteEstimatorService {
|
||||||
private http = inject(HttpClient);
|
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> {
|
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 => {
|
return new Observable(observer => {
|
||||||
const totalItems = request.items.length;
|
const totalItems = request.items.length;
|
||||||
@@ -67,7 +123,24 @@ export class QuoteEstimatorService {
|
|||||||
const uploads = request.items.map((item, index) => {
|
const uploads = request.items.map((item, index) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', item.file);
|
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('filament', this.mapMaterial(request.material));
|
||||||
formData.append('quality', this.mapQuality(request.quality));
|
formData.append('quality', this.mapQuality(request.quality));
|
||||||
|
|
||||||
@@ -104,9 +177,6 @@ export class QuoteEstimatorService {
|
|||||||
|
|
||||||
if (wrapper.error) {
|
if (wrapper.error) {
|
||||||
finalResponses[idx] = { success: false, fileName: wrapper.item.file.name };
|
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;
|
const event = wrapper.event;
|
||||||
@@ -159,8 +229,6 @@ export class QuoteEstimatorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
// If at least one failed? Or all?
|
|
||||||
// For now if NO items succeeded, error.
|
|
||||||
observer.error('All calculations failed.');
|
observer.error('All calculations failed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -196,7 +264,6 @@ export class QuoteEstimatorService {
|
|||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Error in request subscription', err);
|
console.error('Error in request subscription', err);
|
||||||
// Should be caught by inner pipe, but safety net
|
|
||||||
completedRequests++;
|
completedRequests++;
|
||||||
if (completedRequests === totalItems) {
|
if (completedRequests === totalItems) {
|
||||||
observer.error('Requests failed');
|
observer.error('Requests failed');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
@if (isOpen()) {
|
@if (isOpen()) {
|
||||||
<div class="color-popup">
|
<div class="color-popup">
|
||||||
@for (category of categories; track category.name) {
|
@for (category of categories(); track category.name) {
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<div class="category-name">{{ category.name }}</div>
|
<div class="category-name">{{ category.name }}</div>
|
||||||
<div class="colors-grid">
|
<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 { CommonModule } from '@angular/common';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
|
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
|
||||||
|
import { VariantOption } from '../../../features/calculator/services/quote-estimator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-color-selector',
|
selector: 'app-color-selector',
|
||||||
@@ -12,11 +13,28 @@ import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../.
|
|||||||
})
|
})
|
||||||
export class ColorSelectorComponent {
|
export class ColorSelectorComponent {
|
||||||
selectedColor = input<string>('Black');
|
selectedColor = input<string>('Black');
|
||||||
|
variants = input<VariantOption[]>([]);
|
||||||
colorSelected = output<string>();
|
colorSelected = output<string>();
|
||||||
|
|
||||||
isOpen = signal(false);
|
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() {
|
toggleOpen() {
|
||||||
this.isOpen.update(v => !v);
|
this.isOpen.update(v => !v);
|
||||||
@@ -31,6 +49,13 @@ export class ColorSelectorComponent {
|
|||||||
|
|
||||||
// Helper to find hex for the current selected value
|
// Helper to find hex for the current selected value
|
||||||
getCurrentHex(): string {
|
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());
|
return getColorHex(this.selectedColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'https://dev.3d-fab.ch',
|
apiUrl: 'http://localhost:8000',
|
||||||
basicAuth: 'fab:0presura' // Format: 'username:password'
|
basicAuth: 'fab:0presura' // Format: 'username:password'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user