feat/calculator-options #26
@@ -353,6 +353,12 @@ public class OrderController {
|
|||||||
idto.setOriginalFilename(i.getOriginalFilename());
|
idto.setOriginalFilename(i.getOriginalFilename());
|
||||||
idto.setMaterialCode(i.getMaterialCode());
|
idto.setMaterialCode(i.getMaterialCode());
|
||||||
idto.setColorCode(i.getColorCode());
|
idto.setColorCode(i.getColorCode());
|
||||||
|
idto.setQuality(i.getQuality());
|
||||||
|
idto.setNozzleDiameterMm(i.getNozzleDiameterMm());
|
||||||
|
idto.setLayerHeightMm(i.getLayerHeightMm());
|
||||||
|
idto.setInfillPercent(i.getInfillPercent());
|
||||||
|
idto.setInfillPattern(i.getInfillPattern());
|
||||||
|
idto.setSupportsEnabled(i.getSupportsEnabled());
|
||||||
idto.setQuantity(i.getQuantity());
|
idto.setQuantity(i.getQuantity());
|
||||||
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
|
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
|
||||||
idto.setMaterialGrams(i.getMaterialGrams());
|
idto.setMaterialGrams(i.getMaterialGrams());
|
||||||
|
|||||||
@@ -268,6 +268,15 @@ public class QuoteSessionController {
|
|||||||
item.setQuantity(1);
|
item.setQuantity(1);
|
||||||
item.setColorCode(selectedVariant.getColorName());
|
item.setColorCode(selectedVariant.getColorName());
|
||||||
item.setFilamentVariant(selectedVariant);
|
item.setFilamentVariant(selectedVariant);
|
||||||
|
item.setMaterialCode(selectedVariant.getFilamentMaterialType() != null
|
||||||
|
? selectedVariant.getFilamentMaterialType().getMaterialCode()
|
||||||
|
: normalizeRequestedMaterialCode(settings.getMaterial()));
|
||||||
|
item.setQuality(resolveQuality(settings, layerHeight));
|
||||||
|
item.setNozzleDiameterMm(nozzleDiameter);
|
||||||
|
item.setLayerHeightMm(layerHeight);
|
||||||
|
item.setInfillPercent(settings.getInfillDensity() != null ? settings.getInfillDensity().intValue() : 20);
|
||||||
|
item.setInfillPattern(settings.getInfillPattern());
|
||||||
|
item.setSupportsEnabled(settings.getSupportsEnabled() != null ? settings.getSupportsEnabled() : false);
|
||||||
item.setStatus("READY"); // or CALCULATED
|
item.setStatus("READY"); // or CALCULATED
|
||||||
|
|
||||||
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
|
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
|
||||||
@@ -324,6 +333,8 @@ public class QuoteSessionController {
|
|||||||
settings.setInfillDensity(15.0);
|
settings.setInfillDensity(15.0);
|
||||||
settings.setInfillPattern("grid");
|
settings.setInfillPattern("grid");
|
||||||
break;
|
break;
|
||||||
|
case "extra_fine":
|
||||||
|
case "high_definition":
|
||||||
case "high":
|
case "high":
|
||||||
settings.setLayerHeight(0.12);
|
settings.setLayerHeight(0.12);
|
||||||
settings.setInfillDensity(20.0);
|
settings.setInfillDensity(20.0);
|
||||||
@@ -504,6 +515,13 @@ public class QuoteSessionController {
|
|||||||
dto.put("materialGrams", item.getMaterialGrams());
|
dto.put("materialGrams", item.getMaterialGrams());
|
||||||
dto.put("colorCode", item.getColorCode());
|
dto.put("colorCode", item.getColorCode());
|
||||||
dto.put("filamentVariantId", item.getFilamentVariant() != null ? item.getFilamentVariant().getId() : null);
|
dto.put("filamentVariantId", item.getFilamentVariant() != null ? item.getFilamentVariant().getId() : null);
|
||||||
|
dto.put("materialCode", item.getMaterialCode());
|
||||||
|
dto.put("quality", item.getQuality());
|
||||||
|
dto.put("nozzleDiameterMm", item.getNozzleDiameterMm());
|
||||||
|
dto.put("layerHeightMm", item.getLayerHeightMm());
|
||||||
|
dto.put("infillPercent", item.getInfillPercent());
|
||||||
|
dto.put("infillPattern", item.getInfillPattern());
|
||||||
|
dto.put("supportsEnabled", item.getSupportsEnabled());
|
||||||
dto.put("status", item.getStatus());
|
dto.put("status", item.getStatus());
|
||||||
dto.put("convertedStoredPath", extractConvertedStoredPath(item));
|
dto.put("convertedStoredPath", extractConvertedStoredPath(item));
|
||||||
|
|
||||||
@@ -667,4 +685,20 @@ public class QuoteSessionController {
|
|||||||
String path = String.valueOf(converted).trim();
|
String path = String.valueOf(converted).trim();
|
||||||
return path.isEmpty() ? null : path;
|
return path.isEmpty() ? null : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveQuality(com.printcalculator.dto.PrintSettingsDto settings, BigDecimal layerHeight) {
|
||||||
|
if (settings.getQuality() != null && !settings.getQuality().isBlank()) {
|
||||||
|
return settings.getQuality().trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
if (layerHeight == null) {
|
||||||
|
return "standard";
|
||||||
|
}
|
||||||
|
if (layerHeight.compareTo(BigDecimal.valueOf(0.24)) >= 0) {
|
||||||
|
return "draft";
|
||||||
|
}
|
||||||
|
if (layerHeight.compareTo(BigDecimal.valueOf(0.12)) <= 0) {
|
||||||
|
return "extra_fine";
|
||||||
|
}
|
||||||
|
return "standard";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,12 @@ public class AdminOrderController {
|
|||||||
idto.setOriginalFilename(i.getOriginalFilename());
|
idto.setOriginalFilename(i.getOriginalFilename());
|
||||||
idto.setMaterialCode(i.getMaterialCode());
|
idto.setMaterialCode(i.getMaterialCode());
|
||||||
idto.setColorCode(i.getColorCode());
|
idto.setColorCode(i.getColorCode());
|
||||||
|
idto.setQuality(i.getQuality());
|
||||||
|
idto.setNozzleDiameterMm(i.getNozzleDiameterMm());
|
||||||
|
idto.setLayerHeightMm(i.getLayerHeightMm());
|
||||||
|
idto.setInfillPercent(i.getInfillPercent());
|
||||||
|
idto.setInfillPattern(i.getInfillPattern());
|
||||||
|
idto.setSupportsEnabled(i.getSupportsEnabled());
|
||||||
idto.setQuantity(i.getQuantity());
|
idto.setQuantity(i.getQuantity());
|
||||||
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
|
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
|
||||||
idto.setMaterialGrams(i.getMaterialGrams());
|
idto.setMaterialGrams(i.getMaterialGrams());
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ public class OrderItemDto {
|
|||||||
private String originalFilename;
|
private String originalFilename;
|
||||||
private String materialCode;
|
private String materialCode;
|
||||||
private String colorCode;
|
private String colorCode;
|
||||||
|
private String quality;
|
||||||
|
private BigDecimal nozzleDiameterMm;
|
||||||
|
private BigDecimal layerHeightMm;
|
||||||
|
private Integer infillPercent;
|
||||||
|
private String infillPattern;
|
||||||
|
private Boolean supportsEnabled;
|
||||||
private Integer quantity;
|
private Integer quantity;
|
||||||
private Integer printTimeSeconds;
|
private Integer printTimeSeconds;
|
||||||
private BigDecimal materialGrams;
|
private BigDecimal materialGrams;
|
||||||
@@ -27,6 +33,24 @@ public class OrderItemDto {
|
|||||||
public String getColorCode() { return colorCode; }
|
public String getColorCode() { return colorCode; }
|
||||||
public void setColorCode(String colorCode) { this.colorCode = colorCode; }
|
public void setColorCode(String colorCode) { this.colorCode = colorCode; }
|
||||||
|
|
||||||
|
public String getQuality() { return quality; }
|
||||||
|
public void setQuality(String quality) { this.quality = quality; }
|
||||||
|
|
||||||
|
public BigDecimal getNozzleDiameterMm() { return nozzleDiameterMm; }
|
||||||
|
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) { this.nozzleDiameterMm = nozzleDiameterMm; }
|
||||||
|
|
||||||
|
public BigDecimal getLayerHeightMm() { return layerHeightMm; }
|
||||||
|
public void setLayerHeightMm(BigDecimal layerHeightMm) { this.layerHeightMm = layerHeightMm; }
|
||||||
|
|
||||||
|
public Integer getInfillPercent() { return infillPercent; }
|
||||||
|
public void setInfillPercent(Integer infillPercent) { this.infillPercent = infillPercent; }
|
||||||
|
|
||||||
|
public String getInfillPattern() { return infillPattern; }
|
||||||
|
public void setInfillPattern(String infillPattern) { this.infillPattern = infillPattern; }
|
||||||
|
|
||||||
|
public Boolean getSupportsEnabled() { return supportsEnabled; }
|
||||||
|
public void setSupportsEnabled(Boolean supportsEnabled) { this.supportsEnabled = supportsEnabled; }
|
||||||
|
|
||||||
public Integer getQuantity() { return quantity; }
|
public Integer getQuantity() { return quantity; }
|
||||||
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,24 @@ public class OrderItem {
|
|||||||
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
|
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
|
||||||
private String materialCode;
|
private String materialCode;
|
||||||
|
|
||||||
|
@Column(name = "quality", length = Integer.MAX_VALUE)
|
||||||
|
private String quality;
|
||||||
|
|
||||||
|
@Column(name = "nozzle_diameter_mm", precision = 4, scale = 2)
|
||||||
|
private BigDecimal nozzleDiameterMm;
|
||||||
|
|
||||||
|
@Column(name = "layer_height_mm", precision = 5, scale = 3)
|
||||||
|
private BigDecimal layerHeightMm;
|
||||||
|
|
||||||
|
@Column(name = "infill_percent")
|
||||||
|
private Integer infillPercent;
|
||||||
|
|
||||||
|
@Column(name = "infill_pattern", length = Integer.MAX_VALUE)
|
||||||
|
private String infillPattern;
|
||||||
|
|
||||||
|
@Column(name = "supports_enabled")
|
||||||
|
private Boolean supportsEnabled;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "filament_variant_id")
|
@JoinColumn(name = "filament_variant_id")
|
||||||
private FilamentVariant filamentVariant;
|
private FilamentVariant filamentVariant;
|
||||||
@@ -162,6 +180,54 @@ public class OrderItem {
|
|||||||
this.materialCode = materialCode;
|
this.materialCode = materialCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getQuality() {
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuality(String quality) {
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getNozzleDiameterMm() {
|
||||||
|
return nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
|
||||||
|
this.nozzleDiameterMm = nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLayerHeightMm() {
|
||||||
|
return layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayerHeightMm(BigDecimal layerHeightMm) {
|
||||||
|
this.layerHeightMm = layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getInfillPercent() {
|
||||||
|
return infillPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfillPercent(Integer infillPercent) {
|
||||||
|
this.infillPercent = infillPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInfillPattern() {
|
||||||
|
return infillPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfillPattern(String infillPattern) {
|
||||||
|
this.infillPattern = infillPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getSupportsEnabled() {
|
||||||
|
return supportsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSupportsEnabled(Boolean supportsEnabled) {
|
||||||
|
this.supportsEnabled = supportsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public FilamentVariant getFilamentVariant() {
|
public FilamentVariant getFilamentVariant() {
|
||||||
return filamentVariant;
|
return filamentVariant;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,27 @@ public class QuoteLineItem {
|
|||||||
@com.fasterxml.jackson.annotation.JsonIgnore
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
private FilamentVariant filamentVariant;
|
private FilamentVariant filamentVariant;
|
||||||
|
|
||||||
|
@Column(name = "material_code", length = Integer.MAX_VALUE)
|
||||||
|
private String materialCode;
|
||||||
|
|
||||||
|
@Column(name = "quality", length = Integer.MAX_VALUE)
|
||||||
|
private String quality;
|
||||||
|
|
||||||
|
@Column(name = "nozzle_diameter_mm", precision = 4, scale = 2)
|
||||||
|
private BigDecimal nozzleDiameterMm;
|
||||||
|
|
||||||
|
@Column(name = "layer_height_mm", precision = 5, scale = 3)
|
||||||
|
private BigDecimal layerHeightMm;
|
||||||
|
|
||||||
|
@Column(name = "infill_percent")
|
||||||
|
private Integer infillPercent;
|
||||||
|
|
||||||
|
@Column(name = "infill_pattern", length = Integer.MAX_VALUE)
|
||||||
|
private String infillPattern;
|
||||||
|
|
||||||
|
@Column(name = "supports_enabled")
|
||||||
|
private Boolean supportsEnabled;
|
||||||
|
|
||||||
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
|
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
|
||||||
private BigDecimal boundingBoxXMm;
|
private BigDecimal boundingBoxXMm;
|
||||||
|
|
||||||
@@ -137,6 +158,62 @@ public class QuoteLineItem {
|
|||||||
this.filamentVariant = filamentVariant;
|
this.filamentVariant = filamentVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getMaterialCode() {
|
||||||
|
return materialCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaterialCode(String materialCode) {
|
||||||
|
this.materialCode = materialCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQuality() {
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuality(String quality) {
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getNozzleDiameterMm() {
|
||||||
|
return nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
|
||||||
|
this.nozzleDiameterMm = nozzleDiameterMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLayerHeightMm() {
|
||||||
|
return layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayerHeightMm(BigDecimal layerHeightMm) {
|
||||||
|
this.layerHeightMm = layerHeightMm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getInfillPercent() {
|
||||||
|
return infillPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfillPercent(Integer infillPercent) {
|
||||||
|
this.infillPercent = infillPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInfillPattern() {
|
||||||
|
return infillPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfillPattern(String infillPattern) {
|
||||||
|
this.infillPattern = infillPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getSupportsEnabled() {
|
||||||
|
return supportsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSupportsEnabled(Boolean supportsEnabled) {
|
||||||
|
this.supportsEnabled = supportsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getBoundingBoxXMm() {
|
public BigDecimal getBoundingBoxXMm() {
|
||||||
return boundingBoxXMm;
|
return boundingBoxXMm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ public class OrderService {
|
|||||||
} else {
|
} else {
|
||||||
oItem.setMaterialCode(session.getMaterialCode());
|
oItem.setMaterialCode(session.getMaterialCode());
|
||||||
}
|
}
|
||||||
|
oItem.setQuality(qItem.getQuality());
|
||||||
|
oItem.setNozzleDiameterMm(qItem.getNozzleDiameterMm());
|
||||||
|
oItem.setLayerHeightMm(qItem.getLayerHeightMm());
|
||||||
|
oItem.setInfillPercent(qItem.getInfillPercent());
|
||||||
|
oItem.setInfillPattern(qItem.getInfillPattern());
|
||||||
|
oItem.setSupportsEnabled(qItem.getSupportsEnabled());
|
||||||
|
|
||||||
BigDecimal distributedUnitPrice = qItem.getUnitPriceChf() != null ? qItem.getUnitPriceChf() : BigDecimal.ZERO;
|
BigDecimal distributedUnitPrice = qItem.getUnitPriceChf() != null ? qItem.getUnitPriceChf() : BigDecimal.ZERO;
|
||||||
if (totals.totalPrintSeconds().compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
|
if (totals.totalPrintSeconds().compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
|
||||||
|
|||||||
@@ -192,13 +192,18 @@
|
|||||||
<strong>{{ item.originalFilename }}</strong>
|
<strong>{{ item.originalFilename }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="item-meta">
|
<p class="item-meta">
|
||||||
Qta: {{ item.quantity }} | Colore:
|
Qta: {{ item.quantity }} | Materiale:
|
||||||
|
{{ item.materialCode || "-" }} | Colore:
|
||||||
<span
|
<span
|
||||||
class="color-swatch"
|
class="color-swatch"
|
||||||
*ngIf="isHexColor(item.colorCode)"
|
*ngIf="isHexColor(item.colorCode)"
|
||||||
[style.background-color]="item.colorCode"
|
[style.background-color]="item.colorCode"
|
||||||
></span>
|
></span>
|
||||||
<span>{{ item.colorCode || "-" }}</span>
|
<span>{{ item.colorCode || "-" }}</span>
|
||||||
|
| Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer:
|
||||||
|
{{ item.layerHeightMm ?? "-" }} mm | Infill:
|
||||||
|
{{ item.infillPercent ?? "-" }}% | Supporti:
|
||||||
|
{{ item.supportsEnabled ? "Sì" : "No" }}
|
||||||
| Riga:
|
| Riga:
|
||||||
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
||||||
</p>
|
</p>
|
||||||
@@ -273,17 +278,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Colori file</h4>
|
<h4>Parametri per file</h4>
|
||||||
<div class="file-color-list">
|
<div class="file-color-list">
|
||||||
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
||||||
<span class="filename">{{ item.originalFilename }}</span>
|
<span class="filename">{{ item.originalFilename }}</span>
|
||||||
<span class="file-color">
|
<span class="file-color">
|
||||||
<span
|
{{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm
|
||||||
class="color-swatch"
|
| {{ item.layerHeightMm ?? "-" }} mm | {{ item.infillPercent ?? "-" }}%
|
||||||
*ngIf="isHexColor(item.colorCode)"
|
| {{ item.infillPattern || "-" }} |
|
||||||
[style.background-color]="item.colorCode"
|
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
|
||||||
></span>
|
|
||||||
{{ item.colorCode || "-" }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
<th>Qta</th>
|
<th>Qta</th>
|
||||||
<th>Tempo</th>
|
<th>Tempo</th>
|
||||||
<th>Materiale</th>
|
<th>Materiale</th>
|
||||||
|
<th>Scelte utente</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th>Prezzo unit.</th>
|
<th>Prezzo unit.</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -142,6 +143,14 @@
|
|||||||
: "-"
|
: "-"
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.materialCode || "-" }} |
|
||||||
|
{{ item.nozzleDiameterMm ?? "-" }} mm |
|
||||||
|
{{ item.layerHeightMm ?? "-" }} mm |
|
||||||
|
{{ item.infillPercent ?? "-" }}% |
|
||||||
|
{{ item.infillPattern || "-" }} |
|
||||||
|
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
|
||||||
|
</td>
|
||||||
<td>{{ item.status }}</td>
|
<td>{{ item.status }}</td>
|
||||||
<td>{{ item.unitPriceChf | currency: "CHF" }}</td>
|
<td>{{ item.unitPriceChf | currency: "CHF" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -127,7 +127,15 @@ export interface AdminQuoteSessionDetailItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
printTimeSeconds?: number;
|
printTimeSeconds?: number;
|
||||||
materialGrams?: number;
|
materialGrams?: number;
|
||||||
|
materialCode?: string;
|
||||||
|
quality?: string;
|
||||||
|
nozzleDiameterMm?: number;
|
||||||
|
layerHeightMm?: number;
|
||||||
|
infillPercent?: number;
|
||||||
|
infillPattern?: string;
|
||||||
|
supportsEnabled?: boolean;
|
||||||
colorCode?: string;
|
colorCode?: string;
|
||||||
|
filamentVariantId?: number;
|
||||||
status: string;
|
status: string;
|
||||||
unitPriceChf: number;
|
unitPriceChf: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ export interface AdminOrderItem {
|
|||||||
originalFilename: string;
|
originalFilename: string;
|
||||||
materialCode: string;
|
materialCode: string;
|
||||||
colorCode: string;
|
colorCode: string;
|
||||||
|
quality?: string;
|
||||||
|
nozzleDiameterMm?: number;
|
||||||
|
layerHeightMm?: number;
|
||||||
|
infillPercent?: number;
|
||||||
|
infillPattern?: string;
|
||||||
|
supportsEnabled?: boolean;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
printTimeSeconds: number;
|
printTimeSeconds: number;
|
||||||
materialGrams: number;
|
materialGrams: number;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
(submitRequest)="onCalculate($event)"
|
(submitRequest)="onCalculate($event)"
|
||||||
(itemQuantityChange)="onUploadItemQuantityChange($event)"
|
(itemQuantityChange)="onUploadItemQuantityChange($event)"
|
||||||
(printSettingsChange)="onUploadPrintSettingsChange($event)"
|
(printSettingsChange)="onUploadPrintSettingsChange($event)"
|
||||||
|
(itemSettingsDiffChange)="onItemSettingsDiffChange($event)"
|
||||||
></app-upload-form>
|
></app-upload-form>
|
||||||
</app-card>
|
</app-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
<app-quote-result
|
<app-quote-result
|
||||||
[result]="result()!"
|
[result]="result()!"
|
||||||
[recalculationRequired]="requiresRecalculation()"
|
[recalculationRequired]="requiresRecalculation()"
|
||||||
|
[itemSettingsDiffByFileName]="itemSettingsDiffByFileName()"
|
||||||
(consult)="onConsult()"
|
(consult)="onConsult()"
|
||||||
(proceed)="onProceed()"
|
(proceed)="onProceed()"
|
||||||
(itemQuantityPreviewChange)="onQuoteItemQuantityPreviewChange($event)"
|
(itemQuantityPreviewChange)="onQuoteItemQuantityPreviewChange($event)"
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ import { SuccessStateComponent } from '../../shared/components/success-state/suc
|
|||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { LanguageService } from '../../core/services/language.service';
|
import { LanguageService } from '../../core/services/language.service';
|
||||||
|
|
||||||
|
type TrackedPrintSettings = {
|
||||||
|
mode: 'easy' | 'advanced';
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calculator-page',
|
selector: 'app-calculator-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -57,16 +68,11 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
|
|
||||||
orderSuccess = signal(false);
|
orderSuccess = signal(false);
|
||||||
requiresRecalculation = signal(false);
|
requiresRecalculation = signal(false);
|
||||||
private baselinePrintSettings: {
|
itemSettingsDiffByFileName = signal<
|
||||||
mode: 'easy' | 'advanced';
|
Record<string, { differences: string[] }>
|
||||||
material: string;
|
>({});
|
||||||
quality: string;
|
private baselinePrintSettings: TrackedPrintSettings | null = null;
|
||||||
nozzleDiameter: number;
|
private baselineItemSettingsByFileName = new Map<string, TrackedPrintSettings>();
|
||||||
layerHeight: number;
|
|
||||||
infillDensity: number;
|
|
||||||
infillPattern: string;
|
|
||||||
supportEnabled: boolean;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
||||||
@ViewChild('resultCol') resultCol!: ElementRef;
|
@ViewChild('resultCol') resultCol!: ElementRef;
|
||||||
@@ -115,7 +121,12 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.baselinePrintSettings = this.toTrackedSettingsFromSession(
|
this.baselinePrintSettings = this.toTrackedSettingsFromSession(
|
||||||
data.session,
|
data.session,
|
||||||
);
|
);
|
||||||
|
this.baselineItemSettingsByFileName = this.buildBaselineMapFromSession(
|
||||||
|
data.items || [],
|
||||||
|
this.baselinePrintSettings,
|
||||||
|
);
|
||||||
this.requiresRecalculation.set(false);
|
this.requiresRecalculation.set(false);
|
||||||
|
this.itemSettingsDiffByFileName.set({});
|
||||||
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
|
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
|
||||||
this.cadSessionLocked.set(isCadSession);
|
this.cadSessionLocked.set(isCadSession);
|
||||||
this.step.set('quote');
|
this.step.set('quote');
|
||||||
@@ -188,14 +199,21 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
this.uploadForm.patchSettings(session);
|
this.uploadForm.patchSettings(session);
|
||||||
|
|
||||||
// Also restore colors?
|
|
||||||
// setFiles inits with 'Black'. We need to update them if they differ.
|
|
||||||
// items has colorCode.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.uploadForm) {
|
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
// Assuming index matches.
|
const tracked = this.toTrackedSettingsFromSessionItem(
|
||||||
// Need to be careful if items order changed, but usually ID sort or insert order.
|
item,
|
||||||
|
this.toTrackedSettingsFromSession(session),
|
||||||
|
);
|
||||||
|
this.uploadForm.setItemPrintSettingsByIndex(index, {
|
||||||
|
material: tracked.material.toUpperCase(),
|
||||||
|
quality: tracked.quality,
|
||||||
|
nozzleDiameter: tracked.nozzleDiameter,
|
||||||
|
layerHeight: tracked.layerHeight,
|
||||||
|
infillDensity: tracked.infillDensity,
|
||||||
|
infillPattern: tracked.infillPattern,
|
||||||
|
supportEnabled: tracked.supportEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
if (item.colorCode) {
|
if (item.colorCode) {
|
||||||
this.uploadForm.updateItemColor(index, {
|
this.uploadForm.updateItemColor(index, {
|
||||||
colorName: item.colorCode,
|
colorName: item.colorCode,
|
||||||
@@ -203,8 +221,11 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selected = this.uploadForm.selectedFile();
|
||||||
|
if (selected) {
|
||||||
|
this.uploadForm.selectFile(selected);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
@@ -254,7 +275,10 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.errorKey.set('CALC.ERROR_GENERIC');
|
this.errorKey.set('CALC.ERROR_GENERIC');
|
||||||
this.result.set(res);
|
this.result.set(res);
|
||||||
this.baselinePrintSettings = this.toTrackedSettingsFromRequest(req);
|
this.baselinePrintSettings = this.toTrackedSettingsFromRequest(req);
|
||||||
|
this.baselineItemSettingsByFileName =
|
||||||
|
this.buildBaselineMapFromRequest(req);
|
||||||
this.requiresRecalculation.set(false);
|
this.requiresRecalculation.set(false);
|
||||||
|
this.itemSettingsDiffByFileName.set({});
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.uploadProgress.set(100);
|
this.uploadProgress.set(100);
|
||||||
this.step.set('quote');
|
this.step.set('quote');
|
||||||
@@ -395,7 +419,12 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.step.set('upload');
|
this.step.set('upload');
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
this.requiresRecalculation.set(false);
|
this.requiresRecalculation.set(false);
|
||||||
|
this.itemSettingsDiffByFileName.set({});
|
||||||
this.baselinePrintSettings = null;
|
this.baselinePrintSettings = null;
|
||||||
|
this.baselineItemSettingsByFileName = new Map<
|
||||||
|
string,
|
||||||
|
TrackedPrintSettings
|
||||||
|
>();
|
||||||
this.cadSessionLocked.set(false);
|
this.cadSessionLocked.set(false);
|
||||||
this.orderSuccess.set(false);
|
this.orderSuccess.set(false);
|
||||||
this.switchMode('easy'); // Reset to default and sync URL
|
this.switchMode('easy'); // Reset to default and sync URL
|
||||||
@@ -403,21 +432,16 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
|
|
||||||
private currentRequest: QuoteRequest | null = null;
|
private currentRequest: QuoteRequest | null = null;
|
||||||
|
|
||||||
onUploadPrintSettingsChange(settings: {
|
onUploadPrintSettingsChange(_: TrackedPrintSettings) {
|
||||||
mode: 'easy' | 'advanced';
|
void _;
|
||||||
material: string;
|
|
||||||
quality: string;
|
|
||||||
nozzleDiameter: number;
|
|
||||||
layerHeight: number;
|
|
||||||
infillDensity: number;
|
|
||||||
infillPattern: string;
|
|
||||||
supportEnabled: boolean;
|
|
||||||
}) {
|
|
||||||
if (!this.result()) return;
|
if (!this.result()) return;
|
||||||
if (!this.baselinePrintSettings) return;
|
this.refreshRecalculationRequirement();
|
||||||
this.requiresRecalculation.set(
|
}
|
||||||
!this.sameTrackedSettings(this.baselinePrintSettings, settings),
|
|
||||||
);
|
onItemSettingsDiffChange(
|
||||||
|
diffByFileName: Record<string, { differences: string[] }>,
|
||||||
|
) {
|
||||||
|
this.itemSettingsDiffByFileName.set(diffByFileName || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onConsult() {
|
onConsult() {
|
||||||
@@ -478,7 +502,12 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.error.set(true);
|
this.error.set(true);
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
this.requiresRecalculation.set(false);
|
this.requiresRecalculation.set(false);
|
||||||
|
this.itemSettingsDiffByFileName.set({});
|
||||||
this.baselinePrintSettings = null;
|
this.baselinePrintSettings = null;
|
||||||
|
this.baselineItemSettingsByFileName = new Map<
|
||||||
|
string,
|
||||||
|
TrackedPrintSettings
|
||||||
|
>();
|
||||||
}
|
}
|
||||||
|
|
||||||
switchMode(nextMode: 'easy' | 'advanced'): void {
|
switchMode(nextMode: 'easy' | 'advanced'): void {
|
||||||
@@ -499,16 +528,7 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private toTrackedSettingsFromRequest(req: QuoteRequest): {
|
private toTrackedSettingsFromRequest(req: QuoteRequest): TrackedPrintSettings {
|
||||||
mode: 'easy' | 'advanced';
|
|
||||||
material: string;
|
|
||||||
quality: string;
|
|
||||||
nozzleDiameter: number;
|
|
||||||
layerHeight: number;
|
|
||||||
infillDensity: number;
|
|
||||||
infillPattern: string;
|
|
||||||
supportEnabled: boolean;
|
|
||||||
} {
|
|
||||||
return {
|
return {
|
||||||
mode: req.mode,
|
mode: req.mode,
|
||||||
material: this.normalizeString(req.material || 'PLA'),
|
material: this.normalizeString(req.material || 'PLA'),
|
||||||
@@ -521,16 +541,37 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private toTrackedSettingsFromSession(session: any): {
|
private toTrackedSettingsFromItem(
|
||||||
mode: 'easy' | 'advanced';
|
req: QuoteRequest,
|
||||||
material: string;
|
item: QuoteRequest['items'][number],
|
||||||
quality: string;
|
): TrackedPrintSettings {
|
||||||
nozzleDiameter: number;
|
return {
|
||||||
layerHeight: number;
|
mode: req.mode,
|
||||||
infillDensity: number;
|
material: this.normalizeString(item.material || req.material || 'PLA'),
|
||||||
infillPattern: string;
|
quality: this.normalizeString(item.quality || req.quality || 'standard'),
|
||||||
supportEnabled: boolean;
|
nozzleDiameter: this.normalizeNumber(
|
||||||
} {
|
item.nozzleDiameter ?? req.nozzleDiameter,
|
||||||
|
0.4,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
layerHeight: this.normalizeNumber(
|
||||||
|
item.layerHeight ?? req.layerHeight,
|
||||||
|
0.2,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
infillDensity: this.normalizeNumber(
|
||||||
|
item.infillDensity ?? req.infillDensity,
|
||||||
|
20,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
infillPattern: this.normalizeString(
|
||||||
|
item.infillPattern || req.infillPattern || 'grid',
|
||||||
|
),
|
||||||
|
supportEnabled: Boolean(item.supportEnabled ?? req.supportEnabled),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toTrackedSettingsFromSession(session: any): TrackedPrintSettings {
|
||||||
const layer = this.normalizeNumber(session?.layerHeightMm, 0.2, 3);
|
const layer = this.normalizeNumber(session?.layerHeightMm, 0.2, 3);
|
||||||
return {
|
return {
|
||||||
mode: this.mode(),
|
mode: this.mode(),
|
||||||
@@ -545,27 +586,111 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toTrackedSettingsFromSessionItem(
|
||||||
|
item: any,
|
||||||
|
fallback: TrackedPrintSettings,
|
||||||
|
): TrackedPrintSettings {
|
||||||
|
const layer = this.normalizeNumber(item?.layerHeightMm, fallback.layerHeight, 3);
|
||||||
|
return {
|
||||||
|
mode: this.mode(),
|
||||||
|
material: this.normalizeString(item?.materialCode || fallback.material),
|
||||||
|
quality: this.normalizeString(
|
||||||
|
item?.quality ||
|
||||||
|
(layer >= 0.24
|
||||||
|
? 'draft'
|
||||||
|
: layer <= 0.12
|
||||||
|
? 'extra_fine'
|
||||||
|
: 'standard'),
|
||||||
|
),
|
||||||
|
nozzleDiameter: this.normalizeNumber(
|
||||||
|
item?.nozzleDiameterMm,
|
||||||
|
fallback.nozzleDiameter,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
layerHeight: layer,
|
||||||
|
infillDensity: this.normalizeNumber(
|
||||||
|
item?.infillPercent,
|
||||||
|
fallback.infillDensity,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
infillPattern: this.normalizeString(
|
||||||
|
item?.infillPattern || fallback.infillPattern,
|
||||||
|
),
|
||||||
|
supportEnabled: Boolean(
|
||||||
|
item?.supportsEnabled ?? fallback.supportEnabled,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBaselineMapFromRequest(
|
||||||
|
req: QuoteRequest,
|
||||||
|
): Map<string, TrackedPrintSettings> {
|
||||||
|
const map = new Map<string, TrackedPrintSettings>();
|
||||||
|
req.items.forEach((item) => {
|
||||||
|
map.set(
|
||||||
|
this.normalizeFileName(item.file?.name || ''),
|
||||||
|
this.toTrackedSettingsFromItem(req, item),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBaselineMapFromSession(
|
||||||
|
items: any[],
|
||||||
|
defaultSettings: TrackedPrintSettings | null,
|
||||||
|
): Map<string, TrackedPrintSettings> {
|
||||||
|
const map = new Map<string, TrackedPrintSettings>();
|
||||||
|
const fallback = defaultSettings ?? this.defaultTrackedSettings();
|
||||||
|
items.forEach((item) => {
|
||||||
|
map.set(
|
||||||
|
this.normalizeFileName(item?.originalFilename || ''),
|
||||||
|
this.toTrackedSettingsFromSessionItem(item, fallback),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private defaultTrackedSettings(): TrackedPrintSettings {
|
||||||
|
return {
|
||||||
|
mode: this.mode(),
|
||||||
|
material: 'pla',
|
||||||
|
quality: 'standard',
|
||||||
|
nozzleDiameter: 0.4,
|
||||||
|
layerHeight: 0.2,
|
||||||
|
infillDensity: 20,
|
||||||
|
infillPattern: 'grid',
|
||||||
|
supportEnabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshRecalculationRequirement(): void {
|
||||||
|
if (!this.result()) return;
|
||||||
|
|
||||||
|
const draft = this.uploadForm?.getCurrentRequestDraft();
|
||||||
|
if (!draft || draft.items.length === 0) {
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = this.baselinePrintSettings;
|
||||||
|
if (!fallback) {
|
||||||
|
this.requiresRecalculation.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = draft.items.some((item) => {
|
||||||
|
const key = this.normalizeFileName(item.file?.name || '');
|
||||||
|
const baseline = this.baselineItemSettingsByFileName.get(key) || fallback;
|
||||||
|
const current = this.toTrackedSettingsFromItem(draft, item);
|
||||||
|
return !this.sameTrackedSettings(baseline, current);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.requiresRecalculation.set(changed);
|
||||||
|
}
|
||||||
|
|
||||||
private sameTrackedSettings(
|
private sameTrackedSettings(
|
||||||
a: {
|
a: TrackedPrintSettings,
|
||||||
mode: 'easy' | 'advanced';
|
b: TrackedPrintSettings,
|
||||||
material: string;
|
|
||||||
quality: string;
|
|
||||||
nozzleDiameter: number;
|
|
||||||
layerHeight: number;
|
|
||||||
infillDensity: number;
|
|
||||||
infillPattern: string;
|
|
||||||
supportEnabled: boolean;
|
|
||||||
},
|
|
||||||
b: {
|
|
||||||
mode: 'easy' | 'advanced';
|
|
||||||
material: string;
|
|
||||||
quality: string;
|
|
||||||
nozzleDiameter: number;
|
|
||||||
layerHeight: number;
|
|
||||||
infillDensity: number;
|
|
||||||
infillPattern: string;
|
|
||||||
supportEnabled: boolean;
|
|
||||||
},
|
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
a.mode === b.mode &&
|
a.mode === b.mode &&
|
||||||
@@ -583,6 +708,10 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeFileName(fileName: string): string {
|
||||||
|
return (fileName || '').split(/[\\/]/).pop()?.trim().toLowerCase() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeString(value: string): string {
|
private normalizeString(value: string): string {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -63,6 +63,12 @@
|
|||||||
<span class="file-details">
|
<span class="file-details">
|
||||||
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
|
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
|
||||||
{{ item.unitWeight | number: "1.0-0" }}g
|
{{ item.unitWeight | number: "1.0-0" }}g
|
||||||
|
@if (getItemDifferenceLabel(item.fileName)) {
|
||||||
|
|
|
||||||
|
<small class="item-settings-diff">
|
||||||
|
{{ getItemDifferenceLabel(item.fileName) }}
|
||||||
|
</small>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,14 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-settings-diff {
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8a6d1f;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
.file-details {
|
.file-details {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export class QuoteResultComponent implements OnDestroy {
|
|||||||
|
|
||||||
result = input.required<QuoteResult>();
|
result = input.required<QuoteResult>();
|
||||||
recalculationRequired = input<boolean>(false);
|
recalculationRequired = input<boolean>(false);
|
||||||
|
itemSettingsDiffByFileName = input<Record<string, { differences: string[] }>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
consult = output<void>();
|
consult = output<void>();
|
||||||
proceed = output<void>();
|
proceed = output<void>();
|
||||||
itemChange = output<{
|
itemChange = output<{
|
||||||
@@ -185,4 +188,15 @@ export class QuoteResultComponent implements OnDestroy {
|
|||||||
this.quantityTimers.forEach((timer) => clearTimeout(timer));
|
this.quantityTimers.forEach((timer) => clearTimeout(timer));
|
||||||
this.quantityTimers.clear();
|
this.quantityTimers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItemDifferenceLabel(fileName: string): string {
|
||||||
|
const differences =
|
||||||
|
this.itemSettingsDiffByFileName()[fileName]?.differences || [];
|
||||||
|
if (differences.length === 0) return '';
|
||||||
|
|
||||||
|
const materialOnly = differences.find(
|
||||||
|
(entry) => !entry.includes(':') && entry.trim().length > 0,
|
||||||
|
);
|
||||||
|
return materialOnly || differences.join(' | ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<app-color-selector
|
<app-color-selector
|
||||||
[selectedColor]="item.color"
|
[selectedColor]="item.color"
|
||||||
[selectedVariantId]="item.filamentVariantId ?? null"
|
[selectedVariantId]="item.filamentVariantId ?? null"
|
||||||
[variants]="currentMaterialVariants()"
|
[variants]="getVariantsForItem(item)"
|
||||||
(colorSelected)="updateItemColor(i, $event)"
|
(colorSelected)="updateItemColor(i, $event)"
|
||||||
>
|
>
|
||||||
</app-color-selector>
|
</app-color-selector>
|
||||||
@@ -145,6 +145,15 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (items().length > 1) {
|
||||||
|
<div class="checkbox-row sync-all-row">
|
||||||
|
<input type="checkbox" formControlName="syncAllItems" id="syncAllItems" />
|
||||||
|
<label for="syncAllItems">
|
||||||
|
Uguale per tutti i pezzi
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Global quantity removed, now per item -->
|
<!-- Global quantity removed, now per item -->
|
||||||
|
|
||||||
@if (mode() === "advanced") {
|
@if (mode() === "advanced") {
|
||||||
|
|||||||
@@ -211,6 +211,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-all-row {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Progress Bar */
|
/* Progress Bar */
|
||||||
.progress-container {
|
.progress-container {
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
|
|||||||
@@ -37,8 +37,25 @@ interface FormItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
color: string;
|
color: string;
|
||||||
filamentVariantId?: number;
|
filamentVariantId?: number;
|
||||||
|
printSettings: ItemPrintSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ItemPrintSettings {
|
||||||
|
material: string;
|
||||||
|
quality: string;
|
||||||
|
nozzleDiameter: number;
|
||||||
|
layerHeight: number;
|
||||||
|
infillDensity: number;
|
||||||
|
infillPattern: string;
|
||||||
|
supportEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemSettingsDiffInfo {
|
||||||
|
differences: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemPrintSettingsUpdate = Partial<ItemPrintSettings>;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-upload-form',
|
selector: 'app-upload-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -67,6 +84,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
fileName: string;
|
fileName: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}>();
|
}>();
|
||||||
|
itemSettingsDiffChange = output<Record<string, ItemSettingsDiffInfo>>();
|
||||||
printSettingsChange = output<{
|
printSettingsChange = output<{
|
||||||
mode: 'easy' | 'advanced';
|
mode: 'easy' | 'advanced';
|
||||||
material: string;
|
material: string;
|
||||||
@@ -108,7 +126,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (matCode && this.fullMaterialOptions.length > 0) {
|
if (matCode && this.fullMaterialOptions.length > 0) {
|
||||||
const found = this.fullMaterialOptions.find((m) => m.code === matCode);
|
const found = this.fullMaterialOptions.find((m) => m.code === matCode);
|
||||||
this.currentMaterialVariants.set(found ? found.variants : []);
|
this.currentMaterialVariants.set(found ? found.variants : []);
|
||||||
this.syncItemVariantSelections();
|
this.syncSelectedItemVariantSelection();
|
||||||
} else {
|
} else {
|
||||||
this.currentMaterialVariants.set([]);
|
this.currentMaterialVariants.set([]);
|
||||||
}
|
}
|
||||||
@@ -137,6 +155,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
constructor() {
|
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
|
||||||
|
syncAllItems: [true],
|
||||||
material: ['', Validators.required],
|
material: ['', Validators.required],
|
||||||
quality: ['', Validators.required],
|
quality: ['', Validators.required],
|
||||||
items: [[]], // Track items in form for validation if needed
|
items: [[]], // Track items in form for validation if needed
|
||||||
@@ -164,7 +183,9 @@ export class UploadFormComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
this.form.valueChanges.subscribe(() => {
|
this.form.valueChanges.subscribe(() => {
|
||||||
if (this.isPatchingSettings) return;
|
if (this.isPatchingSettings) return;
|
||||||
|
this.syncSelectedItemSettingsFromForm();
|
||||||
this.emitPrintSettingsChange();
|
this.emitPrintSettingsChange();
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -337,6 +358,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
color: defaultSelection.colorName,
|
color: defaultSelection.colorName,
|
||||||
filamentVariantId: defaultSelection.filamentVariantId,
|
filamentVariantId: defaultSelection.filamentVariantId,
|
||||||
|
printSettings: this.getCurrentItemPrintSettings(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +371,8 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.items.update((current) => [...current, ...validItems]);
|
this.items.update((current) => [...current, ...validItems]);
|
||||||
this.form.get('itemsTouched')?.setValue(true);
|
this.form.get('itemsTouched')?.setValue(true);
|
||||||
// Auto select last added
|
// Auto select last added
|
||||||
this.selectedFile.set(validItems[validItems.length - 1].file);
|
this.selectFile(validItems[validItems.length - 1].file);
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,6 +419,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.selectedFile.set(file);
|
this.selectedFile.set(file);
|
||||||
}
|
}
|
||||||
|
this.loadSelectedItemSettingsIntoForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get color of currently selected file
|
// Helper to get color of currently selected file
|
||||||
@@ -451,17 +475,55 @@ export class UploadFormComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemPrintSettingsByIndex(index: number, update: ItemPrintSettingsUpdate) {
|
||||||
|
if (!Number.isInteger(index) || index < 0) return;
|
||||||
|
|
||||||
|
let selectedItemUpdated = false;
|
||||||
|
this.items.update((current) => {
|
||||||
|
if (index >= current.length) return current;
|
||||||
|
const updated = [...current];
|
||||||
|
const target = updated[index];
|
||||||
|
if (!target) return current;
|
||||||
|
|
||||||
|
const merged: ItemPrintSettings = {
|
||||||
|
...target.printSettings,
|
||||||
|
...update,
|
||||||
|
};
|
||||||
|
|
||||||
|
updated[index] = {
|
||||||
|
...target,
|
||||||
|
printSettings: merged,
|
||||||
|
};
|
||||||
|
selectedItemUpdated = target.file === this.selectedFile();
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedItemUpdated) {
|
||||||
|
this.loadSelectedItemSettingsIntoForm();
|
||||||
|
this.emitPrintSettingsChange();
|
||||||
|
}
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem(index: number) {
|
removeItem(index: number) {
|
||||||
|
let nextSelected: File | null = null;
|
||||||
this.items.update((current) => {
|
this.items.update((current) => {
|
||||||
const updated = [...current];
|
const updated = [...current];
|
||||||
const removed = updated.splice(index, 1)[0];
|
const removed = updated.splice(index, 1)[0];
|
||||||
if (this.selectedFile() === removed.file) {
|
if (this.selectedFile() === removed.file) {
|
||||||
this.selectedFile.set(null);
|
nextSelected = updated.length > 0 ? updated[Math.max(0, index - 1)].file : null;
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
if (nextSelected) {
|
||||||
|
this.selectFile(nextSelected);
|
||||||
|
} else if (this.items().length === 0) {
|
||||||
|
this.selectedFile.set(null);
|
||||||
|
}
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles(files: File[]) {
|
setFiles(files: File[]) {
|
||||||
@@ -474,6 +536,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
color: defaultSelection.colorName,
|
color: defaultSelection.colorName,
|
||||||
filamentVariantId: defaultSelection.filamentVariantId,
|
filamentVariantId: defaultSelection.filamentVariantId,
|
||||||
|
printSettings: this.getCurrentItemPrintSettings(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +544,8 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.items.set(validItems);
|
this.items.set(validItems);
|
||||||
this.form.get('itemsTouched')?.setValue(true);
|
this.form.get('itemsTouched')?.setValue(true);
|
||||||
// Auto select last added
|
// Auto select last added
|
||||||
this.selectedFile.set(validItems[validItems.length - 1].file);
|
this.selectFile(validItems[validItems.length - 1].file);
|
||||||
|
this.emitItemSettingsDiffChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,25 +574,48 @@ export class UploadFormComponent implements OnInit {
|
|||||||
return { colorName: 'Black' };
|
return { colorName: 'Black' };
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncItemVariantSelections(): void {
|
getVariantsForItem(item: FormItem): VariantOption[] {
|
||||||
|
return this.getVariantsForMaterialCode(item.printSettings.material);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVariantsForMaterialCode(materialCodeRaw: string): VariantOption[] {
|
||||||
|
const materialCode = String(materialCodeRaw || '').toUpperCase();
|
||||||
|
if (!materialCode) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const material = this.fullMaterialOptions.find(
|
||||||
|
(option) => String(option.code || '').toUpperCase() === materialCode,
|
||||||
|
);
|
||||||
|
return material?.variants || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncSelectedItemVariantSelection(): void {
|
||||||
const vars = this.currentMaterialVariants();
|
const vars = this.currentMaterialVariants();
|
||||||
if (!vars || vars.length === 0) {
|
if (!vars || vars.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selected = this.selectedFile();
|
||||||
|
if (!selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fallback = vars.find((v) => !v.isOutOfStock) || vars[0];
|
const fallback = vars.find((v) => !v.isOutOfStock) || vars[0];
|
||||||
this.items.update((current) =>
|
this.items.update((current) =>
|
||||||
current.map((item) => {
|
current.map((item) => {
|
||||||
|
if (item.file !== selected) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
const byId =
|
const byId =
|
||||||
item.filamentVariantId != null
|
item.filamentVariantId != null
|
||||||
? vars.find((v) => v.id === item.filamentVariantId)
|
? vars.find((v) => v.id === item.filamentVariantId)
|
||||||
: null;
|
: null;
|
||||||
const byColor = vars.find((v) => v.colorName === item.color);
|
const byColor = vars.find((v) => v.colorName === item.color);
|
||||||
const selected = byId || byColor || fallback;
|
const selectedVariant = byId || byColor || fallback;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
color: selected.colorName,
|
color: selectedVariant.colorName,
|
||||||
filamentVariantId: selected.id,
|
filamentVariantId: selectedVariant.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -592,7 +679,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
this.submitRequest.emit({
|
this.submitRequest.emit({
|
||||||
...this.form.getRawValue(),
|
...this.form.getRawValue(),
|
||||||
items: this.items(), // Pass the items array explicitly AFTER form value to prevent overwrite
|
items: this.toQuoteRequestItems(), // Include per-item print settings overrides
|
||||||
mode: this.mode(),
|
mode: this.mode(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -666,7 +753,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
if (this.items().length === 0) return null;
|
if (this.items().length === 0) return null;
|
||||||
const raw = this.form.getRawValue();
|
const raw = this.form.getRawValue();
|
||||||
return {
|
return {
|
||||||
items: this.items(),
|
items: this.toQuoteRequestItems(),
|
||||||
material: raw.material,
|
material: raw.material,
|
||||||
quality: raw.quality,
|
quality: raw.quality,
|
||||||
notes: raw.notes,
|
notes: raw.notes,
|
||||||
@@ -706,8 +793,248 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.printSettingsChange.emit(this.getCurrentPrintSettings());
|
this.printSettingsChange.emit(this.getCurrentPrintSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadSelectedItemSettingsIntoForm(): void {
|
||||||
|
const selected = this.selectedFile();
|
||||||
|
if (!selected) return;
|
||||||
|
const item = this.items().find((current) => current.file === selected);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
this.isPatchingSettings = true;
|
||||||
|
this.form.patchValue(
|
||||||
|
{
|
||||||
|
material: item.printSettings.material,
|
||||||
|
quality: item.printSettings.quality,
|
||||||
|
nozzleDiameter: item.printSettings.nozzleDiameter,
|
||||||
|
layerHeight: item.printSettings.layerHeight,
|
||||||
|
infillDensity: item.printSettings.infillDensity,
|
||||||
|
infillPattern: item.printSettings.infillPattern,
|
||||||
|
supportEnabled: item.printSettings.supportEnabled,
|
||||||
|
},
|
||||||
|
{ emitEvent: false },
|
||||||
|
);
|
||||||
|
this.isPatchingSettings = false;
|
||||||
|
this.updateLayerHeightOptionsForNozzle(
|
||||||
|
item.printSettings.nozzleDiameter,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.updateVariants();
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncSelectedItemSettingsFromForm(): void {
|
||||||
|
const currentSettings = this.getCurrentItemPrintSettings();
|
||||||
|
|
||||||
|
if (this.shouldApplySettingsToAllItems()) {
|
||||||
|
this.applyCurrentSettingsToAllItems(currentSettings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this.selectedFile();
|
||||||
|
if (!selected) return;
|
||||||
|
|
||||||
|
this.items.update((current) =>
|
||||||
|
current.map((item) => {
|
||||||
|
if (item.file !== selected) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const variants = this.getVariantsForMaterialCode(currentSettings.material);
|
||||||
|
const fallback = variants.find((v) => !v.isOutOfStock) || variants[0];
|
||||||
|
const byId =
|
||||||
|
item.filamentVariantId != null
|
||||||
|
? variants.find((v) => v.id === item.filamentVariantId)
|
||||||
|
: null;
|
||||||
|
const byColor = variants.find((v) => v.colorName === item.color);
|
||||||
|
const selectedVariant = byId || byColor || fallback;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
printSettings: { ...currentSettings },
|
||||||
|
color: selectedVariant ? selectedVariant.colorName : item.color,
|
||||||
|
filamentVariantId: selectedVariant ? selectedVariant.id : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitItemSettingsDiffChange(): void {
|
||||||
|
const currentItems = this.items();
|
||||||
|
if (currentItems.length === 0) {
|
||||||
|
this.itemSettingsDiffChange.emit({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureCounts = new Map<string, number>();
|
||||||
|
currentItems.forEach((item) => {
|
||||||
|
const signature = this.settingsSignature(item.printSettings);
|
||||||
|
signatureCounts.set(signature, (signatureCounts.get(signature) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dominantSignature = '';
|
||||||
|
let dominantCount = 0;
|
||||||
|
signatureCounts.forEach((count, signature) => {
|
||||||
|
if (count > dominantCount) {
|
||||||
|
dominantCount = count;
|
||||||
|
dominantSignature = signature;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDominant = dominantCount > 1;
|
||||||
|
const dominantSettings = hasDominant
|
||||||
|
? currentItems.find(
|
||||||
|
(item) =>
|
||||||
|
this.settingsSignature(item.printSettings) === dominantSignature,
|
||||||
|
)?.printSettings
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const diffByFileName: Record<string, ItemSettingsDiffInfo> = {};
|
||||||
|
currentItems.forEach((item) => {
|
||||||
|
const differences = dominantSettings
|
||||||
|
? this.describeSettingsDifferences(dominantSettings, item.printSettings)
|
||||||
|
: [];
|
||||||
|
diffByFileName[item.file.name] = {
|
||||||
|
differences,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.itemSettingsDiffChange.emit(diffByFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sameItemPrintSettings(
|
||||||
|
a: ItemPrintSettings,
|
||||||
|
b: ItemPrintSettings,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
a.material.trim().toUpperCase() === b.material.trim().toUpperCase() &&
|
||||||
|
a.quality.trim().toLowerCase() === b.quality.trim().toLowerCase() &&
|
||||||
|
Math.abs(a.nozzleDiameter - b.nozzleDiameter) < 0.0001 &&
|
||||||
|
Math.abs(a.layerHeight - b.layerHeight) < 0.0001 &&
|
||||||
|
Math.abs(a.infillDensity - b.infillDensity) < 0.0001 &&
|
||||||
|
a.infillPattern.trim().toLowerCase() ===
|
||||||
|
b.infillPattern.trim().toLowerCase() &&
|
||||||
|
Boolean(a.supportEnabled) === Boolean(b.supportEnabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private settingsSignature(settings: ItemPrintSettings): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
material: settings.material.trim().toUpperCase(),
|
||||||
|
quality: settings.quality.trim().toLowerCase(),
|
||||||
|
nozzleDiameter: Number(settings.nozzleDiameter.toFixed(2)),
|
||||||
|
layerHeight: Number(settings.layerHeight.toFixed(3)),
|
||||||
|
infillDensity: Number(settings.infillDensity.toFixed(2)),
|
||||||
|
infillPattern: settings.infillPattern.trim().toLowerCase(),
|
||||||
|
supportEnabled: Boolean(settings.supportEnabled),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private describeSettingsDifferences(
|
||||||
|
baseline: ItemPrintSettings,
|
||||||
|
current: ItemPrintSettings,
|
||||||
|
): string[] {
|
||||||
|
if (this.sameItemPrintSettings(baseline, current)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const differences: string[] = [];
|
||||||
|
if (baseline.material.trim().toUpperCase() !== current.material.trim().toUpperCase()) {
|
||||||
|
differences.push(`${current.material}`);
|
||||||
|
}
|
||||||
|
if (baseline.quality.trim().toLowerCase() !== current.quality.trim().toLowerCase()) {
|
||||||
|
differences.push(`Qualita: ${current.quality}`);
|
||||||
|
}
|
||||||
|
if (Math.abs(baseline.nozzleDiameter - current.nozzleDiameter) >= 0.0001) {
|
||||||
|
differences.push(`Nozzle: ${current.nozzleDiameter.toFixed(1)} mm`);
|
||||||
|
}
|
||||||
|
if (Math.abs(baseline.layerHeight - current.layerHeight) >= 0.0001) {
|
||||||
|
differences.push(`Layer: ${current.layerHeight.toFixed(2)} mm`);
|
||||||
|
}
|
||||||
|
if (Math.abs(baseline.infillDensity - current.infillDensity) >= 0.0001) {
|
||||||
|
differences.push(`Infill: ${current.infillDensity}%`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
baseline.infillPattern.trim().toLowerCase() !==
|
||||||
|
current.infillPattern.trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
differences.push(`Pattern: ${current.infillPattern}`);
|
||||||
|
}
|
||||||
|
if (Boolean(baseline.supportEnabled) !== Boolean(current.supportEnabled)) {
|
||||||
|
differences.push(
|
||||||
|
`Supporti: ${current.supportEnabled ? 'attivi' : 'disattivi'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return differences;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toQuoteRequestItems(): QuoteRequest['items'] {
|
||||||
|
return this.items().map((item) => ({
|
||||||
|
file: item.file,
|
||||||
|
quantity: item.quantity,
|
||||||
|
color: item.color,
|
||||||
|
filamentVariantId: item.filamentVariantId,
|
||||||
|
material: item.printSettings.material,
|
||||||
|
quality: item.printSettings.quality,
|
||||||
|
nozzleDiameter: item.printSettings.nozzleDiameter,
|
||||||
|
layerHeight: item.printSettings.layerHeight,
|
||||||
|
infillDensity: item.printSettings.infillDensity,
|
||||||
|
infillPattern: item.printSettings.infillPattern,
|
||||||
|
supportEnabled: item.printSettings.supportEnabled,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCurrentItemPrintSettings(): ItemPrintSettings {
|
||||||
|
const settings = this.getCurrentPrintSettings();
|
||||||
|
return {
|
||||||
|
material: settings.material,
|
||||||
|
quality: settings.quality,
|
||||||
|
nozzleDiameter: settings.nozzleDiameter,
|
||||||
|
layerHeight: settings.layerHeight,
|
||||||
|
infillDensity: settings.infillDensity,
|
||||||
|
infillPattern: settings.infillPattern,
|
||||||
|
supportEnabled: settings.supportEnabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldApplySettingsToAllItems(): boolean {
|
||||||
|
return this.parseBooleanControlValue(this.form.get('syncAllItems')?.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyCurrentSettingsToAllItems(currentSettings: ItemPrintSettings): void {
|
||||||
|
this.items.update((current) =>
|
||||||
|
current.map((item) => {
|
||||||
|
const variants = this.getVariantsForMaterialCode(currentSettings.material);
|
||||||
|
const fallback = variants.find((v) => !v.isOutOfStock) || variants[0];
|
||||||
|
const byId =
|
||||||
|
item.filamentVariantId != null
|
||||||
|
? variants.find((v) => v.id === item.filamentVariantId)
|
||||||
|
: null;
|
||||||
|
const byColor = variants.find((v) => v.colorName === item.color);
|
||||||
|
const selectedVariant = byId || byColor || fallback;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
printSettings: { ...currentSettings },
|
||||||
|
color: selectedVariant ? selectedVariant.colorName : item.color,
|
||||||
|
filamentVariantId: selectedVariant ? selectedVariant.id : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseBooleanControlValue(raw: unknown): boolean {
|
||||||
|
if (this.items().length <= 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (raw === true || raw === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
return normalized === 'true' || normalized === '1' || normalized === 'on';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private applySettingsLock(locked: boolean): void {
|
private applySettingsLock(locked: boolean): void {
|
||||||
const controlsToLock = [
|
const controlsToLock = [
|
||||||
|
'syncAllItems',
|
||||||
'material',
|
'material',
|
||||||
'quality',
|
'quality',
|
||||||
'nozzleDiameter',
|
'nozzleDiameter',
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export interface QuoteRequest {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
filamentVariantId?: number;
|
filamentVariantId?: number;
|
||||||
|
material?: string;
|
||||||
|
quality?: string;
|
||||||
|
nozzleDiameter?: number;
|
||||||
|
layerHeight?: number;
|
||||||
|
infillDensity?: number;
|
||||||
|
infillPattern?: string;
|
||||||
|
supportEnabled?: boolean;
|
||||||
}[];
|
}[];
|
||||||
material: string;
|
material: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
@@ -150,7 +157,7 @@ export class QuoteEstimatorService {
|
|||||||
|
|
||||||
if (normalized === 'draft') {
|
if (normalized === 'draft') {
|
||||||
return {
|
return {
|
||||||
quality: 'extra_fine',
|
quality: 'draft',
|
||||||
layerHeight: 0.24,
|
layerHeight: 0.24,
|
||||||
infillDensity: 12,
|
infillDensity: 12,
|
||||||
infillPattern: 'grid',
|
infillPattern: 'grid',
|
||||||
@@ -306,23 +313,28 @@ export class QuoteEstimatorService {
|
|||||||
request.mode === 'easy'
|
request.mode === 'easy'
|
||||||
? 'ADVANCED'
|
? 'ADVANCED'
|
||||||
: request.mode.toUpperCase(),
|
: request.mode.toUpperCase(),
|
||||||
material: request.material,
|
material: item.material || request.material,
|
||||||
filamentVariantId: item.filamentVariantId,
|
filamentVariantId: item.filamentVariantId,
|
||||||
quality: easyPreset ? easyPreset.quality : request.quality,
|
quality: easyPreset
|
||||||
supportsEnabled: request.supportEnabled,
|
? easyPreset.quality
|
||||||
|
: item.quality || request.quality,
|
||||||
|
supportsEnabled:
|
||||||
|
easyPreset != null
|
||||||
|
? request.supportEnabled
|
||||||
|
: item.supportEnabled ?? request.supportEnabled,
|
||||||
color: item.color || '#FFFFFF',
|
color: item.color || '#FFFFFF',
|
||||||
layerHeight: easyPreset
|
layerHeight: easyPreset
|
||||||
? easyPreset.layerHeight
|
? easyPreset.layerHeight
|
||||||
: request.layerHeight,
|
: item.layerHeight ?? request.layerHeight,
|
||||||
infillDensity: easyPreset
|
infillDensity: easyPreset
|
||||||
? easyPreset.infillDensity
|
? easyPreset.infillDensity
|
||||||
: request.infillDensity,
|
: item.infillDensity ?? request.infillDensity,
|
||||||
infillPattern: easyPreset
|
infillPattern: easyPreset
|
||||||
? easyPreset.infillPattern
|
? easyPreset.infillPattern
|
||||||
: request.infillPattern,
|
: item.infillPattern ?? request.infillPattern,
|
||||||
nozzleDiameter: easyPreset
|
nozzleDiameter: easyPreset
|
||||||
? easyPreset.nozzleDiameter
|
? easyPreset.nozzleDiameter
|
||||||
: request.nozzleDiameter,
|
: item.nozzleDiameter ?? request.nozzleDiameter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settingsBlob = new Blob([JSON.stringify(settings)], {
|
const settingsBlob = new Blob([JSON.stringify(settings)], {
|
||||||
@@ -477,9 +489,7 @@ export class QuoteEstimatorService {
|
|||||||
unitTime: item.printTimeSeconds,
|
unitTime: item.printTimeSeconds,
|
||||||
unitWeight: item.materialGrams,
|
unitWeight: item.materialGrams,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
material: session.materialCode, // Assumption: session has one material for all? or items have it?
|
material: item.materialCode || session.materialCode,
|
||||||
// Backend model QuoteSession has materialCode.
|
|
||||||
// But line items might have different colors.
|
|
||||||
color: item.colorCode,
|
color: item.colorCode,
|
||||||
filamentVariantId: item.filamentVariantId,
|
filamentVariantId: item.filamentVariantId,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -245,6 +245,10 @@
|
|||||||
<span
|
<span
|
||||||
>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span
|
>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span
|
||||||
>
|
>
|
||||||
|
<span>
|
||||||
|
{{ "CHECKOUT.MATERIAL" | translate }}:
|
||||||
|
{{ itemMaterial(item) }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
*ngIf="item.colorCode"
|
*ngIf="item.colorCode"
|
||||||
class="color-dot"
|
class="color-dot"
|
||||||
@@ -255,7 +259,7 @@
|
|||||||
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
|
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
|
||||||
{{ item.materialGrams | number: "1.0-0" }}g
|
{{ item.materialGrams | number: "1.0-0" }}g
|
||||||
</div>
|
</div>
|
||||||
<div class="item-preview" *ngIf="isStlItem(item)">
|
<div class="item-preview" *ngIf="isCadSession() && isStlItem(item)">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="previewFile(item) as itemPreview; else previewState"
|
*ngIf="previewFile(item) as itemPreview; else previewState"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ export class CheckoutComponent implements OnInit {
|
|||||||
this.quoteService.getQuoteSession(this.sessionId).subscribe({
|
this.quoteService.getQuoteSession(this.sessionId).subscribe({
|
||||||
next: (session) => {
|
next: (session) => {
|
||||||
this.quoteSession.set(session);
|
this.quoteSession.set(session);
|
||||||
|
if (this.isCadSessionData(session)) {
|
||||||
this.loadStlPreviews(session);
|
this.loadStlPreviews(session);
|
||||||
|
} else {
|
||||||
|
this.resetPreviewState();
|
||||||
|
}
|
||||||
console.log('Loaded session:', session);
|
console.log('Loaded session:', session);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -173,7 +177,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isCadSession(): boolean {
|
isCadSession(): boolean {
|
||||||
return this.quoteSession()?.session?.status === 'CAD_ACTIVE';
|
return this.isCadSessionData(this.quoteSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
cadRequestId(): string | null {
|
cadRequestId(): string | null {
|
||||||
@@ -188,6 +192,12 @@ export class CheckoutComponent implements OnInit {
|
|||||||
return this.quoteSession()?.cadTotalChf ?? 0;
|
return this.quoteSession()?.cadTotalChf ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemMaterial(item: any): string {
|
||||||
|
return String(
|
||||||
|
item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
isStlItem(item: any): boolean {
|
isStlItem(item: any): boolean {
|
||||||
const name = String(item?.originalFilename ?? '').toLowerCase();
|
const name = String(item?.originalFilename ?? '').toLowerCase();
|
||||||
return name.endsWith('.stl');
|
return name.endsWith('.stl');
|
||||||
@@ -241,7 +251,11 @@ export class CheckoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadStlPreviews(session: any): void {
|
private loadStlPreviews(session: any): void {
|
||||||
if (!this.sessionId || !Array.isArray(session?.items)) {
|
if (
|
||||||
|
!this.sessionId ||
|
||||||
|
!this.isCadSessionData(session) ||
|
||||||
|
!Array.isArray(session?.items)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +290,17 @@ export class CheckoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isCadSessionData(session: any): boolean {
|
||||||
|
return session?.session?.status === 'CAD_ACTIVE';
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetPreviewState(): void {
|
||||||
|
this.previewFiles.set({});
|
||||||
|
this.previewLoading.set({});
|
||||||
|
this.previewErrors.set({});
|
||||||
|
this.closePreview();
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
if (this.checkoutForm.invalid) {
|
if (this.checkoutForm.invalid) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -402,6 +402,7 @@
|
|||||||
"SETUP_FEE": "Einrichtungskosten",
|
"SETUP_FEE": "Einrichtungskosten",
|
||||||
"TOTAL": "Gesamt",
|
"TOTAL": "Gesamt",
|
||||||
"QTY": "Menge",
|
"QTY": "Menge",
|
||||||
|
"MATERIAL": "Material",
|
||||||
"PER_PIECE": "pro Stück",
|
"PER_PIECE": "pro Stück",
|
||||||
"SHIPPING": "Versand (CH)",
|
"SHIPPING": "Versand (CH)",
|
||||||
"PREVIEW_LOADING": "3D-Vorschau wird geladen...",
|
"PREVIEW_LOADING": "3D-Vorschau wird geladen...",
|
||||||
|
|||||||
@@ -402,6 +402,7 @@
|
|||||||
"SETUP_FEE": "Setup Fee",
|
"SETUP_FEE": "Setup Fee",
|
||||||
"TOTAL": "Total",
|
"TOTAL": "Total",
|
||||||
"QTY": "Qty",
|
"QTY": "Qty",
|
||||||
|
"MATERIAL": "Material",
|
||||||
"PER_PIECE": "per piece",
|
"PER_PIECE": "per piece",
|
||||||
"SHIPPING": "Shipping",
|
"SHIPPING": "Shipping",
|
||||||
"PREVIEW_LOADING": "Loading 3D preview...",
|
"PREVIEW_LOADING": "Loading 3D preview...",
|
||||||
|
|||||||
@@ -459,6 +459,7 @@
|
|||||||
"SETUP_FEE": "Coût de setup",
|
"SETUP_FEE": "Coût de setup",
|
||||||
"TOTAL": "Total",
|
"TOTAL": "Total",
|
||||||
"QTY": "Qté",
|
"QTY": "Qté",
|
||||||
|
"MATERIAL": "Matériau",
|
||||||
"PER_PIECE": "par pièce",
|
"PER_PIECE": "par pièce",
|
||||||
"SHIPPING": "Expédition (CH)",
|
"SHIPPING": "Expédition (CH)",
|
||||||
"PREVIEW_LOADING": "Chargement de l'aperçu 3D...",
|
"PREVIEW_LOADING": "Chargement de l'aperçu 3D...",
|
||||||
|
|||||||
@@ -459,6 +459,7 @@
|
|||||||
"SETUP_FEE": "Costo di Avvio",
|
"SETUP_FEE": "Costo di Avvio",
|
||||||
"TOTAL": "Totale",
|
"TOTAL": "Totale",
|
||||||
"QTY": "Qtà",
|
"QTY": "Qtà",
|
||||||
|
"MATERIAL": "Materiale",
|
||||||
"PER_PIECE": "al pezzo",
|
"PER_PIECE": "al pezzo",
|
||||||
"SHIPPING": "Spedizione (CH)",
|
"SHIPPING": "Spedizione (CH)",
|
||||||
"PREVIEW_LOADING": "Caricamento anteprima 3D...",
|
"PREVIEW_LOADING": "Caricamento anteprima 3D...",
|
||||||
|
|||||||
Reference in New Issue
Block a user