produzione 1 #9

Merged
JoeKung merged 135 commits from dev into main 2026-03-03 09:58:04 +01:00
5 changed files with 92 additions and 16 deletions
Showing only changes of commit 6e52988cdd - Show all commits

View File

@@ -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) {

View File

@@ -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);

View File

@@ -229,7 +229,6 @@ export class CalculatorPageComponent implements OnInit {
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);
} }
}); });
} }

View File

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

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 { 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,19 +26,37 @@ 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];
@@ -45,11 +64,29 @@ export class QuoteResultComponent {
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();
}
} }