fix(back-end): file error handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user