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 => {
const sessionId = params['session'];
if (sessionId) {
if (sessionId && sessionId !== this.result()?.sessionId) {
this.loadSession(sessionId);
}
});
@@ -106,14 +106,14 @@ export class CalculatorPageComponent implements OnInit {
forkJoin(downloads).subscribe({
next: (results: any[]) => {
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) {
this.uploadForm.setFiles(files);
this.uploadForm.setFiles(files, colors);
this.uploadForm.patchSettings(session);
// Also restore colors?
// setFiles inits with 'Black'. We need to update them if they differ.
// items has colorCode.
// setFiles inits with correct colors now.
setTimeout(() => {
if (this.uploadForm) {
items.forEach((item, index) => {
@@ -122,7 +122,11 @@ export class CalculatorPageComponent implements OnInit {
if (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.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
if (res.sessionId) {
this.router.navigate([], {
@@ -200,10 +209,10 @@ export class CalculatorPageComponent implements OnInit {
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)
if (this.uploadForm) {
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity);
this.uploadForm.updateItemQuantityAtIndex(event.index, event.quantity);
}
// 2. Update backend session if ID exists

View File

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

View File

@@ -21,6 +21,11 @@
background: var(--color-neutral-50);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
&.has-error {
border-color: #ef4444;
background: #fef2f2;
}
}
.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-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 {
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 { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component';
import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
import { getColorHex } from '../../../../core/constants/colors.const';
@Component({
selector: 'app-quote-result',
@@ -18,11 +19,13 @@ export class QuoteResultComponent {
result = input.required<QuoteResult>();
consult = 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
items = signal<QuoteItem[]>([]);
getColorHex = getColorHex;
constructor() {
effect(() => {
// Initialize local items when result inputs change
@@ -44,7 +47,8 @@ export class QuoteResultComponent {
this.itemChange.emit({
id: this.items()[index].id,
fileName: this.items()[index].fileName,
quantity: qty
quantity: qty,
index: index
});
}

View File

@@ -25,7 +25,7 @@
<!-- New File List with Details -->
@if (items().length > 0) {
<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="card-header">
<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';
interface FormItem {
id?: string;
file: File;
quantity: number;
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) {
if (this.selectedFile() === file) {
// toggle off? no, keep active
@@ -208,11 +240,7 @@ export class UploadFormComponent implements OnInit {
let val = parseInt(input.value, 10);
if (isNaN(val) || val < 1) val = 1;
this.items.update(current => {
const updated = [...current];
updated[index] = { ...updated[index], quantity: val };
return updated;
});
this.updateItemQuantityAtIndex(index, val);
}
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[] = [];
for (const file of files) {
// Default color is Black or derive from somewhere if possible, but here we just init
validItems.push({ file, quantity: 1, color: 'Black' });
}
files.forEach((file, i) => {
const color = (colors && colors[i]) ? colors[i] : 'Black';
validItems.push({ file, quantity: 1, color: color });
});
if (validItems.length > 0) {
this.items.set(validItems);

View File

@@ -26,6 +26,7 @@ export interface QuoteItem {
quantity: number;
material?: string;
color?: string;
error?: string;
}
export interface QuoteResult {
@@ -255,11 +256,22 @@ export class QuoteEstimatorService {
let validCount = 0;
responses.forEach((res, idx) => {
if (!res || !res.success) return;
validCount++;
const quantity = res?.originalQty || request.items[idx].quantity || 1;
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 quantity = res.originalQty || 1;
items.push({
id: res.id,
@@ -270,8 +282,6 @@ export class QuoteEstimatorService {
quantity: quantity,
material: request.material,
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;

View File

@@ -40,7 +40,7 @@
<div class="form-group">
<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>
<!-- File Upload Section -->

View File

@@ -61,6 +61,9 @@
"ORDER": "Order Now",
"CONSULT": "Request Consultation",
"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",
"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.",

View File

@@ -40,6 +40,9 @@
"ORDER": "Ordina Ora",
"CONSULT": "Richiedi Consulenza",
"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",
"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.",