feat(front-end): calculator improvements

This commit is contained in:
2026-03-05 15:43:37 +01:00
parent d061f21d79
commit fe3951b6c3
26 changed files with 901 additions and 112 deletions

View File

@@ -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());

View File

@@ -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";
}
} }

View File

@@ -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());

View File

@@ -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; }

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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)"

View File

@@ -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()

View File

@@ -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>

View File

@@ -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);

View File

@@ -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(' | ');
}
} }

View File

@@ -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") {

View File

@@ -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);

View File

@@ -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',

View File

@@ -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,
})), })),

View File

@@ -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"
> >

View File

@@ -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;

View File

@@ -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...",

View File

@@ -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...",

View File

@@ -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...",

View File

@@ -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...",