feat/calculator-options #26
@@ -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());
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -192,13 +192,18 @@
|
||||
<strong>{{ item.originalFilename }}</strong>
|
||||
</p>
|
||||
<p class="item-meta">
|
||||
Qta: {{ item.quantity }} | Colore:
|
||||
Qta: {{ item.quantity }} | Materiale:
|
||||
{{ item.materialCode || "-" }} | Colore:
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="isHexColor(item.colorCode)"
|
||||
[style.background-color]="item.colorCode"
|
||||
></span>
|
||||
<span>{{ item.colorCode || "-" }}</span>
|
||||
| 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" }}
|
||||
</p>
|
||||
@@ -273,17 +278,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Colori file</h4>
|
||||
<h4>Parametri per file</h4>
|
||||
<div class="file-color-list">
|
||||
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
||||
<span class="filename">{{ item.originalFilename }}</span>
|
||||
<span class="file-color">
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="isHexColor(item.colorCode)"
|
||||
[style.background-color]="item.colorCode"
|
||||
></span>
|
||||
{{ item.colorCode || "-" }}
|
||||
{{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm
|
||||
| {{ item.layerHeightMm ?? "-" }} mm | {{ item.infillPercent ?? "-" }}%
|
||||
| {{ item.infillPattern || "-" }} |
|
||||
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
<th>Qta</th>
|
||||
<th>Tempo</th>
|
||||
<th>Materiale</th>
|
||||
<th>Scelte utente</th>
|
||||
<th>Stato</th>
|
||||
<th>Prezzo unit.</th>
|
||||
</tr>
|
||||
@@ -142,6 +143,14 @@
|
||||
: "-"
|
||||
}}
|
||||
</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.unitPriceChf | currency: "CHF" }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
(submitRequest)="onCalculate($event)"
|
||||
(itemQuantityChange)="onUploadItemQuantityChange($event)"
|
||||
(printSettingsChange)="onUploadPrintSettingsChange($event)"
|
||||
(itemSettingsDiffChange)="onItemSettingsDiffChange($event)"
|
||||
></app-upload-form>
|
||||
</app-card>
|
||||
</div>
|
||||
@@ -67,6 +68,7 @@
|
||||
<app-quote-result
|
||||
[result]="result()!"
|
||||
[recalculationRequired]="requiresRecalculation()"
|
||||
[itemSettingsDiffByFileName]="itemSettingsDiffByFileName()"
|
||||
(consult)="onConsult()"
|
||||
(proceed)="onProceed()"
|
||||
(itemQuantityPreviewChange)="onQuoteItemQuantityPreviewChange($event)"
|
||||
|
||||
@@ -25,6 +25,17 @@ import { SuccessStateComponent } from '../../shared/components/success-state/suc
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
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({
|
||||
selector: 'app-calculator-page',
|
||||
standalone: true,
|
||||
@@ -57,16 +68,11 @@ export class CalculatorPageComponent implements OnInit {
|
||||
|
||||
orderSuccess = signal(false);
|
||||
requiresRecalculation = signal(false);
|
||||
private baselinePrintSettings: {
|
||||
mode: 'easy' | 'advanced';
|
||||
material: string;
|
||||
quality: string;
|
||||
nozzleDiameter: number;
|
||||
layerHeight: number;
|
||||
infillDensity: number;
|
||||
infillPattern: string;
|
||||
supportEnabled: boolean;
|
||||
} | null = null;
|
||||
itemSettingsDiffByFileName = signal<
|
||||
Record<string, { differences: string[] }>
|
||||
>({});
|
||||
private baselinePrintSettings: TrackedPrintSettings | null = null;
|
||||
private baselineItemSettingsByFileName = new Map<string, TrackedPrintSettings>();
|
||||
|
||||
@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,14 +199,21 @@ 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.
|
||||
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,
|
||||
@@ -203,8 +221,11 @@ export class CalculatorPageComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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<string, { differences: string[] }>,
|
||||
) {
|
||||
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<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(
|
||||
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()
|
||||
|
||||
@@ -63,6 +63,12 @@
|
||||
<span class="file-details">
|
||||
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
|
||||
{{ item.unitWeight | number: "1.0-0" }}g
|
||||
@if (getItemDifferenceLabel(item.fileName)) {
|
||||
|
|
||||
<small class="item-settings-diff">
|
||||
{{ getItemDifferenceLabel(item.fileName) }}
|
||||
</small>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,9 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
|
||||
result = input.required<QuoteResult>();
|
||||
recalculationRequired = input<boolean>(false);
|
||||
itemSettingsDiffByFileName = input<Record<string, { differences: string[] }>>(
|
||||
{},
|
||||
);
|
||||
consult = output<void>();
|
||||
proceed = output<void>();
|
||||
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(' | ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<app-color-selector
|
||||
[selectedColor]="item.color"
|
||||
[selectedVariantId]="item.filamentVariantId ?? null"
|
||||
[variants]="currentMaterialVariants()"
|
||||
[variants]="getVariantsForItem(item)"
|
||||
(colorSelected)="updateItemColor(i, $event)"
|
||||
>
|
||||
</app-color-selector>
|
||||
@@ -145,6 +145,15 @@
|
||||
}
|
||||
</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 -->
|
||||
|
||||
@if (mode() === "advanced") {
|
||||
|
||||
@@ -211,6 +211,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sync-all-row {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
margin-bottom: var(--space-3);
|
||||
|
||||
@@ -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<ItemPrintSettings>;
|
||||
|
||||
@Component({
|
||||
selector: 'app-upload-form',
|
||||
standalone: true,
|
||||
@@ -67,6 +84,7 @@ export class UploadFormComponent implements OnInit {
|
||||
fileName: string;
|
||||
quantity: number;
|
||||
}>();
|
||||
itemSettingsDiffChange = output<Record<string, ItemSettingsDiffInfo>>();
|
||||
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<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 {
|
||||
const controlsToLock = [
|
||||
'syncAllItems',
|
||||
'material',
|
||||
'quality',
|
||||
'nozzleDiameter',
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -245,6 +245,10 @@
|
||||
<span
|
||||
>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span
|
||||
>
|
||||
<span>
|
||||
{{ "CHECKOUT.MATERIAL" | translate }}:
|
||||
{{ itemMaterial(item) }}
|
||||
</span>
|
||||
<span
|
||||
*ngIf="item.colorCode"
|
||||
class="color-dot"
|
||||
@@ -255,7 +259,7 @@
|
||||
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
|
||||
{{ item.materialGrams | number: "1.0-0" }}g
|
||||
</div>
|
||||
<div class="item-preview" *ngIf="isStlItem(item)">
|
||||
<div class="item-preview" *ngIf="isCadSession() && isStlItem(item)">
|
||||
<ng-container
|
||||
*ngIf="previewFile(item) as itemPreview; else previewState"
|
||||
>
|
||||
|
||||
@@ -162,7 +162,11 @@ export class CheckoutComponent implements OnInit {
|
||||
this.quoteService.getQuoteSession(this.sessionId).subscribe({
|
||||
next: (session) => {
|
||||
this.quoteSession.set(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;
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
Reference in New Issue
Block a user