diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index 72b1ed3..07b1912 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -3,7 +3,7 @@

{{ 'CALC.SUBTITLE' | translate }}

@if (error()) { - {{ 'CALC.ERROR_GENERIC' | translate }} + {{ errorKey() | translate }} } diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index a997649..9bef380 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -27,6 +27,7 @@ export class CalculatorPageComponent implements OnInit { uploadProgress = signal(0); result = signal(null); error = signal(false); + errorKey = signal('CALC.ERROR_GENERIC'); orderSuccess = signal(false); @@ -64,6 +65,14 @@ export class CalculatorPageComponent implements OnInit { next: (data) => { // 1. Map to Result const result = this.estimator.mapSessionToQuoteResult(data); + if (this.isInvalidQuote(result)) { + this.setQuoteError('CALC.ERROR_ZERO_PRICE'); + this.loading.set(false); + return; + } + + this.error.set(false); + this.errorKey.set('CALC.ERROR_GENERIC'); this.result.set(result); this.step.set('quote'); @@ -79,7 +88,7 @@ export class CalculatorPageComponent implements OnInit { }, error: (err) => { console.error('Failed to load session', err); - this.error.set(true); + this.setQuoteError('CALC.ERROR_GENERIC'); this.loading.set(false); } }); @@ -146,6 +155,7 @@ export class CalculatorPageComponent implements OnInit { this.loading.set(true); this.uploadProgress.set(0); this.error.set(false); + this.errorKey.set('CALC.ERROR_GENERIC'); this.result.set(null); this.orderSuccess.set(false); @@ -163,6 +173,14 @@ export class CalculatorPageComponent implements OnInit { } else { // It's the result const res = event as QuoteResult; + if (this.isInvalidQuote(res)) { + this.setQuoteError('CALC.ERROR_ZERO_PRICE'); + this.loading.set(false); + return; + } + + this.error.set(false); + this.errorKey.set('CALC.ERROR_GENERIC'); this.result.set(res); this.loading.set(false); this.uploadProgress.set(100); @@ -180,7 +198,7 @@ export class CalculatorPageComponent implements OnInit { } }, error: () => { - this.error.set(true); + this.setQuoteError('CALC.ERROR_GENERIC'); this.loading.set(false); } }); @@ -216,10 +234,19 @@ export class CalculatorPageComponent implements OnInit { next: () => { // 3. Fetch the updated session totals from the backend this.estimator.getQuoteSession(currentSessionId).subscribe({ - next: (sessionData) => { + next: (sessionData) => { const newResult = this.estimator.mapSessionToQuoteResult(sessionData); // Preserve notes newResult.notes = this.result()?.notes; + + if (this.isInvalidQuote(newResult)) { + this.setQuoteError('CALC.ERROR_ZERO_PRICE'); + this.loading.set(false); + return; + } + + this.error.set(false); + this.errorKey.set('CALC.ERROR_GENERIC'); this.result.set(newResult); this.loading.set(false); }, @@ -282,4 +309,14 @@ export class CalculatorPageComponent implements OnInit { this.router.navigate(['/contact']); } + + private isInvalidQuote(result: QuoteResult): boolean { + return !Number.isFinite(result.totalPrice) || result.totalPrice <= 0; + } + + private setQuoteError(key: string): void { + this.errorKey.set(key); + this.error.set(true); + this.result.set(null); + } } diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index b1af8bc..89a4a4d 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -90,7 +90,8 @@ "PROCESSING": "Verarbeitung...", "NOTES_PLACEHOLDER": "Spezifische Anweisungen...", "SETUP_NOTE": "* Beinhaltet {{cost}} als Einrichtungskosten", - "SHIPPING_NOTE": "** Versandkosten ausgeschlossen, werden im nächsten Schritt berechnet" + "SHIPPING_NOTE": "** Versandkosten ausgeschlossen, werden im nächsten Schritt berechnet", + "ERROR_ZERO_PRICE": "Etwas ist schiefgelaufen. Versuche ein anderes Format oder kontaktiere uns." }, "SHOP": { "TITLE": "Technische Lösungen", diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index cb443ef..9d7f4e1 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -90,7 +90,8 @@ "PROCESSING": "Processing...", "NOTES_PLACEHOLDER": "Specific instructions...", "SETUP_NOTE": "* Includes {{cost}} as setup cost", - "SHIPPING_NOTE": "** Shipping costs excluded, calculated at the next step" + "SHIPPING_NOTE": "** Shipping costs excluded, calculated at the next step", + "ERROR_ZERO_PRICE": "Something went wrong. Try another format or contact us." }, "SHOP": { "TITLE": "Technical solutions", diff --git a/frontend/src/assets/i18n/fr.json b/frontend/src/assets/i18n/fr.json index 1dcf7f7..00dffc8 100644 --- a/frontend/src/assets/i18n/fr.json +++ b/frontend/src/assets/i18n/fr.json @@ -115,7 +115,8 @@ "REMOVE_FILE": "Supprimer le fichier", "FALLBACK_MATERIAL": "PLA (fallback)", "FALLBACK_QUALITY_STANDARD": "Standard", - "ERR_FILE_TOO_LARGE": "Certains fichiers dépassent la limite de 200MB et n'ont pas été ajoutés." + "ERR_FILE_TOO_LARGE": "Certains fichiers dépassent la limite de 200MB et n'ont pas été ajoutés.", + "ERROR_ZERO_PRICE": "Quelque chose s'est mal passé. Essayez un autre format ou contactez-nous." }, "QUOTE": { "PROCEED_ORDER": "Procéder à la commande", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 5e89136..b17a3b5 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -115,7 +115,8 @@ "REMOVE_FILE": "Rimuovi file", "FALLBACK_MATERIAL": "PLA (fallback)", "FALLBACK_QUALITY_STANDARD": "Standard", - "ERR_FILE_TOO_LARGE": "Alcuni file superano il limite di 200MB e non sono stati aggiunti." + "ERR_FILE_TOO_LARGE": "Alcuni file superano il limite di 200MB e non sono stati aggiunti.", + "ERROR_ZERO_PRICE": "Qualcosa è andato storto. Prova con un altro formato oppure contattaci." }, "QUOTE": { "PROCEED_ORDER": "Procedi con l'ordine",