produzione 1 #9
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
[max]="maxInputQuantity"
|
||||
[ngModel]="item.quantity"
|
||||
(ngModelChange)="updateQuantity(i, $event)"
|
||||
(blur)="flushQuantityUpdate(i)"
|
||||
class="qty-input">
|
||||
</div>
|
||||
<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 { 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user