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",