Merge pull request 'feat/calculator-options' (#28) from feat/calculator-options into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 36s
PR Checks / prettier-autofix (pull_request) Successful in 12s
Build and Deploy / test-frontend (push) Successful in 1m8s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 25s
Build and Deploy / build-and-push (push) Successful in 25s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / deploy (push) Successful in 13s
All checks were successful
Build and Deploy / test-backend (push) Successful in 36s
PR Checks / prettier-autofix (pull_request) Successful in 12s
Build and Deploy / test-frontend (push) Successful in 1m8s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 25s
Build and Deploy / build-and-push (push) Successful in 25s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / deploy (push) Successful in 13s
Reviewed-on: #28
This commit was merged in pull request #28.
This commit is contained in:
@@ -203,6 +203,13 @@ export class CalculatorPageComponent implements OnInit {
|
|||||||
this.uploadForm.patchSettings(session);
|
this.uploadForm.patchSettings(session);
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
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(
|
const tracked = this.toTrackedSettingsFromSessionItem(
|
||||||
item,
|
item,
|
||||||
this.toTrackedSettingsFromSession(session),
|
this.toTrackedSettingsFromSession(session),
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
<app-card>
|
<app-card>
|
||||||
<h3 class="title">{{ "CALC.RESULT" | translate }}</h3>
|
<h3 class="title">{{ "CALC.RESULT" | translate }}</h3>
|
||||||
|
|
||||||
<!-- Summary Grid (NOW ON TOP) -->
|
<app-price-breakdown
|
||||||
<div class="result-grid">
|
[rows]="priceBreakdownRows()"
|
||||||
<app-summary-card
|
[total]="costBreakdown().total"
|
||||||
class="item full-width"
|
[currency]="result().currency"
|
||||||
[label]="'CHECKOUT.SUBTOTAL' | translate"
|
[totalLabelKey]="'CHECKOUT.TOTAL'"
|
||||||
[large]="true"
|
></app-price-breakdown>
|
||||||
[highlight]="true"
|
|
||||||
>
|
|
||||||
{{ costBreakdown().subtotal | currency: result().currency }}
|
|
||||||
</app-summary-card>
|
|
||||||
|
|
||||||
|
<div class="result-grid">
|
||||||
<app-summary-card [label]="'CALC.TIME' | translate">
|
<app-summary-card [label]="'CALC.TIME' | translate">
|
||||||
{{ totals().hours }}h {{ totals().minutes }}m
|
{{ totals().hours }}h {{ totals().minutes }}m
|
||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
@@ -96,25 +93,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</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">
|
||||||
<div class="actions-left">
|
<div class="actions-left">
|
||||||
<app-button variant="secondary" (click)="consult.emit()">
|
<app-button variant="secondary" (click)="consult.emit()">
|
||||||
|
|||||||
@@ -122,9 +122,6 @@
|
|||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.full-width {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-note {
|
.setup-note {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -205,35 +202,3 @@
|
|||||||
color: #6f5b1a;
|
color: #6f5b1a;
|
||||||
font-size: 0.9rem;
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
|
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
|
||||||
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||||
import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.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';
|
import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -25,6 +29,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
|
|||||||
AppCardComponent,
|
AppCardComponent,
|
||||||
AppButtonComponent,
|
AppButtonComponent,
|
||||||
SummaryCardComponent,
|
SummaryCardComponent,
|
||||||
|
PriceBreakdownComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './quote-result.component.html',
|
templateUrl: './quote-result.component.html',
|
||||||
styleUrl: './quote-result.component.scss',
|
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(() => {
|
totals = computed(() => {
|
||||||
const currentItems = this.items();
|
const currentItems = this.items();
|
||||||
let time = 0;
|
let time = 0;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
[value]="item.quantity"
|
[value]="item.quantity"
|
||||||
(change)="updateItemQuantity(i, $event)"
|
(input)="updateItemQuantity(i, $event)"
|
||||||
class="qty-input"
|
class="qty-input"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
||||||
nozzleDiameter: [0.4, Validators.required],
|
nozzleDiameter: [0.4, Validators.required],
|
||||||
infillPattern: ['grid', Validators.required],
|
infillPattern: ['grid', Validators.required],
|
||||||
supportEnabled: [false],
|
supportEnabled: [true],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.form.get('material')?.valueChanges.subscribe((value) => {
|
this.form.get('material')?.valueChanges.subscribe((value) => {
|
||||||
@@ -189,6 +189,19 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.emitPrintSettingsChange();
|
this.emitPrintSettingsChange();
|
||||||
this.emitItemSettingsDiffChange();
|
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() {
|
ngOnInit() {
|
||||||
@@ -408,10 +421,6 @@ export class UploadFormComponent implements OnInit {
|
|||||||
this.items.update((current) => {
|
this.items.update((current) => {
|
||||||
if (index >= current.length) return current;
|
if (index >= current.length) return current;
|
||||||
|
|
||||||
if (this.sameSettingsForAll()) {
|
|
||||||
return current.map((item) => ({ ...item, quantity: normalizedQty }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return current.map((item, idx) =>
|
return current.map((item, idx) =>
|
||||||
idx === index ? { ...item, quantity: normalizedQty } : item,
|
idx === index ? { ...item, quantity: normalizedQty } : item,
|
||||||
);
|
);
|
||||||
@@ -426,10 +435,6 @@ export class UploadFormComponent implements OnInit {
|
|||||||
let matched = false;
|
let matched = false;
|
||||||
|
|
||||||
return current.map((item) => {
|
return current.map((item) => {
|
||||||
if (this.sameSettingsForAll()) {
|
|
||||||
return { ...item, quantity: normalizedQty };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched && this.normalizeFileName(item.file.name) === targetName) {
|
if (!matched && this.normalizeFileName(item.file.name) === targetName) {
|
||||||
matched = true;
|
matched = true;
|
||||||
return { ...item, quantity: normalizedQty };
|
return { ...item, quantity: normalizedQty };
|
||||||
|
|||||||
@@ -323,34 +323,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary-totals" *ngIf="quoteSession() as session">
|
<app-price-breakdown
|
||||||
<div class="total-row">
|
*ngIf="quoteSession() as session"
|
||||||
<span>{{ "CHECKOUT.SUBTOTAL" | translate }}</span>
|
[rows]="checkoutPriceBreakdownRows(session)"
|
||||||
<span>{{ session.itemsTotalChf | currency: "CHF" }}</span>
|
[total]="session.grandTotalChf || 0"
|
||||||
</div>
|
[currency]="'CHF'"
|
||||||
<div class="total-row">
|
[totalLabelKey]="'CHECKOUT.TOTAL'"
|
||||||
<span>{{ "CHECKOUT.SETUP_FEE" | translate }}</span>
|
></app-price-breakdown>
|
||||||
<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-card>
|
</app-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -355,32 +355,6 @@ app-toggle-selector.user-type-selector-compact {
|
|||||||
padding-right: var(--space-3);
|
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 {
|
.actions {
|
||||||
margin-top: var(--space-8);
|
margin-top: var(--space-8);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
AppToggleSelectorComponent,
|
AppToggleSelectorComponent,
|
||||||
ToggleOption,
|
ToggleOption,
|
||||||
} from '../../shared/components/app-toggle-selector/app-toggle-selector.component';
|
} 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 { LanguageService } from '../../core/services/language.service';
|
||||||
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
||||||
import { getColorHex } from '../../core/constants/colors.const';
|
import { getColorHex } from '../../core/constants/colors.const';
|
||||||
@@ -31,6 +35,7 @@ import { getColorHex } from '../../core/constants/colors.const';
|
|||||||
AppButtonComponent,
|
AppButtonComponent,
|
||||||
AppCardComponent,
|
AppCardComponent,
|
||||||
AppToggleSelectorComponent,
|
AppToggleSelectorComponent,
|
||||||
|
PriceBreakdownComponent,
|
||||||
StlViewerComponent,
|
StlViewerComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './checkout.component.html',
|
templateUrl: './checkout.component.html',
|
||||||
@@ -197,6 +202,29 @@ export class CheckoutComponent implements OnInit {
|
|||||||
return this.quoteSession()?.cadTotalChf ?? 0;
|
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 {
|
itemMaterial(item: any): string {
|
||||||
return String(
|
return String(
|
||||||
item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-',
|
item?.materialCode ?? this.quoteSession()?.session?.materialCode ?? '-',
|
||||||
|
|||||||
@@ -193,28 +193,12 @@
|
|||||||
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
|
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary-totals">
|
<app-price-breakdown
|
||||||
<div class="total-row">
|
[rows]="orderPriceBreakdownRows(o)"
|
||||||
<span>{{ "PAYMENT.SUBTOTAL" | translate }}</span>
|
[total]="o.totalChf || 0"
|
||||||
<span>{{ o.subtotalChf | currency: "CHF" }}</span>
|
[currency]="'CHF'"
|
||||||
</div>
|
[totalLabelKey]="'PAYMENT.TOTAL'"
|
||||||
<div class="total-row" *ngIf="o.cadTotalChf > 0">
|
></app-price-breakdown>
|
||||||
<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-card>
|
</app-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -184,31 +184,6 @@
|
|||||||
top: var(--space-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 {
|
.actions {
|
||||||
margin-top: var(--space-8);
|
margin-top: var(--space-8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp
|
|||||||
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import {
|
||||||
|
PriceBreakdownComponent,
|
||||||
|
PriceBreakdownRow,
|
||||||
|
} from '../../shared/components/price-breakdown/price-breakdown.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-order',
|
selector: 'app-order',
|
||||||
@@ -15,6 +19,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
AppButtonComponent,
|
AppButtonComponent,
|
||||||
AppCardComponent,
|
AppCardComponent,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
|
PriceBreakdownComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './order.component.html',
|
templateUrl: './order.component.html',
|
||||||
styleUrl: './order.component.scss',
|
styleUrl: './order.component.scss',
|
||||||
@@ -171,6 +176,28 @@ export class OrderComponent implements OnInit {
|
|||||||
return this.translate.instant('ORDER.NOT_AVAILABLE');
|
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 {
|
private extractOrderNumber(orderId: string): string {
|
||||||
return orderId.split('-')[0];
|
return orderId.split('-')[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="price-breakdown">
|
||||||
|
<div class="price-row" *ngFor="let row of visibleRows()">
|
||||||
|
<span>
|
||||||
|
<ng-container *ngIf="row.label; else translatedRowLabel">
|
||||||
|
{{ row.label }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #translatedRowLabel>
|
||||||
|
{{ row.labelKey ? (row.labelKey | translate) : "" }}
|
||||||
|
</ng-template>
|
||||||
|
</span>
|
||||||
|
<span>{{ row.amount | currency: currency() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price-total">
|
||||||
|
<span>
|
||||||
|
<ng-container *ngIf="totalLabel(); else translatedTotalLabel">
|
||||||
|
{{ totalLabel() }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #translatedTotalLabel>
|
||||||
|
{{ totalLabelKey() ? (totalLabelKey() | translate) : "" }}
|
||||||
|
</ng-template>
|
||||||
|
</span>
|
||||||
|
<span>{{ total() | currency: currency() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.price-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row,
|
||||||
|
.price-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, computed, input } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
export interface PriceBreakdownRow {
|
||||||
|
label?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
amount: number;
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-price-breakdown',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TranslateModule],
|
||||||
|
templateUrl: './price-breakdown.component.html',
|
||||||
|
styleUrl: './price-breakdown.component.scss',
|
||||||
|
})
|
||||||
|
export class PriceBreakdownComponent {
|
||||||
|
rows = input<PriceBreakdownRow[]>([]);
|
||||||
|
total = input.required<number>();
|
||||||
|
currency = input<string>('CHF');
|
||||||
|
totalLabel = input<string>('');
|
||||||
|
totalLabelKey = input<string>('');
|
||||||
|
|
||||||
|
visibleRows = computed(() =>
|
||||||
|
this.rows().filter((row) => row.visible === undefined || row.visible),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user