diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index 9a57fc7..f40412b 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -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 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 f33740d..5a660a4 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 @@ -35,28 +35,37 @@
- @for (item of items(); track item.fileName; let i = $index) { -
+ @for (item of items(); track item; let i = $index) { +
{{ item.fileName }} - - {{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g - + @if (item.error) { + {{ 'CALC.ERROR_' + item.error | translate }} + } @else { + + + {{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g + + }
-
- - -
-
- {{ (item.unitPrice * item.quantity) | currency:result().currency }} -
+ @if (!item.error) { +
+ + +
+
+ {{ (item.unitPrice * item.quantity) | currency:result().currency }} +
+ } @else { +
-
+ }
} 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 c9f6973..afc1e0d 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 @@ -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; 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 8d3e193..e507e43 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 @@ -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(); consult = output(); proceed = output(); - 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([]); + 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 }); } 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 eba5371..c9775f4 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 @@ -25,7 +25,7 @@ @if (items().length > 0) {
- @for (item of items(); track item.file.name; let i = $index) { + @for (item of items(); track item; let i = $index) {
{{ item.file.name }} 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 9258067..d407b69 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 @@ -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); 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 93a5f80..35c8cdf 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -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; diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html index b0f7bed..3119dab 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -37,20 +37,20 @@
- +
- +

{{ 'CONTACT.UPLOAD_HINT' | translate }}

- -
-

{{ 'CONTACT.DROP_FILES' | translate }}

diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 22dbba7..2b5a432 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -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.", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 9597eed..b8d0504 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -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.",