feat(back-end and front-end): calculator improvements

This commit is contained in:
2026-03-05 21:46:11 +01:00
parent 235fe7780d
commit 7a699d2adf
15 changed files with 206 additions and 172 deletions

View File

@@ -203,6 +203,13 @@ export class CalculatorPageComponent implements OnInit {
this.uploadForm.patchSettings(session);
items.forEach((item, index) => {
// Preserve persisted quantities when restoring from session.
// Without this, setFiles() defaults every item back to 1.
this.uploadForm.updateItemQuantityByIndex(
index,
Number(item.quantity || 1),
);
const tracked = this.toTrackedSettingsFromSessionItem(
item,
this.toTrackedSettingsFromSession(session),

View File

@@ -1,17 +1,14 @@
<app-card>
<h3 class="title">{{ "CALC.RESULT" | translate }}</h3>
<!-- Summary Grid (NOW ON TOP) -->
<div class="result-grid">
<app-summary-card
class="item full-width"
[label]="'CHECKOUT.SUBTOTAL' | translate"
[large]="true"
[highlight]="true"
>
{{ costBreakdown().subtotal | currency: result().currency }}
</app-summary-card>
<app-price-breakdown
[rows]="priceBreakdownRows()"
[total]="costBreakdown().total"
[currency]="result().currency"
[totalLabelKey]="'CHECKOUT.TOTAL'"
></app-price-breakdown>
<div class="result-grid">
<app-summary-card [label]="'CALC.TIME' | translate">
{{ totals().hours }}h {{ totals().minutes }}m
</app-summary-card>
@@ -96,25 +93,6 @@
}
</div>
<div class="cost-breakdown">
<div class="cost-row">
<span>Costo di Avvio</span>
<span>{{ costBreakdown().baseSetup | currency: result().currency }}</span>
</div>
@if (costBreakdown().nozzleChange > 0) {
<div class="cost-row">
<span>Cambio Ugello</span>
<span>{{
costBreakdown().nozzleChange | currency: result().currency
}}</span>
</div>
}
<div class="cost-total">
<span>Totale</span>
<span>{{ costBreakdown().total | currency: result().currency }}</span>
</div>
</div>
<div class="actions">
<div class="actions-left">
<app-button variant="secondary" (click)="consult.emit()">

View File

@@ -122,9 +122,6 @@
gap: var(--space-4);
}
}
.full-width {
grid-column: span 2;
}
.setup-note {
text-align: center;
@@ -205,35 +202,3 @@
color: #6f5b1a;
font-size: 0.9rem;
}
.cost-breakdown {
margin-top: var(--space-2);
margin-bottom: var(--space-4);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-100);
}
.cost-row,
.cost-total {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
}
.cost-row {
color: var(--color-text);
font-size: 0.95rem;
margin-bottom: var(--space-2);
}
.cost-total {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 2px solid var(--color-border);
font-size: 1.2rem;
font-weight: 700;
color: var(--color-text);
}

View File

@@ -13,6 +13,10 @@ import { TranslateModule } from '@ngx-translate/core';
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component';
import {
PriceBreakdownComponent,
PriceBreakdownRow,
} from '../../../../shared/components/price-breakdown/price-breakdown.component';
import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
@Component({
@@ -25,6 +29,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
AppCardComponent,
AppButtonComponent,
SummaryCardComponent,
PriceBreakdownComponent,
],
templateUrl: './quote-result.component.html',
styleUrl: './quote-result.component.scss',
@@ -159,6 +164,26 @@ export class QuoteResultComponent implements OnDestroy {
};
});
priceBreakdownRows = computed<PriceBreakdownRow[]>(() => {
const breakdown = this.costBreakdown();
return [
{
labelKey: 'CHECKOUT.SUBTOTAL',
amount: breakdown.subtotal,
},
{
labelKey: 'CHECKOUT.SETUP_FEE',
amount: breakdown.baseSetup,
},
{
label: 'Cambio Ugello',
amount: breakdown.nozzleChange,
visible: breakdown.nozzleChange > 0,
},
];
});
totals = computed(() => {
const currentItems = this.items();
let time = 0;

View File

@@ -49,7 +49,7 @@
type="number"
min="1"
[value]="item.quantity"
(change)="updateItemQuantity(i, $event)"
(input)="updateItemQuantity(i, $event)"
class="qty-input"
(click)="$event.stopPropagation()"
/>

View File

@@ -138,7 +138,7 @@ export class UploadFormComponent implements OnInit {
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
nozzleDiameter: [0.4, Validators.required],
infillPattern: ['grid', Validators.required],
supportEnabled: [false],
supportEnabled: [true],
});
this.form.get('material')?.valueChanges.subscribe((value) => {
@@ -189,6 +189,19 @@ export class UploadFormComponent implements OnInit {
this.emitPrintSettingsChange();
this.emitItemSettingsDiffChange();
});
effect(() => {
if (this.mode() !== 'advanced') {
return;
}
if (this.items().length > 0 || this.sameSettingsForAll()) {
return;
}
this.sameSettingsForAll.set(true);
this.form.get('syncAllItems')?.setValue(true, { emitEvent: false });
});
}
ngOnInit() {
@@ -408,10 +421,6 @@ export class UploadFormComponent implements OnInit {
this.items.update((current) => {
if (index >= current.length) return current;
if (this.sameSettingsForAll()) {
return current.map((item) => ({ ...item, quantity: normalizedQty }));
}
return current.map((item, idx) =>
idx === index ? { ...item, quantity: normalizedQty } : item,
);
@@ -426,10 +435,6 @@ export class UploadFormComponent implements OnInit {
let matched = false;
return current.map((item) => {
if (this.sameSettingsForAll()) {
return { ...item, quantity: normalizedQty };
}
if (!matched && this.normalizeFileName(item.file.name) === targetName) {
matched = true;
return { ...item, quantity: normalizedQty };

View File

@@ -323,31 +323,13 @@
</div>
</div>
<div class="summary-totals" *ngIf="quoteSession() as session">
<div class="total-row">
<span>{{ "CHECKOUT.SUBTOTAL" | translate }}</span>
<span>{{ session.itemsTotalChf | currency: "CHF" }}</span>
</div>
<div class="total-row">
<span>{{ "CHECKOUT.SETUP_FEE" | translate }}</span>
<span>{{
(session.baseSetupCostChf ?? session.session.setupCostChf)
| currency: "CHF"
}}</span>
</div>
<div class="total-row" *ngIf="(session.nozzleChangeCostChf || 0) > 0">
<span>Cambio Ugello</span>
<span>{{ session.nozzleChangeCostChf | currency: "CHF" }}</span>
</div>
<div class="total-row">
<span>{{ "CHECKOUT.SHIPPING" | translate }}</span>
<span>{{ session.shippingCostChf | currency: "CHF" }}</span>
</div>
<div class="grand-total">
<span>{{ "CHECKOUT.TOTAL" | translate }}</span>
<span>{{ session.grandTotalChf | currency: "CHF" }}</span>
</div>
</div>
<app-price-breakdown
*ngIf="quoteSession() as session"
[rows]="checkoutPriceBreakdownRows(session)"
[total]="session.grandTotalChf || 0"
[currency]="'CHF'"
[totalLabelKey]="'CHECKOUT.TOTAL'"
></app-price-breakdown>
</app-card>
</div>
</div>

View File

@@ -355,32 +355,6 @@ app-toggle-selector.user-type-selector-compact {
padding-right: var(--space-3);
}
.summary-totals {
background: var(--color-neutral-100);
padding: var(--space-4);
border-radius: var(--radius-md);
margin-top: var(--space-6);
.total-row {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-2);
font-size: 0.95rem;
color: var(--color-text);
}
.grand-total {
display: flex;
justify-content: space-between;
color: var(--color-text);
font-weight: 700;
font-size: 1.5rem;
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 2px solid var(--color-border);
}
}
.actions {
margin-top: var(--space-8);

View File

@@ -16,6 +16,10 @@ import {
AppToggleSelectorComponent,
ToggleOption,
} from '../../shared/components/app-toggle-selector/app-toggle-selector.component';
import {
PriceBreakdownComponent,
PriceBreakdownRow,
} from '../../shared/components/price-breakdown/price-breakdown.component';
import { LanguageService } from '../../core/services/language.service';
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
import { getColorHex } from '../../core/constants/colors.const';
@@ -31,6 +35,7 @@ import { getColorHex } from '../../core/constants/colors.const';
AppButtonComponent,
AppCardComponent,
AppToggleSelectorComponent,
PriceBreakdownComponent,
StlViewerComponent,
],
templateUrl: './checkout.component.html',
@@ -197,6 +202,28 @@ export class CheckoutComponent implements OnInit {
return this.quoteSession()?.cadTotalChf ?? 0;
}
checkoutPriceBreakdownRows(session: any): PriceBreakdownRow[] {
return [
{
labelKey: 'CHECKOUT.SUBTOTAL',
amount: session?.itemsTotalChf ?? 0,
},
{
labelKey: 'CHECKOUT.SETUP_FEE',
amount: session?.baseSetupCostChf ?? session?.session?.setupCostChf ?? 0,
},
{
label: 'Cambio Ugello',
amount: session?.nozzleChangeCostChf ?? 0,
visible: (session?.nozzleChangeCostChf ?? 0) > 0,
},
{
labelKey: 'CHECKOUT.SHIPPING',
amount: session?.shippingCostChf ?? 0,
},
];
}
itemMaterial(item: any): string {
return String(
item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-',

View File

@@ -193,28 +193,12 @@
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
</div>
<div class="summary-totals">
<div class="total-row">
<span>{{ "PAYMENT.SUBTOTAL" | translate }}</span>
<span>{{ o.subtotalChf | currency: "CHF" }}</span>
</div>
<div class="total-row" *ngIf="o.cadTotalChf > 0">
<span>Servizio CAD ({{ o.cadHours || 0 }}h)</span>
<span>{{ o.cadTotalChf | currency: "CHF" }}</span>
</div>
<div class="total-row">
<span>{{ "PAYMENT.SHIPPING" | translate }}</span>
<span>{{ o.shippingCostChf | currency: "CHF" }}</span>
</div>
<div class="total-row">
<span>{{ "PAYMENT.SETUP_FEE" | translate }}</span>
<span>{{ o.setupCostChf | currency: "CHF" }}</span>
</div>
<div class="grand-total-row">
<span>{{ "PAYMENT.TOTAL" | translate }}</span>
<span>{{ o.totalChf | currency: "CHF" }}</span>
</div>
</div>
<app-price-breakdown
[rows]="orderPriceBreakdownRows(o)"
[total]="o.totalChf || 0"
[currency]="'CHF'"
[totalLabelKey]="'PAYMENT.TOTAL'"
></app-price-breakdown>
</app-card>
</div>
</div>

View File

@@ -184,31 +184,6 @@
top: var(--space-6);
}
.summary-totals {
background: var(--color-neutral-100);
padding: var(--space-6);
border-radius: var(--radius-md);
.total-row {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-2);
font-size: 0.95rem;
color: var(--color-text-muted);
}
.grand-total-row {
display: flex;
justify-content: space-between;
color: var(--color-text);
font-weight: 700;
font-size: 1.5rem;
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 2px solid var(--color-border);
}
}
.actions {
margin-top: var(--space-8);
}

View File

@@ -6,6 +6,10 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { environment } from '../../../environments/environment';
import {
PriceBreakdownComponent,
PriceBreakdownRow,
} from '../../shared/components/price-breakdown/price-breakdown.component';
@Component({
selector: 'app-order',
@@ -15,6 +19,7 @@ import { environment } from '../../../environments/environment';
AppButtonComponent,
AppCardComponent,
TranslateModule,
PriceBreakdownComponent,
],
templateUrl: './order.component.html',
styleUrl: './order.component.scss',
@@ -171,6 +176,28 @@ export class OrderComponent implements OnInit {
return this.translate.instant('ORDER.NOT_AVAILABLE');
}
orderPriceBreakdownRows(order: any): PriceBreakdownRow[] {
return [
{
labelKey: 'PAYMENT.SUBTOTAL',
amount: order?.subtotalChf ?? 0,
},
{
label: `Servizio CAD (${order?.cadHours || 0}h)`,
amount: order?.cadTotalChf ?? 0,
visible: (order?.cadTotalChf ?? 0) > 0,
},
{
labelKey: 'PAYMENT.SHIPPING',
amount: order?.shippingCostChf ?? 0,
},
{
labelKey: 'PAYMENT.SETUP_FEE',
amount: order?.setupCostChf ?? 0,
},
];
}
private extractOrderNumber(orderId: string): string {
return orderId.split('-')[0];
}