diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java
index eb11a76..f83cf59 100644
--- a/backend/src/main/java/com/printcalculator/controller/OrderController.java
+++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java
@@ -353,6 +353,12 @@ public class OrderController {
idto.setOriginalFilename(i.getOriginalFilename());
idto.setMaterialCode(i.getMaterialCode());
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.setPrintTimeSeconds(i.getPrintTimeSeconds());
idto.setMaterialGrams(i.getMaterialGrams());
diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java
index 803a045..015cb8e 100644
--- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java
+++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java
@@ -268,6 +268,15 @@ public class QuoteSessionController {
item.setQuantity(1);
item.setColorCode(selectedVariant.getColorName());
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.setPrintTimeSeconds((int) stats.printTimeSeconds());
@@ -324,6 +333,8 @@ public class QuoteSessionController {
settings.setInfillDensity(15.0);
settings.setInfillPattern("grid");
break;
+ case "extra_fine":
+ case "high_definition":
case "high":
settings.setLayerHeight(0.12);
settings.setInfillDensity(20.0);
@@ -504,6 +515,13 @@ public class QuoteSessionController {
dto.put("materialGrams", item.getMaterialGrams());
dto.put("colorCode", item.getColorCode());
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("convertedStoredPath", extractConvertedStoredPath(item));
@@ -667,4 +685,20 @@ public class QuoteSessionController {
String path = String.valueOf(converted).trim();
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";
+ }
}
diff --git a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java
index c41e8c1..e7140ef 100644
--- a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java
+++ b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java
@@ -277,6 +277,12 @@ public class AdminOrderController {
idto.setOriginalFilename(i.getOriginalFilename());
idto.setMaterialCode(i.getMaterialCode());
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.setPrintTimeSeconds(i.getPrintTimeSeconds());
idto.setMaterialGrams(i.getMaterialGrams());
diff --git a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java
index d31d208..b098507 100644
--- a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java
+++ b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java
@@ -8,6 +8,12 @@ public class OrderItemDto {
private String originalFilename;
private String materialCode;
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 printTimeSeconds;
private BigDecimal materialGrams;
@@ -27,6 +33,24 @@ public class OrderItemDto {
public String getColorCode() { return 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 void setQuantity(Integer quantity) { this.quantity = quantity; }
diff --git a/backend/src/main/java/com/printcalculator/entity/OrderItem.java b/backend/src/main/java/com/printcalculator/entity/OrderItem.java
index e5d6f65..b77573d 100644
--- a/backend/src/main/java/com/printcalculator/entity/OrderItem.java
+++ b/backend/src/main/java/com/printcalculator/entity/OrderItem.java
@@ -44,6 +44,24 @@ public class OrderItem {
@Column(name = "material_code", nullable = false, 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;
+
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "filament_variant_id")
private FilamentVariant filamentVariant;
@@ -162,6 +180,54 @@ public class OrderItem {
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() {
return filamentVariant;
}
diff --git a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java
index 321c705..c55f446 100644
--- a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java
+++ b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java
@@ -45,6 +45,27 @@ public class QuoteLineItem {
@com.fasterxml.jackson.annotation.JsonIgnore
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)
private BigDecimal boundingBoxXMm;
@@ -137,6 +158,62 @@ public class QuoteLineItem {
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() {
return boundingBoxXMm;
}
diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java
index f42b17c..b8efcf3 100644
--- a/backend/src/main/java/com/printcalculator/service/OrderService.java
+++ b/backend/src/main/java/com/printcalculator/service/OrderService.java
@@ -182,6 +182,12 @@ public class OrderService {
} else {
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;
if (totals.totalPrintSeconds().compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
diff --git a/frontend/src/app/features/admin/pages/admin-dashboard.component.html b/frontend/src/app/features/admin/pages/admin-dashboard.component.html
index a8696f9..b3f156b 100644
--- a/frontend/src/app/features/admin/pages/admin-dashboard.component.html
+++ b/frontend/src/app/features/admin/pages/admin-dashboard.component.html
@@ -192,13 +192,18 @@
{{ item.originalFilename }}
- Qta: {{ item.quantity }} | Colore:
+ Qta: {{ item.quantity }} | Materiale:
+ {{ item.materialCode || "-" }} | Colore:
{{ item.colorCode || "-" }}
+ | Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer:
+ {{ item.layerHeightMm ?? "-" }} mm | Infill:
+ {{ item.infillPercent ?? "-" }}% | Supporti:
+ {{ item.supportsEnabled ? "Sì" : "No" }}
| Riga:
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
@@ -273,17 +278,15 @@
- Colori file
+ Parametri per file
{{ item.originalFilename }}
-
- {{ item.colorCode || "-" }}
+ {{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm
+ | {{ item.layerHeightMm ?? "-" }} mm | {{ item.infillPercent ?? "-" }}%
+ | {{ item.infillPattern || "-" }} |
+ {{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
diff --git a/frontend/src/app/features/admin/pages/admin-sessions.component.html b/frontend/src/app/features/admin/pages/admin-sessions.component.html
index 48492ef..48ee86d 100644
--- a/frontend/src/app/features/admin/pages/admin-sessions.component.html
+++ b/frontend/src/app/features/admin/pages/admin-sessions.component.html
@@ -126,6 +126,7 @@
Qta |
Tempo |
Materiale |
+ Scelte utente |
Stato |
Prezzo unit. |
@@ -142,6 +143,14 @@
: "-"
}}
+
+ {{ item.materialCode || "-" }} |
+ {{ item.nozzleDiameterMm ?? "-" }} mm |
+ {{ item.layerHeightMm ?? "-" }} mm |
+ {{ item.infillPercent ?? "-" }}% |
+ {{ item.infillPattern || "-" }} |
+ {{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
+ |
{{ item.status }} |
{{ item.unitPriceChf | currency: "CHF" }} |
diff --git a/frontend/src/app/features/admin/services/admin-operations.service.ts b/frontend/src/app/features/admin/services/admin-operations.service.ts
index 2df5333..4f21cf5 100644
--- a/frontend/src/app/features/admin/services/admin-operations.service.ts
+++ b/frontend/src/app/features/admin/services/admin-operations.service.ts
@@ -127,7 +127,15 @@ export interface AdminQuoteSessionDetailItem {
quantity: number;
printTimeSeconds?: number;
materialGrams?: number;
+ materialCode?: string;
+ quality?: string;
+ nozzleDiameterMm?: number;
+ layerHeightMm?: number;
+ infillPercent?: number;
+ infillPattern?: string;
+ supportsEnabled?: boolean;
colorCode?: string;
+ filamentVariantId?: number;
status: string;
unitPriceChf: number;
}
diff --git a/frontend/src/app/features/admin/services/admin-orders.service.ts b/frontend/src/app/features/admin/services/admin-orders.service.ts
index 39225fe..395010c 100644
--- a/frontend/src/app/features/admin/services/admin-orders.service.ts
+++ b/frontend/src/app/features/admin/services/admin-orders.service.ts
@@ -8,6 +8,12 @@ export interface AdminOrderItem {
originalFilename: string;
materialCode: string;
colorCode: string;
+ quality?: string;
+ nozzleDiameterMm?: number;
+ layerHeightMm?: number;
+ infillPercent?: number;
+ infillPattern?: string;
+ supportsEnabled?: boolean;
quantity: number;
printTimeSeconds: number;
materialGrams: number;
diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html
index 8aab319..14c4970 100644
--- a/frontend/src/app/features/calculator/calculator-page.component.html
+++ b/frontend/src/app/features/calculator/calculator-page.component.html
@@ -47,6 +47,7 @@
(submitRequest)="onCalculate($event)"
(itemQuantityChange)="onUploadItemQuantityChange($event)"
(printSettingsChange)="onUploadPrintSettingsChange($event)"
+ (itemSettingsDiffChange)="onItemSettingsDiffChange($event)"
>
@@ -67,6 +68,7 @@
+ >({});
+ private baselinePrintSettings: TrackedPrintSettings | null = null;
+ private baselineItemSettingsByFileName = new Map();
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
@ViewChild('resultCol') resultCol!: ElementRef;
@@ -115,7 +121,12 @@ export class CalculatorPageComponent implements OnInit {
this.baselinePrintSettings = this.toTrackedSettingsFromSession(
data.session,
);
+ this.baselineItemSettingsByFileName = this.buildBaselineMapFromSession(
+ data.items || [],
+ this.baselinePrintSettings,
+ );
this.requiresRecalculation.set(false);
+ this.itemSettingsDiffByFileName.set({});
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
this.cadSessionLocked.set(isCadSession);
this.step.set('quote');
@@ -188,23 +199,33 @@ export class CalculatorPageComponent implements OnInit {
});
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) => {
- // Assuming index matches.
- // Need to be careful if items order changed, but usually ID sort or insert order.
- if (item.colorCode) {
- this.uploadForm.updateItemColor(index, {
- colorName: item.colorCode,
- filamentVariantId: item.filamentVariantId,
- });
- }
+ items.forEach((item, index) => {
+ const tracked = this.toTrackedSettingsFromSessionItem(
+ 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) {
+ this.uploadForm.updateItemColor(index, {
+ colorName: item.colorCode,
+ filamentVariantId: item.filamentVariantId,
});
}
});
+
+ const selected = this.uploadForm.selectedFile();
+ if (selected) {
+ this.uploadForm.selectFile(selected);
+ }
}
this.loading.set(false);
},
@@ -254,7 +275,10 @@ export class CalculatorPageComponent implements OnInit {
this.errorKey.set('CALC.ERROR_GENERIC');
this.result.set(res);
this.baselinePrintSettings = this.toTrackedSettingsFromRequest(req);
+ this.baselineItemSettingsByFileName =
+ this.buildBaselineMapFromRequest(req);
this.requiresRecalculation.set(false);
+ this.itemSettingsDiffByFileName.set({});
this.loading.set(false);
this.uploadProgress.set(100);
this.step.set('quote');
@@ -395,7 +419,12 @@ export class CalculatorPageComponent implements OnInit {
this.step.set('upload');
this.result.set(null);
this.requiresRecalculation.set(false);
+ this.itemSettingsDiffByFileName.set({});
this.baselinePrintSettings = null;
+ this.baselineItemSettingsByFileName = new Map<
+ string,
+ TrackedPrintSettings
+ >();
this.cadSessionLocked.set(false);
this.orderSuccess.set(false);
this.switchMode('easy'); // Reset to default and sync URL
@@ -403,21 +432,16 @@ export class CalculatorPageComponent implements OnInit {
private currentRequest: QuoteRequest | null = null;
- onUploadPrintSettingsChange(settings: {
- mode: 'easy' | 'advanced';
- material: string;
- quality: string;
- nozzleDiameter: number;
- layerHeight: number;
- infillDensity: number;
- infillPattern: string;
- supportEnabled: boolean;
- }) {
+ onUploadPrintSettingsChange(_: TrackedPrintSettings) {
+ void _;
if (!this.result()) return;
- if (!this.baselinePrintSettings) return;
- this.requiresRecalculation.set(
- !this.sameTrackedSettings(this.baselinePrintSettings, settings),
- );
+ this.refreshRecalculationRequirement();
+ }
+
+ onItemSettingsDiffChange(
+ diffByFileName: Record,
+ ) {
+ this.itemSettingsDiffByFileName.set(diffByFileName || {});
}
onConsult() {
@@ -478,7 +502,12 @@ export class CalculatorPageComponent implements OnInit {
this.error.set(true);
this.result.set(null);
this.requiresRecalculation.set(false);
+ this.itemSettingsDiffByFileName.set({});
this.baselinePrintSettings = null;
+ this.baselineItemSettingsByFileName = new Map<
+ string,
+ TrackedPrintSettings
+ >();
}
switchMode(nextMode: 'easy' | 'advanced'): void {
@@ -499,16 +528,7 @@ export class CalculatorPageComponent implements OnInit {
});
}
- private toTrackedSettingsFromRequest(req: QuoteRequest): {
- mode: 'easy' | 'advanced';
- material: string;
- quality: string;
- nozzleDiameter: number;
- layerHeight: number;
- infillDensity: number;
- infillPattern: string;
- supportEnabled: boolean;
- } {
+ private toTrackedSettingsFromRequest(req: QuoteRequest): TrackedPrintSettings {
return {
mode: req.mode,
material: this.normalizeString(req.material || 'PLA'),
@@ -521,16 +541,37 @@ export class CalculatorPageComponent implements OnInit {
};
}
- private toTrackedSettingsFromSession(session: any): {
- mode: 'easy' | 'advanced';
- material: string;
- quality: string;
- nozzleDiameter: number;
- layerHeight: number;
- infillDensity: number;
- infillPattern: string;
- supportEnabled: boolean;
- } {
+ private toTrackedSettingsFromItem(
+ req: QuoteRequest,
+ item: QuoteRequest['items'][number],
+ ): TrackedPrintSettings {
+ return {
+ mode: req.mode,
+ material: this.normalizeString(item.material || req.material || 'PLA'),
+ quality: this.normalizeString(item.quality || req.quality || 'standard'),
+ 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);
return {
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 {
+ const map = new Map();
+ 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 {
+ const map = new Map();
+ 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(
- a: {
- mode: 'easy' | 'advanced';
- material: string;
- quality: string;
- nozzleDiameter: number;
- layerHeight: number;
- infillDensity: number;
- infillPattern: string;
- supportEnabled: boolean;
- },
- b: {
- mode: 'easy' | 'advanced';
- material: string;
- quality: string;
- nozzleDiameter: number;
- layerHeight: number;
- infillDensity: number;
- infillPattern: string;
- supportEnabled: boolean;
- },
+ a: TrackedPrintSettings,
+ b: TrackedPrintSettings,
): boolean {
return (
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 {
return String(value || '')
.trim()
diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html
index 2c81b84..67e5618 100644
--- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html
+++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html
@@ -63,6 +63,12 @@
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
{{ item.unitWeight | number: "1.0-0" }}g
+ @if (getItemDifferenceLabel(item.fileName)) {
+ |
+
+ {{ getItemDifferenceLabel(item.fileName) }}
+
+ }
diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss
index b4e94b2..8218937 100644
--- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss
+++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss
@@ -41,6 +41,14 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+.item-settings-diff {
+ margin-left: 2px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: #8a6d1f;
+ white-space: normal;
+}
.file-details {
font-size: 0.8rem;
color: var(--color-text-muted);
diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts
index ce0df34..2aad331 100644
--- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts
+++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts
@@ -36,6 +36,9 @@ export class QuoteResultComponent implements OnDestroy {
result = input.required();
recalculationRequired = input(false);
+ itemSettingsDiffByFileName = input>(
+ {},
+ );
consult = output();
proceed = output();
itemChange = output<{
@@ -185,4 +188,15 @@ export class QuoteResultComponent implements OnDestroy {
this.quantityTimers.forEach((timer) => clearTimeout(timer));
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(' | ');
+ }
}
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html
index ec755cd..da2719b 100644
--- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html
+++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html
@@ -63,7 +63,7 @@
@@ -145,6 +145,15 @@
}
+ @if (items().length > 1) {
+
+
+
+
+ }
+
@if (mode() === "advanced") {
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss
index 16ac239..f5d4f4b 100644
--- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss
+++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss
@@ -211,6 +211,12 @@
}
}
+.sync-all-row {
+ margin-top: var(--space-2);
+ margin-bottom: var(--space-4);
+ padding-top: 0;
+}
+
/* Progress Bar */
.progress-container {
margin-bottom: var(--space-3);
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
index 6b9d46c..b41ffcb 100644
--- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
+++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
@@ -37,8 +37,25 @@ interface FormItem {
quantity: number;
color: string;
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;
+
@Component({
selector: 'app-upload-form',
standalone: true,
@@ -67,6 +84,7 @@ export class UploadFormComponent implements OnInit {
fileName: string;
quantity: number;
}>();
+ itemSettingsDiffChange = output>();
printSettingsChange = output<{
mode: 'easy' | 'advanced';
material: string;
@@ -108,7 +126,7 @@ export class UploadFormComponent implements OnInit {
if (matCode && this.fullMaterialOptions.length > 0) {
const found = this.fullMaterialOptions.find((m) => m.code === matCode);
this.currentMaterialVariants.set(found ? found.variants : []);
- this.syncItemVariantSelections();
+ this.syncSelectedItemVariantSelection();
} else {
this.currentMaterialVariants.set([]);
}
@@ -137,6 +155,7 @@ export class UploadFormComponent implements OnInit {
constructor() {
this.form = this.fb.group({
itemsTouched: [false], // Hack to track touched state for custom items list
+ syncAllItems: [true],
material: ['', Validators.required],
quality: ['', Validators.required],
items: [[]], // Track items in form for validation if needed
@@ -164,7 +183,9 @@ export class UploadFormComponent implements OnInit {
});
this.form.valueChanges.subscribe(() => {
if (this.isPatchingSettings) return;
+ this.syncSelectedItemSettingsFromForm();
this.emitPrintSettingsChange();
+ this.emitItemSettingsDiffChange();
});
effect(() => {
@@ -337,6 +358,7 @@ export class UploadFormComponent implements OnInit {
quantity: 1,
color: defaultSelection.colorName,
filamentVariantId: defaultSelection.filamentVariantId,
+ printSettings: this.getCurrentItemPrintSettings(),
});
}
}
@@ -349,7 +371,8 @@ export class UploadFormComponent implements OnInit {
this.items.update((current) => [...current, ...validItems]);
this.form.get('itemsTouched')?.setValue(true);
// 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 {
this.selectedFile.set(file);
}
+ this.loadSelectedItemSettingsIntoForm();
}
// Helper to get color of currently selected file
@@ -451,17 +475,55 @@ export class UploadFormComponent implements OnInit {
};
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) {
+ let nextSelected: File | null = null;
this.items.update((current) => {
const updated = [...current];
const removed = updated.splice(index, 1)[0];
if (this.selectedFile() === removed.file) {
- this.selectedFile.set(null);
+ nextSelected = updated.length > 0 ? updated[Math.max(0, index - 1)].file : null;
}
return updated;
});
+ if (nextSelected) {
+ this.selectFile(nextSelected);
+ } else if (this.items().length === 0) {
+ this.selectedFile.set(null);
+ }
+ this.emitItemSettingsDiffChange();
}
setFiles(files: File[]) {
@@ -474,6 +536,7 @@ export class UploadFormComponent implements OnInit {
quantity: 1,
color: defaultSelection.colorName,
filamentVariantId: defaultSelection.filamentVariantId,
+ printSettings: this.getCurrentItemPrintSettings(),
});
}
@@ -481,7 +544,8 @@ export class UploadFormComponent implements OnInit {
this.items.set(validItems);
this.form.get('itemsTouched')?.setValue(true);
// 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' };
}
- 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();
if (!vars || vars.length === 0) {
return;
}
+ const selected = this.selectedFile();
+ if (!selected) {
+ return;
+ }
+
const fallback = vars.find((v) => !v.isOutOfStock) || vars[0];
this.items.update((current) =>
current.map((item) => {
+ if (item.file !== selected) {
+ return item;
+ }
const byId =
item.filamentVariantId != null
? vars.find((v) => v.id === item.filamentVariantId)
: null;
const byColor = vars.find((v) => v.colorName === item.color);
- const selected = byId || byColor || fallback;
+ const selectedVariant = byId || byColor || fallback;
return {
...item,
- color: selected.colorName,
- filamentVariantId: selected.id,
+ color: selectedVariant.colorName,
+ filamentVariantId: selectedVariant.id,
};
}),
);
@@ -592,7 +679,7 @@ export class UploadFormComponent implements OnInit {
);
this.submitRequest.emit({
...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(),
});
} else {
@@ -666,7 +753,7 @@ export class UploadFormComponent implements OnInit {
if (this.items().length === 0) return null;
const raw = this.form.getRawValue();
return {
- items: this.items(),
+ items: this.toQuoteRequestItems(),
material: raw.material,
quality: raw.quality,
notes: raw.notes,
@@ -706,8 +793,248 @@ export class UploadFormComponent implements OnInit {
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();
+ 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 = {};
+ 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 {
const controlsToLock = [
+ 'syncAllItems',
'material',
'quality',
'nozzleDiameter',
diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts
index fed6db7..152b7f2 100644
--- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts
+++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts
@@ -10,6 +10,13 @@ export interface QuoteRequest {
quantity: number;
color?: string;
filamentVariantId?: number;
+ material?: string;
+ quality?: string;
+ nozzleDiameter?: number;
+ layerHeight?: number;
+ infillDensity?: number;
+ infillPattern?: string;
+ supportEnabled?: boolean;
}[];
material: string;
quality: string;
@@ -150,7 +157,7 @@ export class QuoteEstimatorService {
if (normalized === 'draft') {
return {
- quality: 'extra_fine',
+ quality: 'draft',
layerHeight: 0.24,
infillDensity: 12,
infillPattern: 'grid',
@@ -306,23 +313,28 @@ export class QuoteEstimatorService {
request.mode === 'easy'
? 'ADVANCED'
: request.mode.toUpperCase(),
- material: request.material,
+ material: item.material || request.material,
filamentVariantId: item.filamentVariantId,
- quality: easyPreset ? easyPreset.quality : request.quality,
- supportsEnabled: request.supportEnabled,
+ quality: easyPreset
+ ? easyPreset.quality
+ : item.quality || request.quality,
+ supportsEnabled:
+ easyPreset != null
+ ? request.supportEnabled
+ : item.supportEnabled ?? request.supportEnabled,
color: item.color || '#FFFFFF',
layerHeight: easyPreset
? easyPreset.layerHeight
- : request.layerHeight,
+ : item.layerHeight ?? request.layerHeight,
infillDensity: easyPreset
? easyPreset.infillDensity
- : request.infillDensity,
+ : item.infillDensity ?? request.infillDensity,
infillPattern: easyPreset
? easyPreset.infillPattern
- : request.infillPattern,
+ : item.infillPattern ?? request.infillPattern,
nozzleDiameter: easyPreset
? easyPreset.nozzleDiameter
- : request.nozzleDiameter,
+ : item.nozzleDiameter ?? request.nozzleDiameter,
};
const settingsBlob = new Blob([JSON.stringify(settings)], {
@@ -477,9 +489,7 @@ export class QuoteEstimatorService {
unitTime: item.printTimeSeconds,
unitWeight: item.materialGrams,
quantity: item.quantity,
- material: session.materialCode, // Assumption: session has one material for all? or items have it?
- // Backend model QuoteSession has materialCode.
- // But line items might have different colors.
+ material: item.materialCode || session.materialCode,
color: item.colorCode,
filamentVariantId: item.filamentVariantId,
})),
diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html
index 123df04..0a85813 100644
--- a/frontend/src/app/features/checkout/checkout.component.html
+++ b/frontend/src/app/features/checkout/checkout.component.html
@@ -245,6 +245,10 @@
{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}
+
+ {{ "CHECKOUT.MATERIAL" | translate }}:
+ {{ itemMaterial(item) }}
+
-
+
diff --git a/frontend/src/app/features/checkout/checkout.component.ts b/frontend/src/app/features/checkout/checkout.component.ts
index ad95985..66e98fc 100644
--- a/frontend/src/app/features/checkout/checkout.component.ts
+++ b/frontend/src/app/features/checkout/checkout.component.ts
@@ -162,7 +162,11 @@ export class CheckoutComponent implements OnInit {
this.quoteService.getQuoteSession(this.sessionId).subscribe({
next: (session) => {
this.quoteSession.set(session);
- this.loadStlPreviews(session);
+ if (this.isCadSessionData(session)) {
+ this.loadStlPreviews(session);
+ } else {
+ this.resetPreviewState();
+ }
console.log('Loaded session:', session);
},
error: (err) => {
@@ -173,7 +177,7 @@ export class CheckoutComponent implements OnInit {
}
isCadSession(): boolean {
- return this.quoteSession()?.session?.status === 'CAD_ACTIVE';
+ return this.isCadSessionData(this.quoteSession());
}
cadRequestId(): string | null {
@@ -188,6 +192,12 @@ export class CheckoutComponent implements OnInit {
return this.quoteSession()?.cadTotalChf ?? 0;
}
+ itemMaterial(item: any): string {
+ return String(
+ item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-',
+ );
+ }
+
isStlItem(item: any): boolean {
const name = String(item?.originalFilename ?? '').toLowerCase();
return name.endsWith('.stl');
@@ -241,7 +251,11 @@ export class CheckoutComponent implements OnInit {
}
private loadStlPreviews(session: any): void {
- if (!this.sessionId || !Array.isArray(session?.items)) {
+ if (
+ !this.sessionId ||
+ !this.isCadSessionData(session) ||
+ !Array.isArray(session?.items)
+ ) {
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() {
if (this.checkoutForm.invalid) {
return;
diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json
index ca1275b..d5b49f4 100644
--- a/frontend/src/assets/i18n/de.json
+++ b/frontend/src/assets/i18n/de.json
@@ -402,6 +402,7 @@
"SETUP_FEE": "Einrichtungskosten",
"TOTAL": "Gesamt",
"QTY": "Menge",
+ "MATERIAL": "Material",
"PER_PIECE": "pro Stück",
"SHIPPING": "Versand (CH)",
"PREVIEW_LOADING": "3D-Vorschau wird geladen...",
diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json
index e464d13..b02eeb0 100644
--- a/frontend/src/assets/i18n/en.json
+++ b/frontend/src/assets/i18n/en.json
@@ -402,6 +402,7 @@
"SETUP_FEE": "Setup Fee",
"TOTAL": "Total",
"QTY": "Qty",
+ "MATERIAL": "Material",
"PER_PIECE": "per piece",
"SHIPPING": "Shipping",
"PREVIEW_LOADING": "Loading 3D preview...",
diff --git a/frontend/src/assets/i18n/fr.json b/frontend/src/assets/i18n/fr.json
index 02ebd0c..7e54110 100644
--- a/frontend/src/assets/i18n/fr.json
+++ b/frontend/src/assets/i18n/fr.json
@@ -459,6 +459,7 @@
"SETUP_FEE": "Coût de setup",
"TOTAL": "Total",
"QTY": "Qté",
+ "MATERIAL": "Matériau",
"PER_PIECE": "par pièce",
"SHIPPING": "Expédition (CH)",
"PREVIEW_LOADING": "Chargement de l'aperçu 3D...",
diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json
index 9444cb8..4e6b8f5 100644
--- a/frontend/src/assets/i18n/it.json
+++ b/frontend/src/assets/i18n/it.json
@@ -459,6 +459,7 @@
"SETUP_FEE": "Costo di Avvio",
"TOTAL": "Totale",
"QTY": "Qtà",
+ "MATERIAL": "Materiale",
"PER_PIECE": "al pezzo",
"SHIPPING": "Spedizione (CH)",
"PREVIEW_LOADING": "Caricamento anteprima 3D...",