diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index eb11a76..f83cf59 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -353,6 +353,12 @@ public class OrderController { idto.setOriginalFilename(i.getOriginalFilename()); idto.setMaterialCode(i.getMaterialCode()); idto.setColorCode(i.getColorCode()); + idto.setQuality(i.getQuality()); + idto.setNozzleDiameterMm(i.getNozzleDiameterMm()); + idto.setLayerHeightMm(i.getLayerHeightMm()); + idto.setInfillPercent(i.getInfillPercent()); + idto.setInfillPattern(i.getInfillPattern()); + idto.setSupportsEnabled(i.getSupportsEnabled()); idto.setQuantity(i.getQuantity()); idto.setPrintTimeSeconds(i.getPrintTimeSeconds()); idto.setMaterialGrams(i.getMaterialGrams()); diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java index 803a045..015cb8e 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -268,6 +268,15 @@ public class QuoteSessionController { item.setQuantity(1); item.setColorCode(selectedVariant.getColorName()); item.setFilamentVariant(selectedVariant); + item.setMaterialCode(selectedVariant.getFilamentMaterialType() != null + ? selectedVariant.getFilamentMaterialType().getMaterialCode() + : normalizeRequestedMaterialCode(settings.getMaterial())); + item.setQuality(resolveQuality(settings, layerHeight)); + item.setNozzleDiameterMm(nozzleDiameter); + item.setLayerHeightMm(layerHeight); + item.setInfillPercent(settings.getInfillDensity() != null ? settings.getInfillDensity().intValue() : 20); + item.setInfillPattern(settings.getInfillPattern()); + item.setSupportsEnabled(settings.getSupportsEnabled() != null ? settings.getSupportsEnabled() : false); item.setStatus("READY"); // or CALCULATED item.setPrintTimeSeconds((int) stats.printTimeSeconds()); @@ -324,6 +333,8 @@ public class QuoteSessionController { settings.setInfillDensity(15.0); settings.setInfillPattern("grid"); break; + case "extra_fine": + case "high_definition": case "high": settings.setLayerHeight(0.12); settings.setInfillDensity(20.0); @@ -504,6 +515,13 @@ public class QuoteSessionController { dto.put("materialGrams", item.getMaterialGrams()); dto.put("colorCode", item.getColorCode()); dto.put("filamentVariantId", item.getFilamentVariant() != null ? item.getFilamentVariant().getId() : null); + dto.put("materialCode", item.getMaterialCode()); + dto.put("quality", item.getQuality()); + dto.put("nozzleDiameterMm", item.getNozzleDiameterMm()); + dto.put("layerHeightMm", item.getLayerHeightMm()); + dto.put("infillPercent", item.getInfillPercent()); + dto.put("infillPattern", item.getInfillPattern()); + dto.put("supportsEnabled", item.getSupportsEnabled()); dto.put("status", item.getStatus()); dto.put("convertedStoredPath", extractConvertedStoredPath(item)); @@ -667,4 +685,20 @@ public class QuoteSessionController { String path = String.valueOf(converted).trim(); return path.isEmpty() ? null : path; } + + private String resolveQuality(com.printcalculator.dto.PrintSettingsDto settings, BigDecimal layerHeight) { + if (settings.getQuality() != null && !settings.getQuality().isBlank()) { + return settings.getQuality().trim().toLowerCase(Locale.ROOT); + } + if (layerHeight == null) { + return "standard"; + } + if (layerHeight.compareTo(BigDecimal.valueOf(0.24)) >= 0) { + return "draft"; + } + if (layerHeight.compareTo(BigDecimal.valueOf(0.12)) <= 0) { + return "extra_fine"; + } + return "standard"; + } } diff --git a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java index c41e8c1..e7140ef 100644 --- a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java @@ -277,6 +277,12 @@ public class AdminOrderController { idto.setOriginalFilename(i.getOriginalFilename()); idto.setMaterialCode(i.getMaterialCode()); idto.setColorCode(i.getColorCode()); + idto.setQuality(i.getQuality()); + idto.setNozzleDiameterMm(i.getNozzleDiameterMm()); + idto.setLayerHeightMm(i.getLayerHeightMm()); + idto.setInfillPercent(i.getInfillPercent()); + idto.setInfillPattern(i.getInfillPattern()); + idto.setSupportsEnabled(i.getSupportsEnabled()); idto.setQuantity(i.getQuantity()); idto.setPrintTimeSeconds(i.getPrintTimeSeconds()); idto.setMaterialGrams(i.getMaterialGrams()); diff --git a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java index d31d208..b098507 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java @@ -8,6 +8,12 @@ public class OrderItemDto { private String originalFilename; private String materialCode; private String colorCode; + private String quality; + private BigDecimal nozzleDiameterMm; + private BigDecimal layerHeightMm; + private Integer infillPercent; + private String infillPattern; + private Boolean supportsEnabled; private Integer quantity; private Integer printTimeSeconds; private BigDecimal materialGrams; @@ -27,6 +33,24 @@ public class OrderItemDto { public String getColorCode() { return colorCode; } public void setColorCode(String colorCode) { this.colorCode = colorCode; } + public String getQuality() { return quality; } + public void setQuality(String quality) { this.quality = quality; } + + public BigDecimal getNozzleDiameterMm() { return nozzleDiameterMm; } + public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) { this.nozzleDiameterMm = nozzleDiameterMm; } + + public BigDecimal getLayerHeightMm() { return layerHeightMm; } + public void setLayerHeightMm(BigDecimal layerHeightMm) { this.layerHeightMm = layerHeightMm; } + + public Integer getInfillPercent() { return infillPercent; } + public void setInfillPercent(Integer infillPercent) { this.infillPercent = infillPercent; } + + public String getInfillPattern() { return infillPattern; } + public void setInfillPattern(String infillPattern) { this.infillPattern = infillPattern; } + + public Boolean getSupportsEnabled() { return supportsEnabled; } + public void setSupportsEnabled(Boolean supportsEnabled) { this.supportsEnabled = supportsEnabled; } + public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } diff --git a/backend/src/main/java/com/printcalculator/entity/OrderItem.java b/backend/src/main/java/com/printcalculator/entity/OrderItem.java index e5d6f65..b77573d 100644 --- a/backend/src/main/java/com/printcalculator/entity/OrderItem.java +++ b/backend/src/main/java/com/printcalculator/entity/OrderItem.java @@ -44,6 +44,24 @@ public class OrderItem { @Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE) private String materialCode; + @Column(name = "quality", length = Integer.MAX_VALUE) + private String quality; + + @Column(name = "nozzle_diameter_mm", precision = 4, scale = 2) + private BigDecimal nozzleDiameterMm; + + @Column(name = "layer_height_mm", precision = 5, scale = 3) + private BigDecimal layerHeightMm; + + @Column(name = "infill_percent") + private Integer infillPercent; + + @Column(name = "infill_pattern", length = Integer.MAX_VALUE) + private String infillPattern; + + @Column(name = "supports_enabled") + private Boolean supportsEnabled; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "filament_variant_id") private FilamentVariant filamentVariant; @@ -162,6 +180,54 @@ public class OrderItem { this.materialCode = materialCode; } + public String getQuality() { + return quality; + } + + public void setQuality(String quality) { + this.quality = quality; + } + + public BigDecimal getNozzleDiameterMm() { + return nozzleDiameterMm; + } + + public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) { + this.nozzleDiameterMm = nozzleDiameterMm; + } + + public BigDecimal getLayerHeightMm() { + return layerHeightMm; + } + + public void setLayerHeightMm(BigDecimal layerHeightMm) { + this.layerHeightMm = layerHeightMm; + } + + public Integer getInfillPercent() { + return infillPercent; + } + + public void setInfillPercent(Integer infillPercent) { + this.infillPercent = infillPercent; + } + + public String getInfillPattern() { + return infillPattern; + } + + public void setInfillPattern(String infillPattern) { + this.infillPattern = infillPattern; + } + + public Boolean getSupportsEnabled() { + return supportsEnabled; + } + + public void setSupportsEnabled(Boolean supportsEnabled) { + this.supportsEnabled = supportsEnabled; + } + public FilamentVariant getFilamentVariant() { return filamentVariant; } diff --git a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java index 321c705..c55f446 100644 --- a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java +++ b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java @@ -45,6 +45,27 @@ public class QuoteLineItem { @com.fasterxml.jackson.annotation.JsonIgnore private FilamentVariant filamentVariant; + @Column(name = "material_code", length = Integer.MAX_VALUE) + private String materialCode; + + @Column(name = "quality", length = Integer.MAX_VALUE) + private String quality; + + @Column(name = "nozzle_diameter_mm", precision = 4, scale = 2) + private BigDecimal nozzleDiameterMm; + + @Column(name = "layer_height_mm", precision = 5, scale = 3) + private BigDecimal layerHeightMm; + + @Column(name = "infill_percent") + private Integer infillPercent; + + @Column(name = "infill_pattern", length = Integer.MAX_VALUE) + private String infillPattern; + + @Column(name = "supports_enabled") + private Boolean supportsEnabled; + @Column(name = "bounding_box_x_mm", precision = 10, scale = 3) private BigDecimal boundingBoxXMm; @@ -137,6 +158,62 @@ public class QuoteLineItem { this.filamentVariant = filamentVariant; } + public String getMaterialCode() { + return materialCode; + } + + public void setMaterialCode(String materialCode) { + this.materialCode = materialCode; + } + + public String getQuality() { + return quality; + } + + public void setQuality(String quality) { + this.quality = quality; + } + + public BigDecimal getNozzleDiameterMm() { + return nozzleDiameterMm; + } + + public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) { + this.nozzleDiameterMm = nozzleDiameterMm; + } + + public BigDecimal getLayerHeightMm() { + return layerHeightMm; + } + + public void setLayerHeightMm(BigDecimal layerHeightMm) { + this.layerHeightMm = layerHeightMm; + } + + public Integer getInfillPercent() { + return infillPercent; + } + + public void setInfillPercent(Integer infillPercent) { + this.infillPercent = infillPercent; + } + + public String getInfillPattern() { + return infillPattern; + } + + public void setInfillPattern(String infillPattern) { + this.infillPattern = infillPattern; + } + + public Boolean getSupportsEnabled() { + return supportsEnabled; + } + + public void setSupportsEnabled(Boolean supportsEnabled) { + this.supportsEnabled = supportsEnabled; + } + public BigDecimal getBoundingBoxXMm() { return boundingBoxXMm; } diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index f42b17c..b8efcf3 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -182,6 +182,12 @@ public class OrderService { } else { oItem.setMaterialCode(session.getMaterialCode()); } + oItem.setQuality(qItem.getQuality()); + oItem.setNozzleDiameterMm(qItem.getNozzleDiameterMm()); + oItem.setLayerHeightMm(qItem.getLayerHeightMm()); + oItem.setInfillPercent(qItem.getInfillPercent()); + oItem.setInfillPattern(qItem.getInfillPattern()); + oItem.setSupportsEnabled(qItem.getSupportsEnabled()); BigDecimal distributedUnitPrice = qItem.getUnitPriceChf() != null ? qItem.getUnitPriceChf() : BigDecimal.ZERO; if (totals.totalPrintSeconds().compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) { diff --git a/frontend/src/app/features/admin/pages/admin-dashboard.component.html b/frontend/src/app/features/admin/pages/admin-dashboard.component.html index a8696f9..b3f156b 100644 --- a/frontend/src/app/features/admin/pages/admin-dashboard.component.html +++ b/frontend/src/app/features/admin/pages/admin-dashboard.component.html @@ -192,13 +192,18 @@ {{ item.originalFilename }}

- Qta: {{ item.quantity }} | Colore: + Qta: {{ item.quantity }} | Materiale: + {{ item.materialCode || "-" }} | Colore: {{ item.colorCode || "-" }} + | Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer: + {{ item.layerHeightMm ?? "-" }} mm | Infill: + {{ item.infillPercent ?? "-" }}% | Supporti: + {{ item.supportsEnabled ? "Sì" : "No" }} | Riga: {{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}

@@ -273,17 +278,15 @@ -

Colori file

+

Parametri per file

{{ item.originalFilename }} - - {{ item.colorCode || "-" }} + {{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm + | {{ item.layerHeightMm ?? "-" }} mm | {{ item.infillPercent ?? "-" }}% + | {{ item.infillPattern || "-" }} | + {{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
diff --git a/frontend/src/app/features/admin/pages/admin-sessions.component.html b/frontend/src/app/features/admin/pages/admin-sessions.component.html index 48492ef..48ee86d 100644 --- a/frontend/src/app/features/admin/pages/admin-sessions.component.html +++ b/frontend/src/app/features/admin/pages/admin-sessions.component.html @@ -126,6 +126,7 @@ Qta Tempo Materiale + Scelte utente Stato Prezzo unit. @@ -142,6 +143,14 @@ : "-" }} + + {{ item.materialCode || "-" }} | + {{ item.nozzleDiameterMm ?? "-" }} mm | + {{ item.layerHeightMm ?? "-" }} mm | + {{ item.infillPercent ?? "-" }}% | + {{ item.infillPattern || "-" }} | + {{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }} + {{ item.status }} {{ item.unitPriceChf | currency: "CHF" }} diff --git a/frontend/src/app/features/admin/services/admin-operations.service.ts b/frontend/src/app/features/admin/services/admin-operations.service.ts index 2df5333..4f21cf5 100644 --- a/frontend/src/app/features/admin/services/admin-operations.service.ts +++ b/frontend/src/app/features/admin/services/admin-operations.service.ts @@ -127,7 +127,15 @@ export interface AdminQuoteSessionDetailItem { quantity: number; printTimeSeconds?: number; materialGrams?: number; + materialCode?: string; + quality?: string; + nozzleDiameterMm?: number; + layerHeightMm?: number; + infillPercent?: number; + infillPattern?: string; + supportsEnabled?: boolean; colorCode?: string; + filamentVariantId?: number; status: string; unitPriceChf: number; } diff --git a/frontend/src/app/features/admin/services/admin-orders.service.ts b/frontend/src/app/features/admin/services/admin-orders.service.ts index 39225fe..395010c 100644 --- a/frontend/src/app/features/admin/services/admin-orders.service.ts +++ b/frontend/src/app/features/admin/services/admin-orders.service.ts @@ -8,6 +8,12 @@ export interface AdminOrderItem { originalFilename: string; materialCode: string; colorCode: string; + quality?: string; + nozzleDiameterMm?: number; + layerHeightMm?: number; + infillPercent?: number; + infillPattern?: string; + supportsEnabled?: boolean; quantity: number; printTimeSeconds: number; materialGrams: number; diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index 8aab319..14c4970 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -47,6 +47,7 @@ (submitRequest)="onCalculate($event)" (itemQuantityChange)="onUploadItemQuantityChange($event)" (printSettingsChange)="onUploadPrintSettingsChange($event)" + (itemSettingsDiffChange)="onItemSettingsDiffChange($event)" > @@ -67,6 +68,7 @@ + >({}); + private baselinePrintSettings: TrackedPrintSettings | null = null; + private baselineItemSettingsByFileName = new Map(); @ViewChild('uploadForm') uploadForm!: UploadFormComponent; @ViewChild('resultCol') resultCol!: ElementRef; @@ -115,7 +121,12 @@ export class CalculatorPageComponent implements OnInit { this.baselinePrintSettings = this.toTrackedSettingsFromSession( data.session, ); + this.baselineItemSettingsByFileName = this.buildBaselineMapFromSession( + data.items || [], + this.baselinePrintSettings, + ); this.requiresRecalculation.set(false); + this.itemSettingsDiffByFileName.set({}); const isCadSession = data?.session?.status === 'CAD_ACTIVE'; this.cadSessionLocked.set(isCadSession); this.step.set('quote'); @@ -188,23 +199,33 @@ export class CalculatorPageComponent implements OnInit { }); this.uploadForm.patchSettings(session); - // Also restore colors? - // setFiles inits with 'Black'. We need to update them if they differ. - // items has colorCode. - setTimeout(() => { - if (this.uploadForm) { - items.forEach((item, index) => { - // Assuming index matches. - // Need to be careful if items order changed, but usually ID sort or insert order. - if (item.colorCode) { - this.uploadForm.updateItemColor(index, { - colorName: item.colorCode, - filamentVariantId: item.filamentVariantId, - }); - } + items.forEach((item, index) => { + const tracked = this.toTrackedSettingsFromSessionItem( + item, + this.toTrackedSettingsFromSession(session), + ); + this.uploadForm.setItemPrintSettingsByIndex(index, { + material: tracked.material.toUpperCase(), + quality: tracked.quality, + nozzleDiameter: tracked.nozzleDiameter, + layerHeight: tracked.layerHeight, + infillDensity: tracked.infillDensity, + infillPattern: tracked.infillPattern, + supportEnabled: tracked.supportEnabled, + }); + + if (item.colorCode) { + this.uploadForm.updateItemColor(index, { + colorName: item.colorCode, + filamentVariantId: item.filamentVariantId, }); } }); + + const selected = this.uploadForm.selectedFile(); + if (selected) { + this.uploadForm.selectFile(selected); + } } this.loading.set(false); }, @@ -254,7 +275,10 @@ export class CalculatorPageComponent implements OnInit { this.errorKey.set('CALC.ERROR_GENERIC'); this.result.set(res); this.baselinePrintSettings = this.toTrackedSettingsFromRequest(req); + this.baselineItemSettingsByFileName = + this.buildBaselineMapFromRequest(req); this.requiresRecalculation.set(false); + this.itemSettingsDiffByFileName.set({}); this.loading.set(false); this.uploadProgress.set(100); this.step.set('quote'); @@ -395,7 +419,12 @@ export class CalculatorPageComponent implements OnInit { this.step.set('upload'); this.result.set(null); this.requiresRecalculation.set(false); + this.itemSettingsDiffByFileName.set({}); this.baselinePrintSettings = null; + this.baselineItemSettingsByFileName = new Map< + string, + TrackedPrintSettings + >(); this.cadSessionLocked.set(false); this.orderSuccess.set(false); this.switchMode('easy'); // Reset to default and sync URL @@ -403,21 +432,16 @@ export class CalculatorPageComponent implements OnInit { private currentRequest: QuoteRequest | null = null; - onUploadPrintSettingsChange(settings: { - mode: 'easy' | 'advanced'; - material: string; - quality: string; - nozzleDiameter: number; - layerHeight: number; - infillDensity: number; - infillPattern: string; - supportEnabled: boolean; - }) { + onUploadPrintSettingsChange(_: TrackedPrintSettings) { + void _; if (!this.result()) return; - if (!this.baselinePrintSettings) return; - this.requiresRecalculation.set( - !this.sameTrackedSettings(this.baselinePrintSettings, settings), - ); + this.refreshRecalculationRequirement(); + } + + onItemSettingsDiffChange( + diffByFileName: Record, + ) { + this.itemSettingsDiffByFileName.set(diffByFileName || {}); } onConsult() { @@ -478,7 +502,12 @@ export class CalculatorPageComponent implements OnInit { this.error.set(true); this.result.set(null); this.requiresRecalculation.set(false); + this.itemSettingsDiffByFileName.set({}); this.baselinePrintSettings = null; + this.baselineItemSettingsByFileName = new Map< + string, + TrackedPrintSettings + >(); } switchMode(nextMode: 'easy' | 'advanced'): void { @@ -499,16 +528,7 @@ export class CalculatorPageComponent implements OnInit { }); } - private toTrackedSettingsFromRequest(req: QuoteRequest): { - mode: 'easy' | 'advanced'; - material: string; - quality: string; - nozzleDiameter: number; - layerHeight: number; - infillDensity: number; - infillPattern: string; - supportEnabled: boolean; - } { + private toTrackedSettingsFromRequest(req: QuoteRequest): TrackedPrintSettings { return { mode: req.mode, material: this.normalizeString(req.material || 'PLA'), @@ -521,16 +541,37 @@ export class CalculatorPageComponent implements OnInit { }; } - private toTrackedSettingsFromSession(session: any): { - mode: 'easy' | 'advanced'; - material: string; - quality: string; - nozzleDiameter: number; - layerHeight: number; - infillDensity: number; - infillPattern: string; - supportEnabled: boolean; - } { + private toTrackedSettingsFromItem( + req: QuoteRequest, + item: QuoteRequest['items'][number], + ): TrackedPrintSettings { + return { + mode: req.mode, + material: this.normalizeString(item.material || req.material || 'PLA'), + quality: this.normalizeString(item.quality || req.quality || 'standard'), + nozzleDiameter: this.normalizeNumber( + item.nozzleDiameter ?? req.nozzleDiameter, + 0.4, + 2, + ), + layerHeight: this.normalizeNumber( + item.layerHeight ?? req.layerHeight, + 0.2, + 3, + ), + infillDensity: this.normalizeNumber( + item.infillDensity ?? req.infillDensity, + 20, + 2, + ), + infillPattern: this.normalizeString( + item.infillPattern || req.infillPattern || 'grid', + ), + supportEnabled: Boolean(item.supportEnabled ?? req.supportEnabled), + }; + } + + private toTrackedSettingsFromSession(session: any): TrackedPrintSettings { const layer = this.normalizeNumber(session?.layerHeightMm, 0.2, 3); return { mode: this.mode(), @@ -545,27 +586,111 @@ export class CalculatorPageComponent implements OnInit { }; } + private toTrackedSettingsFromSessionItem( + item: any, + fallback: TrackedPrintSettings, + ): TrackedPrintSettings { + const layer = this.normalizeNumber(item?.layerHeightMm, fallback.layerHeight, 3); + return { + mode: this.mode(), + material: this.normalizeString(item?.materialCode || fallback.material), + quality: this.normalizeString( + item?.quality || + (layer >= 0.24 + ? 'draft' + : layer <= 0.12 + ? 'extra_fine' + : 'standard'), + ), + nozzleDiameter: this.normalizeNumber( + item?.nozzleDiameterMm, + fallback.nozzleDiameter, + 2, + ), + layerHeight: layer, + infillDensity: this.normalizeNumber( + item?.infillPercent, + fallback.infillDensity, + 2, + ), + infillPattern: this.normalizeString( + item?.infillPattern || fallback.infillPattern, + ), + supportEnabled: Boolean( + item?.supportsEnabled ?? fallback.supportEnabled, + ), + }; + } + + private buildBaselineMapFromRequest( + req: QuoteRequest, + ): Map { + const map = new Map(); + req.items.forEach((item) => { + map.set( + this.normalizeFileName(item.file?.name || ''), + this.toTrackedSettingsFromItem(req, item), + ); + }); + return map; + } + + private buildBaselineMapFromSession( + items: any[], + defaultSettings: TrackedPrintSettings | null, + ): Map { + const map = new Map(); + const fallback = defaultSettings ?? this.defaultTrackedSettings(); + items.forEach((item) => { + map.set( + this.normalizeFileName(item?.originalFilename || ''), + this.toTrackedSettingsFromSessionItem(item, fallback), + ); + }); + return map; + } + + private defaultTrackedSettings(): TrackedPrintSettings { + return { + mode: this.mode(), + material: 'pla', + quality: 'standard', + nozzleDiameter: 0.4, + layerHeight: 0.2, + infillDensity: 20, + infillPattern: 'grid', + supportEnabled: false, + }; + } + + private refreshRecalculationRequirement(): void { + if (!this.result()) return; + + const draft = this.uploadForm?.getCurrentRequestDraft(); + if (!draft || draft.items.length === 0) { + this.requiresRecalculation.set(false); + return; + } + + const fallback = this.baselinePrintSettings; + if (!fallback) { + this.requiresRecalculation.set(false); + return; + } + + const changed = draft.items.some((item) => { + const key = this.normalizeFileName(item.file?.name || ''); + const baseline = this.baselineItemSettingsByFileName.get(key) || fallback; + const current = this.toTrackedSettingsFromItem(draft, item); + return !this.sameTrackedSettings(baseline, current); + }); + + this.requiresRecalculation.set(changed); + } + private sameTrackedSettings( - a: { - mode: 'easy' | 'advanced'; - material: string; - quality: string; - nozzleDiameter: number; - layerHeight: number; - infillDensity: number; - infillPattern: string; - supportEnabled: boolean; - }, - b: { - mode: 'easy' | 'advanced'; - material: string; - quality: string; - nozzleDiameter: number; - layerHeight: number; - infillDensity: number; - infillPattern: string; - supportEnabled: boolean; - }, + a: TrackedPrintSettings, + b: TrackedPrintSettings, ): boolean { return ( a.mode === b.mode && @@ -583,6 +708,10 @@ export class CalculatorPageComponent implements OnInit { ); } + private normalizeFileName(fileName: string): string { + return (fileName || '').split(/[\\/]/).pop()?.trim().toLowerCase() ?? ''; + } + private normalizeString(value: string): string { return String(value || '') .trim() diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index 2c81b84..67e5618 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -63,6 +63,12 @@ {{ item.unitTime / 3600 | number: "1.1-1" }}h | {{ item.unitWeight | number: "1.0-0" }}g + @if (getItemDifferenceLabel(item.fileName)) { + | + + {{ getItemDifferenceLabel(item.fileName) }} + + } diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss index b4e94b2..8218937 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss @@ -41,6 +41,14 @@ overflow: hidden; text-overflow: ellipsis; } + +.item-settings-diff { + margin-left: 2px; + font-size: 0.78rem; + font-weight: 600; + color: #8a6d1f; + white-space: normal; +} .file-details { font-size: 0.8rem; color: var(--color-text-muted); diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index ce0df34..2aad331 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -36,6 +36,9 @@ export class QuoteResultComponent implements OnDestroy { result = input.required(); recalculationRequired = input(false); + itemSettingsDiffByFileName = input>( + {}, + ); consult = output(); proceed = output(); itemChange = output<{ @@ -185,4 +188,15 @@ export class QuoteResultComponent implements OnDestroy { this.quantityTimers.forEach((timer) => clearTimeout(timer)); this.quantityTimers.clear(); } + + getItemDifferenceLabel(fileName: string): string { + const differences = + this.itemSettingsDiffByFileName()[fileName]?.differences || []; + if (differences.length === 0) return ''; + + const materialOnly = differences.find( + (entry) => !entry.includes(':') && entry.trim().length > 0, + ); + return materialOnly || differences.join(' | '); + } } diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html index ec755cd..da2719b 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html @@ -63,7 +63,7 @@ @@ -145,6 +145,15 @@ } + @if (items().length > 1) { +
+ + +
+ } + @if (mode() === "advanced") { diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss index 16ac239..f5d4f4b 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss @@ -211,6 +211,12 @@ } } +.sync-all-row { + margin-top: var(--space-2); + margin-bottom: var(--space-4); + padding-top: 0; +} + /* Progress Bar */ .progress-container { margin-bottom: var(--space-3); diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 6b9d46c..b41ffcb 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -37,8 +37,25 @@ interface FormItem { quantity: number; color: string; filamentVariantId?: number; + printSettings: ItemPrintSettings; } +interface ItemPrintSettings { + material: string; + quality: string; + nozzleDiameter: number; + layerHeight: number; + infillDensity: number; + infillPattern: string; + supportEnabled: boolean; +} + +interface ItemSettingsDiffInfo { + differences: string[]; +} + +type ItemPrintSettingsUpdate = Partial; + @Component({ selector: 'app-upload-form', standalone: true, @@ -67,6 +84,7 @@ export class UploadFormComponent implements OnInit { fileName: string; quantity: number; }>(); + itemSettingsDiffChange = output>(); printSettingsChange = output<{ mode: 'easy' | 'advanced'; material: string; @@ -108,7 +126,7 @@ export class UploadFormComponent implements OnInit { if (matCode && this.fullMaterialOptions.length > 0) { const found = this.fullMaterialOptions.find((m) => m.code === matCode); this.currentMaterialVariants.set(found ? found.variants : []); - this.syncItemVariantSelections(); + this.syncSelectedItemVariantSelection(); } else { this.currentMaterialVariants.set([]); } @@ -137,6 +155,7 @@ export class UploadFormComponent implements OnInit { constructor() { this.form = this.fb.group({ itemsTouched: [false], // Hack to track touched state for custom items list + syncAllItems: [true], material: ['', Validators.required], quality: ['', Validators.required], items: [[]], // Track items in form for validation if needed @@ -164,7 +183,9 @@ export class UploadFormComponent implements OnInit { }); this.form.valueChanges.subscribe(() => { if (this.isPatchingSettings) return; + this.syncSelectedItemSettingsFromForm(); this.emitPrintSettingsChange(); + this.emitItemSettingsDiffChange(); }); effect(() => { @@ -337,6 +358,7 @@ export class UploadFormComponent implements OnInit { quantity: 1, color: defaultSelection.colorName, filamentVariantId: defaultSelection.filamentVariantId, + printSettings: this.getCurrentItemPrintSettings(), }); } } @@ -349,7 +371,8 @@ export class UploadFormComponent implements OnInit { this.items.update((current) => [...current, ...validItems]); this.form.get('itemsTouched')?.setValue(true); // Auto select last added - this.selectedFile.set(validItems[validItems.length - 1].file); + this.selectFile(validItems[validItems.length - 1].file); + this.emitItemSettingsDiffChange(); } } @@ -396,6 +419,7 @@ export class UploadFormComponent implements OnInit { } else { this.selectedFile.set(file); } + this.loadSelectedItemSettingsIntoForm(); } // Helper to get color of currently selected file @@ -451,17 +475,55 @@ export class UploadFormComponent implements OnInit { }; return updated; }); + this.emitItemSettingsDiffChange(); + } + + setItemPrintSettingsByIndex(index: number, update: ItemPrintSettingsUpdate) { + if (!Number.isInteger(index) || index < 0) return; + + let selectedItemUpdated = false; + this.items.update((current) => { + if (index >= current.length) return current; + const updated = [...current]; + const target = updated[index]; + if (!target) return current; + + const merged: ItemPrintSettings = { + ...target.printSettings, + ...update, + }; + + updated[index] = { + ...target, + printSettings: merged, + }; + selectedItemUpdated = target.file === this.selectedFile(); + return updated; + }); + + if (selectedItemUpdated) { + this.loadSelectedItemSettingsIntoForm(); + this.emitPrintSettingsChange(); + } + this.emitItemSettingsDiffChange(); } removeItem(index: number) { + let nextSelected: File | null = null; this.items.update((current) => { const updated = [...current]; const removed = updated.splice(index, 1)[0]; if (this.selectedFile() === removed.file) { - this.selectedFile.set(null); + nextSelected = updated.length > 0 ? updated[Math.max(0, index - 1)].file : null; } return updated; }); + if (nextSelected) { + this.selectFile(nextSelected); + } else if (this.items().length === 0) { + this.selectedFile.set(null); + } + this.emitItemSettingsDiffChange(); } setFiles(files: File[]) { @@ -474,6 +536,7 @@ export class UploadFormComponent implements OnInit { quantity: 1, color: defaultSelection.colorName, filamentVariantId: defaultSelection.filamentVariantId, + printSettings: this.getCurrentItemPrintSettings(), }); } @@ -481,7 +544,8 @@ export class UploadFormComponent implements OnInit { this.items.set(validItems); this.form.get('itemsTouched')?.setValue(true); // Auto select last added - this.selectedFile.set(validItems[validItems.length - 1].file); + this.selectFile(validItems[validItems.length - 1].file); + this.emitItemSettingsDiffChange(); } } @@ -510,25 +574,48 @@ export class UploadFormComponent implements OnInit { return { colorName: 'Black' }; } - private syncItemVariantSelections(): void { + getVariantsForItem(item: FormItem): VariantOption[] { + return this.getVariantsForMaterialCode(item.printSettings.material); + } + + private getVariantsForMaterialCode(materialCodeRaw: string): VariantOption[] { + const materialCode = String(materialCodeRaw || '').toUpperCase(); + if (!materialCode) { + return []; + } + const material = this.fullMaterialOptions.find( + (option) => String(option.code || '').toUpperCase() === materialCode, + ); + return material?.variants || []; + } + + private syncSelectedItemVariantSelection(): void { const vars = this.currentMaterialVariants(); if (!vars || vars.length === 0) { return; } + const selected = this.selectedFile(); + if (!selected) { + return; + } + const fallback = vars.find((v) => !v.isOutOfStock) || vars[0]; this.items.update((current) => current.map((item) => { + if (item.file !== selected) { + return item; + } const byId = item.filamentVariantId != null ? vars.find((v) => v.id === item.filamentVariantId) : null; const byColor = vars.find((v) => v.colorName === item.color); - const selected = byId || byColor || fallback; + const selectedVariant = byId || byColor || fallback; return { ...item, - color: selected.colorName, - filamentVariantId: selected.id, + color: selectedVariant.colorName, + filamentVariantId: selectedVariant.id, }; }), ); @@ -592,7 +679,7 @@ export class UploadFormComponent implements OnInit { ); this.submitRequest.emit({ ...this.form.getRawValue(), - items: this.items(), // Pass the items array explicitly AFTER form value to prevent overwrite + items: this.toQuoteRequestItems(), // Include per-item print settings overrides mode: this.mode(), }); } else { @@ -666,7 +753,7 @@ export class UploadFormComponent implements OnInit { if (this.items().length === 0) return null; const raw = this.form.getRawValue(); return { - items: this.items(), + items: this.toQuoteRequestItems(), material: raw.material, quality: raw.quality, notes: raw.notes, @@ -706,8 +793,248 @@ export class UploadFormComponent implements OnInit { this.printSettingsChange.emit(this.getCurrentPrintSettings()); } + private loadSelectedItemSettingsIntoForm(): void { + const selected = this.selectedFile(); + if (!selected) return; + const item = this.items().find((current) => current.file === selected); + if (!item) return; + + this.isPatchingSettings = true; + this.form.patchValue( + { + material: item.printSettings.material, + quality: item.printSettings.quality, + nozzleDiameter: item.printSettings.nozzleDiameter, + layerHeight: item.printSettings.layerHeight, + infillDensity: item.printSettings.infillDensity, + infillPattern: item.printSettings.infillPattern, + supportEnabled: item.printSettings.supportEnabled, + }, + { emitEvent: false }, + ); + this.isPatchingSettings = false; + this.updateLayerHeightOptionsForNozzle( + item.printSettings.nozzleDiameter, + true, + ); + this.updateVariants(); + } + + private syncSelectedItemSettingsFromForm(): void { + const currentSettings = this.getCurrentItemPrintSettings(); + + if (this.shouldApplySettingsToAllItems()) { + this.applyCurrentSettingsToAllItems(currentSettings); + return; + } + + const selected = this.selectedFile(); + if (!selected) return; + + this.items.update((current) => + current.map((item) => { + if (item.file !== selected) { + return item; + } + const variants = this.getVariantsForMaterialCode(currentSettings.material); + const fallback = variants.find((v) => !v.isOutOfStock) || variants[0]; + const byId = + item.filamentVariantId != null + ? variants.find((v) => v.id === item.filamentVariantId) + : null; + const byColor = variants.find((v) => v.colorName === item.color); + const selectedVariant = byId || byColor || fallback; + return { + ...item, + printSettings: { ...currentSettings }, + color: selectedVariant ? selectedVariant.colorName : item.color, + filamentVariantId: selectedVariant ? selectedVariant.id : undefined, + }; + }), + ); + } + + private emitItemSettingsDiffChange(): void { + const currentItems = this.items(); + if (currentItems.length === 0) { + this.itemSettingsDiffChange.emit({}); + return; + } + + const signatureCounts = new Map(); + currentItems.forEach((item) => { + const signature = this.settingsSignature(item.printSettings); + signatureCounts.set(signature, (signatureCounts.get(signature) || 0) + 1); + }); + + let dominantSignature = ''; + let dominantCount = 0; + signatureCounts.forEach((count, signature) => { + if (count > dominantCount) { + dominantCount = count; + dominantSignature = signature; + } + }); + + const hasDominant = dominantCount > 1; + const dominantSettings = hasDominant + ? currentItems.find( + (item) => + this.settingsSignature(item.printSettings) === dominantSignature, + )?.printSettings + : null; + + const diffByFileName: Record = {}; + currentItems.forEach((item) => { + const differences = dominantSettings + ? this.describeSettingsDifferences(dominantSettings, item.printSettings) + : []; + diffByFileName[item.file.name] = { + differences, + }; + }); + + this.itemSettingsDiffChange.emit(diffByFileName); + } + + private sameItemPrintSettings( + a: ItemPrintSettings, + b: ItemPrintSettings, + ): boolean { + return ( + a.material.trim().toUpperCase() === b.material.trim().toUpperCase() && + a.quality.trim().toLowerCase() === b.quality.trim().toLowerCase() && + Math.abs(a.nozzleDiameter - b.nozzleDiameter) < 0.0001 && + Math.abs(a.layerHeight - b.layerHeight) < 0.0001 && + Math.abs(a.infillDensity - b.infillDensity) < 0.0001 && + a.infillPattern.trim().toLowerCase() === + b.infillPattern.trim().toLowerCase() && + Boolean(a.supportEnabled) === Boolean(b.supportEnabled) + ); + } + + private settingsSignature(settings: ItemPrintSettings): string { + return JSON.stringify({ + material: settings.material.trim().toUpperCase(), + quality: settings.quality.trim().toLowerCase(), + nozzleDiameter: Number(settings.nozzleDiameter.toFixed(2)), + layerHeight: Number(settings.layerHeight.toFixed(3)), + infillDensity: Number(settings.infillDensity.toFixed(2)), + infillPattern: settings.infillPattern.trim().toLowerCase(), + supportEnabled: Boolean(settings.supportEnabled), + }); + } + + private describeSettingsDifferences( + baseline: ItemPrintSettings, + current: ItemPrintSettings, + ): string[] { + if (this.sameItemPrintSettings(baseline, current)) { + return []; + } + + const differences: string[] = []; + if (baseline.material.trim().toUpperCase() !== current.material.trim().toUpperCase()) { + differences.push(`${current.material}`); + } + if (baseline.quality.trim().toLowerCase() !== current.quality.trim().toLowerCase()) { + differences.push(`Qualita: ${current.quality}`); + } + if (Math.abs(baseline.nozzleDiameter - current.nozzleDiameter) >= 0.0001) { + differences.push(`Nozzle: ${current.nozzleDiameter.toFixed(1)} mm`); + } + if (Math.abs(baseline.layerHeight - current.layerHeight) >= 0.0001) { + differences.push(`Layer: ${current.layerHeight.toFixed(2)} mm`); + } + if (Math.abs(baseline.infillDensity - current.infillDensity) >= 0.0001) { + differences.push(`Infill: ${current.infillDensity}%`); + } + if ( + baseline.infillPattern.trim().toLowerCase() !== + current.infillPattern.trim().toLowerCase() + ) { + differences.push(`Pattern: ${current.infillPattern}`); + } + if (Boolean(baseline.supportEnabled) !== Boolean(current.supportEnabled)) { + differences.push( + `Supporti: ${current.supportEnabled ? 'attivi' : 'disattivi'}`, + ); + } + return differences; + } + + private toQuoteRequestItems(): QuoteRequest['items'] { + return this.items().map((item) => ({ + file: item.file, + quantity: item.quantity, + color: item.color, + filamentVariantId: item.filamentVariantId, + material: item.printSettings.material, + quality: item.printSettings.quality, + nozzleDiameter: item.printSettings.nozzleDiameter, + layerHeight: item.printSettings.layerHeight, + infillDensity: item.printSettings.infillDensity, + infillPattern: item.printSettings.infillPattern, + supportEnabled: item.printSettings.supportEnabled, + })); + } + + private getCurrentItemPrintSettings(): ItemPrintSettings { + const settings = this.getCurrentPrintSettings(); + return { + material: settings.material, + quality: settings.quality, + nozzleDiameter: settings.nozzleDiameter, + layerHeight: settings.layerHeight, + infillDensity: settings.infillDensity, + infillPattern: settings.infillPattern, + supportEnabled: settings.supportEnabled, + }; + } + + private shouldApplySettingsToAllItems(): boolean { + return this.parseBooleanControlValue(this.form.get('syncAllItems')?.value); + } + + private applyCurrentSettingsToAllItems(currentSettings: ItemPrintSettings): void { + this.items.update((current) => + current.map((item) => { + const variants = this.getVariantsForMaterialCode(currentSettings.material); + const fallback = variants.find((v) => !v.isOutOfStock) || variants[0]; + const byId = + item.filamentVariantId != null + ? variants.find((v) => v.id === item.filamentVariantId) + : null; + const byColor = variants.find((v) => v.colorName === item.color); + const selectedVariant = byId || byColor || fallback; + + return { + ...item, + printSettings: { ...currentSettings }, + color: selectedVariant ? selectedVariant.colorName : item.color, + filamentVariantId: selectedVariant ? selectedVariant.id : undefined, + }; + }), + ); + } + + private parseBooleanControlValue(raw: unknown): boolean { + if (this.items().length <= 1) { + return false; + } + if (raw === true || raw === 1) { + return true; + } + if (typeof raw === 'string') { + const normalized = raw.trim().toLowerCase(); + return normalized === 'true' || normalized === '1' || normalized === 'on'; + } + return false; + } + private applySettingsLock(locked: boolean): void { const controlsToLock = [ + 'syncAllItems', 'material', 'quality', 'nozzleDiameter', diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index fed6db7..152b7f2 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -10,6 +10,13 @@ export interface QuoteRequest { quantity: number; color?: string; filamentVariantId?: number; + material?: string; + quality?: string; + nozzleDiameter?: number; + layerHeight?: number; + infillDensity?: number; + infillPattern?: string; + supportEnabled?: boolean; }[]; material: string; quality: string; @@ -150,7 +157,7 @@ export class QuoteEstimatorService { if (normalized === 'draft') { return { - quality: 'extra_fine', + quality: 'draft', layerHeight: 0.24, infillDensity: 12, infillPattern: 'grid', @@ -306,23 +313,28 @@ export class QuoteEstimatorService { request.mode === 'easy' ? 'ADVANCED' : request.mode.toUpperCase(), - material: request.material, + material: item.material || request.material, filamentVariantId: item.filamentVariantId, - quality: easyPreset ? easyPreset.quality : request.quality, - supportsEnabled: request.supportEnabled, + quality: easyPreset + ? easyPreset.quality + : item.quality || request.quality, + supportsEnabled: + easyPreset != null + ? request.supportEnabled + : item.supportEnabled ?? request.supportEnabled, color: item.color || '#FFFFFF', layerHeight: easyPreset ? easyPreset.layerHeight - : request.layerHeight, + : item.layerHeight ?? request.layerHeight, infillDensity: easyPreset ? easyPreset.infillDensity - : request.infillDensity, + : item.infillDensity ?? request.infillDensity, infillPattern: easyPreset ? easyPreset.infillPattern - : request.infillPattern, + : item.infillPattern ?? request.infillPattern, nozzleDiameter: easyPreset ? easyPreset.nozzleDiameter - : request.nozzleDiameter, + : item.nozzleDiameter ?? request.nozzleDiameter, }; const settingsBlob = new Blob([JSON.stringify(settings)], { @@ -477,9 +489,7 @@ export class QuoteEstimatorService { unitTime: item.printTimeSeconds, unitWeight: item.materialGrams, quantity: item.quantity, - material: session.materialCode, // Assumption: session has one material for all? or items have it? - // Backend model QuoteSession has materialCode. - // But line items might have different colors. + material: item.materialCode || session.materialCode, color: item.colorCode, filamentVariantId: item.filamentVariantId, })), diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html index 123df04..0a85813 100644 --- a/frontend/src/app/features/checkout/checkout.component.html +++ b/frontend/src/app/features/checkout/checkout.component.html @@ -245,6 +245,10 @@ {{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }} + + {{ "CHECKOUT.MATERIAL" | translate }}: + {{ itemMaterial(item) }} + -
+
diff --git a/frontend/src/app/features/checkout/checkout.component.ts b/frontend/src/app/features/checkout/checkout.component.ts index ad95985..66e98fc 100644 --- a/frontend/src/app/features/checkout/checkout.component.ts +++ b/frontend/src/app/features/checkout/checkout.component.ts @@ -162,7 +162,11 @@ export class CheckoutComponent implements OnInit { this.quoteService.getQuoteSession(this.sessionId).subscribe({ next: (session) => { this.quoteSession.set(session); - this.loadStlPreviews(session); + if (this.isCadSessionData(session)) { + this.loadStlPreviews(session); + } else { + this.resetPreviewState(); + } console.log('Loaded session:', session); }, error: (err) => { @@ -173,7 +177,7 @@ export class CheckoutComponent implements OnInit { } isCadSession(): boolean { - return this.quoteSession()?.session?.status === 'CAD_ACTIVE'; + return this.isCadSessionData(this.quoteSession()); } cadRequestId(): string | null { @@ -188,6 +192,12 @@ export class CheckoutComponent implements OnInit { return this.quoteSession()?.cadTotalChf ?? 0; } + itemMaterial(item: any): string { + return String( + item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-', + ); + } + isStlItem(item: any): boolean { const name = String(item?.originalFilename ?? '').toLowerCase(); return name.endsWith('.stl'); @@ -241,7 +251,11 @@ export class CheckoutComponent implements OnInit { } private loadStlPreviews(session: any): void { - if (!this.sessionId || !Array.isArray(session?.items)) { + if ( + !this.sessionId || + !this.isCadSessionData(session) || + !Array.isArray(session?.items) + ) { return; } @@ -276,6 +290,17 @@ export class CheckoutComponent implements OnInit { } } + private isCadSessionData(session: any): boolean { + return session?.session?.status === 'CAD_ACTIVE'; + } + + private resetPreviewState(): void { + this.previewFiles.set({}); + this.previewLoading.set({}); + this.previewErrors.set({}); + this.closePreview(); + } + onSubmit() { if (this.checkoutForm.invalid) { return; diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index ca1275b..d5b49f4 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -402,6 +402,7 @@ "SETUP_FEE": "Einrichtungskosten", "TOTAL": "Gesamt", "QTY": "Menge", + "MATERIAL": "Material", "PER_PIECE": "pro Stück", "SHIPPING": "Versand (CH)", "PREVIEW_LOADING": "3D-Vorschau wird geladen...", diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index e464d13..b02eeb0 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -402,6 +402,7 @@ "SETUP_FEE": "Setup Fee", "TOTAL": "Total", "QTY": "Qty", + "MATERIAL": "Material", "PER_PIECE": "per piece", "SHIPPING": "Shipping", "PREVIEW_LOADING": "Loading 3D preview...", diff --git a/frontend/src/assets/i18n/fr.json b/frontend/src/assets/i18n/fr.json index 02ebd0c..7e54110 100644 --- a/frontend/src/assets/i18n/fr.json +++ b/frontend/src/assets/i18n/fr.json @@ -459,6 +459,7 @@ "SETUP_FEE": "Coût de setup", "TOTAL": "Total", "QTY": "Qté", + "MATERIAL": "Matériau", "PER_PIECE": "par pièce", "SHIPPING": "Expédition (CH)", "PREVIEW_LOADING": "Chargement de l'aperçu 3D...", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 9444cb8..4e6b8f5 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -459,6 +459,7 @@ "SETUP_FEE": "Costo di Avvio", "TOTAL": "Totale", "QTY": "Qtà", + "MATERIAL": "Materiale", "PER_PIECE": "al pezzo", "SHIPPING": "Spedizione (CH)", "PREVIEW_LOADING": "Caricamento anteprima 3D...",