feat(front-end): delay on update
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 36s
Build, Test and Deploy / build-and-push (push) Successful in 22s
Build, Test and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-02-27 11:50:17 +01:00
parent 2e701d5597
commit 6e52988cdd
5 changed files with 92 additions and 16 deletions

View File

@@ -19,7 +19,7 @@
<div class="actions">
<select
class="lang-switch"
[value]="langService.currentLang()"
[value]="langService.selectedLang()"
(change)="onLanguageChange($event)"
[attr.aria-label]="'NAV.LANGUAGE_SELECTOR' | translate">
@for (option of languageOptions; track option.value) {

View File

@@ -15,6 +15,12 @@ export class LanguageService {
) {
this.translate.addLangs(this.supportedLangs);
this.translate.setDefaultLang('it');
this.translate.onLangChange.subscribe(event => {
const lang = typeof event.lang === 'string' ? event.lang.toLowerCase() : null;
if (this.isSupportedLang(lang) && lang !== this.currentLang()) {
this.currentLang.set(lang);
}
});
const initialTree = this.router.parseUrl(this.router.url);
const initialSegments = this.getPrimarySegments(initialTree);
@@ -55,6 +61,13 @@ export class LanguageService {
this.navigateIfChanged(currentTree, targetSegments);
}
selectedLang(): 'it' | 'en' | 'de' | 'fr' {
const activeLang = typeof this.translate.currentLang === 'string'
? this.translate.currentLang.toLowerCase()
: null;
return this.isSupportedLang(activeLang) ? activeLang : this.currentLang();
}
private ensureLanguageInPath(urlTree: UrlTree): void {
const segments = this.getPrimarySegments(urlTree);

View File

@@ -229,7 +229,6 @@ export class CalculatorPageComponent implements OnInit {
const currentSessionId = this.result()?.sessionId;
if (!currentSessionId) return;
this.loading.set(true);
this.estimator.updateLineItem(event.id, { quantity: event.quantity }).subscribe({
next: () => {
// 3. Fetch the updated session totals from the backend
@@ -241,24 +240,20 @@ export class CalculatorPageComponent implements OnInit {
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);
},
error: (err) => {
console.error('Failed to refresh session totals', err);
this.loading.set(false);
}
});
},
error: (err) => {
console.error('Failed to update line item', err);
this.loading.set(false);
}
});
}

View File

@@ -54,6 +54,7 @@
[max]="maxInputQuantity"
[ngModel]="item.quantity"
(ngModelChange)="updateQuantity(i, $event)"
(blur)="flushQuantityUpdate(i)"
class="qty-input">
</div>
<div class="item-price">

View File

@@ -1,4 +1,4 @@
import { Component, input, output, signal, computed, effect } from '@angular/core';
import { Component, OnDestroy, input, output, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
@@ -14,9 +14,10 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
templateUrl: './quote-result.component.html',
styleUrl: './quote-result.component.scss'
})
export class QuoteResultComponent {
export class QuoteResultComponent implements OnDestroy {
readonly maxInputQuantity = 500;
readonly directOrderLimit = 100;
readonly quantityAutoRefreshMs = 2000;
result = input.required<QuoteResult>();
consult = output<void>();
@@ -25,19 +26,37 @@ export class QuoteResultComponent {
// Local mutable state for items to handle quantity changes
items = signal<QuoteItem[]>([]);
private lastSentQuantities = new Map<string, number>();
private quantityTimers = new Map<string, ReturnType<typeof setTimeout>>();
constructor() {
effect(() => {
this.clearAllQuantityTimers();
// Initialize local items when result inputs change
// We map to new objects to avoid mutating the input directly if it was a reference
this.items.set(this.result().items.map(i => ({...i})));
const nextItems = this.result().items.map(i => ({...i}));
this.items.set(nextItems);
this.lastSentQuantities.clear();
nextItems.forEach(item => {
const key = item.id ?? item.fileName;
this.lastSentQuantities.set(key, item.quantity);
});
}, { allowSignalWrites: true });
}
ngOnDestroy(): void {
this.clearAllQuantityTimers();
}
updateQuantity(index: number, newQty: number | string) {
const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty;
if (qty < 1 || isNaN(qty)) return;
const normalizedQty = Math.min(qty, this.maxInputQuantity);
const normalizedQty = this.normalizeQuantity(newQty);
if (normalizedQty === null) return;
const item = this.items()[index];
if (!item) return;
const key = item.id ?? item.fileName;
this.items.update(current => {
const updated = [...current];
@@ -45,11 +64,29 @@ export class QuoteResultComponent {
return updated;
});
this.scheduleQuantityRefresh(index, key);
}
flushQuantityUpdate(index: number): void {
const item = this.items()[index];
if (!item) return;
const key = item.id ?? item.fileName;
this.clearQuantityRefreshTimer(key);
const normalizedQty = this.normalizeQuantity(item.quantity);
if (normalizedQty === null) return;
if (this.lastSentQuantities.get(key) === normalizedQty) {
return;
}
this.itemChange.emit({
id: this.items()[index].id,
fileName: this.items()[index].fileName,
id: item.id,
fileName: item.fileName,
quantity: normalizedQty
});
this.lastSentQuantities.set(key, normalizedQty);
}
hasQuantityOverLimit = computed(() => this.items().some(item => item.quantity > this.directOrderLimit));
@@ -78,4 +115,34 @@ export class QuoteResultComponent {
weight: Math.ceil(weight)
};
});
private normalizeQuantity(newQty: number | string): number | null {
const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty;
if (!Number.isFinite(qty) || qty < 1) {
return null;
}
return Math.min(qty, this.maxInputQuantity);
}
private scheduleQuantityRefresh(index: number, key: string): void {
this.clearQuantityRefreshTimer(key);
const timer = setTimeout(() => {
this.quantityTimers.delete(key);
this.flushQuantityUpdate(index);
}, this.quantityAutoRefreshMs);
this.quantityTimers.set(key, timer);
}
private clearQuantityRefreshTimer(key: string): void {
const timer = this.quantityTimers.get(key);
if (!timer) return;
clearTimeout(timer);
this.quantityTimers.delete(key);
}
private clearAllQuantityTimers(): void {
this.quantityTimers.forEach(timer => clearTimeout(timer));
this.quantityTimers.clear();
}
}