fix(back-end): file error handling
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 1m21s
Build, Test and Deploy / build-and-push (push) Successful in 21s
Build, Test and Deploy / deploy (push) Successful in 6s

This commit is contained in:
2026-02-16 14:14:47 +01:00
parent ef6a5278a7
commit 8c82470401
10 changed files with 132 additions and 47 deletions

View File

@@ -48,7 +48,7 @@ export class CalculatorPageComponent implements OnInit {
this.route.queryParams.subscribe(params => { this.route.queryParams.subscribe(params => {
const sessionId = params['session']; const sessionId = params['session'];
if (sessionId) { if (sessionId && sessionId !== this.result()?.sessionId) {
this.loadSession(sessionId); this.loadSession(sessionId);
} }
}); });
@@ -106,14 +106,14 @@ export class CalculatorPageComponent implements OnInit {
forkJoin(downloads).subscribe({ forkJoin(downloads).subscribe({
next: (results: any[]) => { next: (results: any[]) => {
const files = results.map(res => new File([res.blob], res.fileName, { type: 'application/octet-stream' })); const files = results.map(res => new File([res.blob], res.fileName, { type: 'application/octet-stream' }));
const colors = items.map(i => i.colorCode || 'Black');
if (this.uploadForm) { if (this.uploadForm) {
this.uploadForm.setFiles(files); this.uploadForm.setFiles(files, colors);
this.uploadForm.patchSettings(session); this.uploadForm.patchSettings(session);
// Also restore colors? // Also restore colors?
// setFiles inits with 'Black'. We need to update them if they differ. // setFiles inits with correct colors now.
// items has colorCode.
setTimeout(() => { setTimeout(() => {
if (this.uploadForm) { if (this.uploadForm) {
items.forEach((item, index) => { items.forEach((item, index) => {
@@ -122,7 +122,11 @@ export class CalculatorPageComponent implements OnInit {
if (item.colorCode) { if (item.colorCode) {
this.uploadForm.updateItemColor(index, item.colorCode); this.uploadForm.updateItemColor(index, item.colorCode);
} }
if (item.quantity) {
this.uploadForm.updateItemQuantityAtIndex(index, item.quantity);
}
}); });
this.uploadForm.updateItemIdsByIndex(items.map(i => i.id));
} }
}); });
} }
@@ -164,6 +168,11 @@ export class CalculatorPageComponent implements OnInit {
this.uploadProgress.set(100); this.uploadProgress.set(100);
this.step.set('quote'); this.step.set('quote');
// Sync IDs back to upload form for future updates
if (this.uploadForm) {
this.uploadForm.updateItemIdsByIndex(res.items.map(i => i.id));
}
// Update URL with session ID without reloading // Update URL with session ID without reloading
if (res.sessionId) { if (res.sessionId) {
this.router.navigate([], { this.router.navigate([], {
@@ -200,10 +209,10 @@ export class CalculatorPageComponent implements OnInit {
this.step.set('quote'); this.step.set('quote');
} }
onItemChange(event: {id?: string, fileName: string, quantity: number}) { onItemChange(event: {id?: string, fileName: string, quantity: number, index: number}) {
// 1. Update local form for consistency (UI feedback) // 1. Update local form for consistency (UI feedback)
if (this.uploadForm) { if (this.uploadForm) {
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity); this.uploadForm.updateItemQuantityAtIndex(event.index, event.quantity);
} }
// 2. Update backend session if ID exists // 2. Update backend session if ID exists

View File

@@ -35,28 +35,37 @@
<!-- Detailed Items List (NOW ON BOTTOM) --> <!-- Detailed Items List (NOW ON BOTTOM) -->
<div class="items-list"> <div class="items-list">
@for (item of items(); track item.fileName; let i = $index) { @for (item of items(); track item; let i = $index) {
<div class="item-row"> <div class="item-row" [class.has-error]="item.error">
<div class="item-info"> <div class="item-info">
<span class="file-name">{{ item.fileName }}</span> <span class="file-name">{{ item.fileName }}</span>
<span class="file-details"> @if (item.error) {
{{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g <span class="file-error">{{ 'CALC.ERROR_' + item.error | translate }}</span>
</span> } @else {
<span class="file-details">
<span class="color-badge" [title]="item.color" [style.background-color]="getColorHex(item.color!)"></span>
{{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g
</span>
}
</div> </div>
<div class="item-controls"> <div class="item-controls">
<div class="qty-control"> @if (!item.error) {
<label>Qtà:</label> <div class="qty-control">
<input <label>Qtà:</label>
type="number" <input
min="1" type="number"
[ngModel]="item.quantity" min="1"
(ngModelChange)="updateQuantity(i, $event)" [ngModel]="item.quantity"
class="qty-input"> (ngModelChange)="updateQuantity(i, $event)"
</div> class="qty-input">
<div class="item-price"> </div>
{{ (item.unitPrice * item.quantity) | currency:result().currency }} <div class="item-price">
</div> {{ (item.unitPrice * item.quantity) | currency:result().currency }}
</div>
} @else {
<div class="item-price error">-</div>
}
</div> </div>
</div> </div>
} }

View File

@@ -21,6 +21,11 @@
background: var(--color-neutral-50); background: var(--color-neutral-50);
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
&.has-error {
border-color: #ef4444;
background: #fef2f2;
}
} }
.item-info { .item-info {
@@ -31,7 +36,21 @@
} }
.file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-details { font-size: 0.8rem; color: var(--color-text-muted); } .file-details {
font-size: 0.8rem;
color: var(--color-text-muted);
display: flex;
align-items: center;
gap: var(--space-2);
}
.color-badge {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid var(--color-border);
display: inline-block;
}
.file-error { font-size: 0.8rem; color: #ef4444; font-weight: 500; }
.item-controls { .item-controls {
display: flex; display: flex;

View File

@@ -6,6 +6,7 @@ import { AppCardComponent } from '../../../../shared/components/app-card/app-car
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component'; import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component';
import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
import { getColorHex } from '../../../../core/constants/colors.const';
@Component({ @Component({
selector: 'app-quote-result', selector: 'app-quote-result',
@@ -18,11 +19,13 @@ export class QuoteResultComponent {
result = input.required<QuoteResult>(); result = input.required<QuoteResult>();
consult = output<void>(); consult = output<void>();
proceed = output<void>(); proceed = output<void>();
itemChange = output<{id?: string, fileName: string, quantity: number}>(); itemChange = output<{id?: string, fileName: string, quantity: number, index: number}>();
// Local mutable state for items to handle quantity changes // Local mutable state for items to handle quantity changes
items = signal<QuoteItem[]>([]); items = signal<QuoteItem[]>([]);
getColorHex = getColorHex;
constructor() { constructor() {
effect(() => { effect(() => {
// Initialize local items when result inputs change // Initialize local items when result inputs change
@@ -44,7 +47,8 @@ export class QuoteResultComponent {
this.itemChange.emit({ this.itemChange.emit({
id: this.items()[index].id, id: this.items()[index].id,
fileName: this.items()[index].fileName, fileName: this.items()[index].fileName,
quantity: qty quantity: qty,
index: index
}); });
} }

View File

@@ -25,7 +25,7 @@
<!-- New File List with Details --> <!-- New File List with Details -->
@if (items().length > 0) { @if (items().length > 0) {
<div class="items-grid"> <div class="items-grid">
@for (item of items(); track item.file.name; let i = $index) { @for (item of items(); track item; let i = $index) {
<div class="file-card" [class.active]="item.file === selectedFile()" (click)="selectFile(item.file)"> <div class="file-card" [class.active]="item.file === selectedFile()" (click)="selectFile(item.file)">
<div class="card-header"> <div class="card-header">
<span class="file-name" [title]="item.file.name">{{ item.file.name }}</span> <span class="file-name" [title]="item.file.name">{{ item.file.name }}</span>

View File

@@ -12,6 +12,7 @@ import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, Mat
import { getColorHex } from '../../../../core/constants/colors.const'; import { getColorHex } from '../../../../core/constants/colors.const';
interface FormItem { interface FormItem {
id?: string;
file: File; file: File;
quantity: number; quantity: number;
color: string; color: string;
@@ -178,6 +179,37 @@ export class UploadFormComponent implements OnInit {
}); });
} }
updateItemQuantityAtIndex(index: number, quantity: number) {
this.items.update(current => {
const updated = [...current];
if (updated[index]) {
updated[index] = { ...updated[index], quantity };
}
return updated;
});
}
updateItemIds(itemsWithIds: { fileName: string, id: string }[]) {
this.items.update(current => {
return current.map(item => {
const match = itemsWithIds.find(i => i.fileName === item.file.name && !i.id); // This matching is weak
// Better: matching should be based on index if we trust order
return item;
});
});
}
updateItemIdsByIndex(ids: (string | undefined)[]) {
this.items.update(current => {
return current.map((item, i) => {
if (ids[i]) {
return { ...item, id: ids[i] };
}
return item;
});
});
}
selectFile(file: File) { selectFile(file: File) {
if (this.selectedFile() === file) { if (this.selectedFile() === file) {
// toggle off? no, keep active // toggle off? no, keep active
@@ -208,11 +240,7 @@ export class UploadFormComponent implements OnInit {
let val = parseInt(input.value, 10); let val = parseInt(input.value, 10);
if (isNaN(val) || val < 1) val = 1; if (isNaN(val) || val < 1) val = 1;
this.items.update(current => { this.updateItemQuantityAtIndex(index, val);
const updated = [...current];
updated[index] = { ...updated[index], quantity: val };
return updated;
});
} }
updateItemColor(index: number, newColor: string) { updateItemColor(index: number, newColor: string) {
@@ -234,12 +262,12 @@ export class UploadFormComponent implements OnInit {
}); });
} }
setFiles(files: File[]) { setFiles(files: File[], colors?: string[]) {
const validItems: FormItem[] = []; const validItems: FormItem[] = [];
for (const file of files) { files.forEach((file, i) => {
// Default color is Black or derive from somewhere if possible, but here we just init const color = (colors && colors[i]) ? colors[i] : 'Black';
validItems.push({ file, quantity: 1, color: 'Black' }); validItems.push({ file, quantity: 1, color: color });
} });
if (validItems.length > 0) { if (validItems.length > 0) {
this.items.set(validItems); this.items.set(validItems);

View File

@@ -26,6 +26,7 @@ export interface QuoteItem {
quantity: number; quantity: number;
material?: string; material?: string;
color?: string; color?: string;
error?: string;
} }
export interface QuoteResult { export interface QuoteResult {
@@ -255,11 +256,22 @@ export class QuoteEstimatorService {
let validCount = 0; let validCount = 0;
responses.forEach((res, idx) => { responses.forEach((res, idx) => {
if (!res || !res.success) return; const quantity = res?.originalQty || request.items[idx].quantity || 1;
validCount++;
if (!res || !res.success) {
items.push({
fileName: request.items[idx].file.name,
unitPrice: 0,
unitTime: 0,
unitWeight: 0,
quantity: quantity,
error: res?.error || 'UPLOAD_FAILED'
});
return;
}
validCount++;
const unitPrice = res.unitPriceChf || 0; const unitPrice = res.unitPriceChf || 0;
const quantity = res.originalQty || 1;
items.push({ items.push({
id: res.id, id: res.id,
@@ -270,8 +282,6 @@ export class QuoteEstimatorService {
quantity: quantity, quantity: quantity,
material: request.material, material: request.material,
color: res.originalItem.color || 'Default' color: res.originalItem.color || 'Default'
// Store ID if needed for updates? QuoteItem interface might need update
// or we map it in component
}); });
grandTotal += unitPrice * quantity; grandTotal += unitPrice * quantity;

View File

@@ -37,20 +37,20 @@
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input> <app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input> <app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ 'CONTACT.LABEL_MESSAGE' | translate }}</label> <label>{{ 'CONTACT.LABEL_MESSAGE' | translate }}</label>
<textarea formControlName="message" class="form-control" rows="4"></textarea> <textarea formControlName="message" class="form-control" rows="10"></textarea>
</div> </div>
<!-- File Upload Section --> <!-- File Upload Section -->
<div class="form-group"> <div class="form-group">
<label>{{ 'CONTACT.UPLOAD_LABEL' | translate }}</label> <label>{{ 'CONTACT.UPLOAD_LABEL' | translate }}</label>
<p class="hint">{{ 'CONTACT.UPLOAD_HINT' | translate }}</p> <p class="hint">{{ 'CONTACT.UPLOAD_HINT' | translate }}</p>
<div class="drop-zone" (click)="fileInput.click()" <div class="drop-zone" (click)="fileInput.click()"
(dragover)="onDragOver($event)" (drop)="onDrop($event)"> (dragover)="onDragOver($event)" (drop)="onDrop($event)">
<input #fileInput type="file" multiple (change)="onFileSelected($event)" hidden <input #fileInput type="file" multiple (change)="onFileSelected($event)" hidden
accept=".jpg,.jpeg,.png,.pdf,.stl,.step,.stp,.3mf,.obj"> accept=".jpg,.jpeg,.png,.pdf,.stl,.step,.stp,.3mf,.obj">
<p>{{ 'CONTACT.DROP_FILES' | translate }}</p> <p>{{ 'CONTACT.DROP_FILES' | translate }}</p>
</div> </div>

View File

@@ -61,6 +61,9 @@
"ORDER": "Order Now", "ORDER": "Order Now",
"CONSULT": "Request Consultation", "CONSULT": "Request Consultation",
"ERROR_GENERIC": "An error occurred while calculating the quote.", "ERROR_GENERIC": "An error occurred while calculating the quote.",
"ERROR_UPLOAD_FAILED": "Calculation failed",
"ERROR_VIRUS_DETECTED": "File removed (virus detected)",
"ERROR_SLICING_FAILED": "Slicing error (complex geometry?)",
"NEW_QUOTE": "Calculate New Quote", "NEW_QUOTE": "Calculate New Quote",
"ORDER_SUCCESS_TITLE": "Order Submitted Successfully", "ORDER_SUCCESS_TITLE": "Order Submitted Successfully",
"ORDER_SUCCESS_DESC": "We have received your order details. You will receive a confirmation email shortly with the payment details.", "ORDER_SUCCESS_DESC": "We have received your order details. You will receive a confirmation email shortly with the payment details.",

View File

@@ -40,6 +40,9 @@
"ORDER": "Ordina Ora", "ORDER": "Ordina Ora",
"CONSULT": "Richiedi Consulenza", "CONSULT": "Richiedi Consulenza",
"ERROR_GENERIC": "Si è verificato un errore durante il calcolo del preventivo.", "ERROR_GENERIC": "Si è verificato un errore durante il calcolo del preventivo.",
"ERROR_UPLOAD_FAILED": "Calcolo fallito",
"ERROR_VIRUS_DETECTED": "File rimosso (virus rilevato)",
"ERROR_SLICING_FAILED": "Errore slicing (geometria complessa?)",
"NEW_QUOTE": "Calcola Nuovo Preventivo", "NEW_QUOTE": "Calcola Nuovo Preventivo",
"ORDER_SUCCESS_TITLE": "Ordine Inviato con Successo", "ORDER_SUCCESS_TITLE": "Ordine Inviato con Successo",
"ORDER_SUCCESS_DESC": "Abbiamo ricevuto i dettagli del tuo ordine. Riceverai a breve una email di conferma con i dettagli per il pagamento.", "ORDER_SUCCESS_DESC": "Abbiamo ricevuto i dettagli del tuo ordine. Riceverai a breve una email di conferma con i dettagli per il pagamento.",