feat(front-end): delay on update
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<select
|
<select
|
||||||
class="lang-switch"
|
class="lang-switch"
|
||||||
[value]="langService.currentLang()"
|
[value]="langService.selectedLang()"
|
||||||
(change)="onLanguageChange($event)"
|
(change)="onLanguageChange($event)"
|
||||||
[attr.aria-label]="'NAV.LANGUAGE_SELECTOR' | translate">
|
[attr.aria-label]="'NAV.LANGUAGE_SELECTOR' | translate">
|
||||||
@for (option of languageOptions; track option.value) {
|
@for (option of languageOptions; track option.value) {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export class LanguageService {
|
|||||||
) {
|
) {
|
||||||
this.translate.addLangs(this.supportedLangs);
|
this.translate.addLangs(this.supportedLangs);
|
||||||
this.translate.setDefaultLang('it');
|
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 initialTree = this.router.parseUrl(this.router.url);
|
||||||
const initialSegments = this.getPrimarySegments(initialTree);
|
const initialSegments = this.getPrimarySegments(initialTree);
|
||||||
@@ -55,6 +61,13 @@ export class LanguageService {
|
|||||||
this.navigateIfChanged(currentTree, targetSegments);
|
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 {
|
private ensureLanguageInPath(urlTree: UrlTree): void {
|
||||||
const segments = this.getPrimarySegments(urlTree);
|
const segments = this.getPrimarySegments(urlTree);
|
||||||
|
|
||||||
|
|||||||
@@ -228,8 +228,7 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
if (event.id) {
|
if (event.id) {
|
||||||
const currentSessionId = this.result()?.sessionId;
|
const currentSessionId = this.result()?.sessionId;
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
|
||||||
this.estimator.updateLineItem(event.id, { quantity: event.quantity }).subscribe({
|
this.estimator.updateLineItem(event.id, { quantity: event.quantity }).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// 3. Fetch the updated session totals from the backend
|
// 3. Fetch the updated session totals from the backend
|
||||||
@@ -241,24 +240,20 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.isInvalidQuote(newResult)) {
|
if (this.isInvalidQuote(newResult)) {
|
||||||
this.setQuoteError('CALC.ERROR_ZERO_PRICE');
|
this.setQuoteError('CALC.ERROR_ZERO_PRICE');
|
||||||
this.loading.set(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.error.set(false);
|
this.error.set(false);
|
||||||
this.errorKey.set('CALC.ERROR_GENERIC');
|
this.errorKey.set('CALC.ERROR_GENERIC');
|
||||||
this.result.set(newResult);
|
this.result.set(newResult);
|
||||||
this.loading.set(false);
|
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Failed to refresh session totals', err);
|
console.error('Failed to refresh session totals', err);
|
||||||
this.loading.set(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Failed to update line item', err);
|
console.error('Failed to update line item', err);
|
||||||
this.loading.set(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
[max]="maxInputQuantity"
|
[max]="maxInputQuantity"
|
||||||
[ngModel]="item.quantity"
|
[ngModel]="item.quantity"
|
||||||
(ngModelChange)="updateQuantity(i, $event)"
|
(ngModelChange)="updateQuantity(i, $event)"
|
||||||
|
(blur)="flushQuantityUpdate(i)"
|
||||||
class="qty-input">
|
class="qty-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="item-price">
|
<div class="item-price">
|
||||||
|
|||||||
@@ -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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -14,9 +14,10 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
|
|||||||
templateUrl: './quote-result.component.html',
|
templateUrl: './quote-result.component.html',
|
||||||
styleUrl: './quote-result.component.scss'
|
styleUrl: './quote-result.component.scss'
|
||||||
})
|
})
|
||||||
export class QuoteResultComponent {
|
export class QuoteResultComponent implements OnDestroy {
|
||||||
readonly maxInputQuantity = 500;
|
readonly maxInputQuantity = 500;
|
||||||
readonly directOrderLimit = 100;
|
readonly directOrderLimit = 100;
|
||||||
|
readonly quantityAutoRefreshMs = 2000;
|
||||||
|
|
||||||
result = input.required<QuoteResult>();
|
result = input.required<QuoteResult>();
|
||||||
consult = output<void>();
|
consult = output<void>();
|
||||||
@@ -25,31 +26,67 @@ export class QuoteResultComponent {
|
|||||||
|
|
||||||
// Local mutable state for items to handle quantity changes
|
// Local mutable state for items to handle quantity changes
|
||||||
items = signal<QuoteItem[]>([]);
|
items = signal<QuoteItem[]>([]);
|
||||||
|
private lastSentQuantities = new Map<string, number>();
|
||||||
|
private quantityTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
this.clearAllQuantityTimers();
|
||||||
|
|
||||||
// Initialize local items when result inputs change
|
// Initialize local items when result inputs change
|
||||||
// We map to new objects to avoid mutating the input directly if it was a reference
|
// 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 });
|
}, { allowSignalWrites: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.clearAllQuantityTimers();
|
||||||
|
}
|
||||||
|
|
||||||
updateQuantity(index: number, newQty: number | string) {
|
updateQuantity(index: number, newQty: number | string) {
|
||||||
const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty;
|
const normalizedQty = this.normalizeQuantity(newQty);
|
||||||
if (qty < 1 || isNaN(qty)) return;
|
if (normalizedQty === null) return;
|
||||||
const normalizedQty = Math.min(qty, this.maxInputQuantity);
|
|
||||||
|
const item = this.items()[index];
|
||||||
|
if (!item) return;
|
||||||
|
const key = item.id ?? item.fileName;
|
||||||
|
|
||||||
this.items.update(current => {
|
this.items.update(current => {
|
||||||
const updated = [...current];
|
const updated = [...current];
|
||||||
updated[index] = { ...updated[index], quantity: normalizedQty };
|
updated[index] = { ...updated[index], quantity: normalizedQty };
|
||||||
return updated;
|
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({
|
this.itemChange.emit({
|
||||||
id: this.items()[index].id,
|
id: item.id,
|
||||||
fileName: this.items()[index].fileName,
|
fileName: item.fileName,
|
||||||
quantity: normalizedQty
|
quantity: normalizedQty
|
||||||
});
|
});
|
||||||
|
this.lastSentQuantities.set(key, normalizedQty);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasQuantityOverLimit = computed(() => this.items().some(item => item.quantity > this.directOrderLimit));
|
hasQuantityOverLimit = computed(() => this.items().some(item => item.quantity > this.directOrderLimit));
|
||||||
@@ -78,4 +115,34 @@ export class QuoteResultComponent {
|
|||||||
weight: Math.ceil(weight)
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user