feat(back-end and front-end) cad bill with order

This commit is contained in:
2026-03-04 12:03:09 +01:00
parent 0f2f2bc7a9
commit 1b3f0b16ff
43 changed files with 1594 additions and 150 deletions

View File

@@ -28,6 +28,13 @@ const appChildRoutes: Routes = [
loadChildren: () =>
import('./features/contact/contact.routes').then((m) => m.CONTACT_ROUTES),
},
{
path: 'checkout/cad',
loadComponent: () =>
import('./features/checkout/checkout.component').then(
(m) => m.CheckoutComponent,
),
},
{
path: 'checkout',
loadComponent: () =>

View File

@@ -53,7 +53,7 @@
}
</select>
<div class="icon-placeholder">
<div class="icon-placeholder" routerLink="/admin">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { LanguageService } from '../services/language.service';
import {routes} from '../../app.routes';
@Component({
selector: 'app-navbar',
@@ -37,4 +38,6 @@ export class NavbarComponent {
closeMenu() {
this.isMenuOpen = false;
}
protected readonly routes = routes;
}

View File

@@ -50,6 +50,13 @@ export const ADMIN_ROUTES: Routes = [
(m) => m.AdminSessionsComponent,
),
},
{
path: 'cad-invoices',
loadComponent: () =>
import('./pages/admin-cad-invoices.component').then(
(m) => m.AdminCadInvoicesComponent,
),
},
],
},
];

View File

@@ -0,0 +1,128 @@
<section class="cad-page">
<header class="page-header">
<div>
<h1>Fatture CAD</h1>
<p>
Crea un checkout CAD partendo da una sessione esistente (opzionale) e
gestisci lo stato fino all'ordine.
</p>
</div>
<button type="button" (click)="loadCadInvoices()" [disabled]="loading">
Aggiorna
</button>
</header>
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<section class="create-box">
<h2>Crea nuova fattura CAD</h2>
<div class="form-grid">
<label>
<span>ID Sessione (opzionale)</span>
<input
[(ngModel)]="form.sessionId"
placeholder="UUID sessione quote"
type="text"
/>
</label>
<label>
<span>ID Richiesta Contatto (opzionale)</span>
<input
[(ngModel)]="form.sourceRequestId"
placeholder="UUID richiesta contatto"
type="text"
/>
</label>
<label>
<span>Ore CAD</span>
<input [(ngModel)]="form.cadHours" min="0.1" step="0.1" type="number" />
</label>
<label>
<span>Tariffa CAD CHF/h (opzionale)</span>
<input
[(ngModel)]="form.cadHourlyRateChf"
placeholder="Se vuoto usa pricing policy attiva"
min="0"
step="0.05"
type="number"
/>
</label>
<label class="notes-field">
<span>Nota (opzionale)</span>
<textarea
[(ngModel)]="form.notes"
placeholder="Nota visibile nel checkout CAD (es. dettagli lavorazione)"
rows="3"
></textarea>
</label>
</div>
<div class="create-actions">
<button type="button" (click)="createCadInvoice()" [disabled]="creating">
{{ creating ? "Creazione..." : "Crea link checkout CAD" }}
</button>
</div>
</section>
<section class="table-wrap" *ngIf="!loading; else loadingTpl">
<table>
<thead>
<tr>
<th>Sessione</th>
<th>Richiesta</th>
<th>Ore CAD</th>
<th>Tariffa</th>
<th>Totale CAD</th>
<th>Totale ordine</th>
<th>Stato sessione</th>
<th>Nota</th>
<th>Ordine</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of invoices">
<td [title]="row.sessionId">{{ row.sessionId | slice: 0 : 8 }}</td>
<td>{{ row.sourceRequestId || "-" }}</td>
<td>{{ row.cadHours }}</td>
<td>{{ row.cadHourlyRateChf | currency: "CHF" }}</td>
<td>{{ row.cadTotalChf | currency: "CHF" }}</td>
<td>{{ row.grandTotalChf | currency: "CHF" }}</td>
<td>{{ row.sessionStatus }}</td>
<td class="notes-cell" [title]="row.notes || ''">{{ row.notes || "-" }}</td>
<td>
<span *ngIf="row.convertedOrderId; else noOrder">
{{ row.convertedOrderId | slice: 0 : 8 }} ({{
row.convertedOrderStatus || "-"
}})
</span>
<ng-template #noOrder>-</ng-template>
</td>
<td class="actions">
<button type="button" class="ghost" (click)="openCheckout(row.checkoutPath)">
Apri checkout
</button>
<button type="button" class="ghost" (click)="copyCheckout(row.checkoutPath)">
Copia link
</button>
<button
type="button"
class="ghost"
*ngIf="row.convertedOrderId"
(click)="downloadInvoice(row.convertedOrderId)"
>
Scarica fattura
</button>
</td>
</tr>
<tr *ngIf="invoices.length === 0">
<td colspan="10">Nessuna fattura CAD trovata.</td>
</tr>
</tbody>
</table>
</section>
</section>
<ng-template #loadingTpl>
<p>Caricamento fatture CAD...</p>
</ng-template>

View File

@@ -0,0 +1,140 @@
.cad-page {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.page-header {
display: flex;
justify-content: space-between;
gap: var(--space-4);
align-items: flex-start;
}
.page-header h1 {
margin: 0;
}
.page-header p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
button {
border: 0;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
background: var(--color-brand);
color: var(--color-neutral-900);
font-weight: 600;
cursor: pointer;
}
button:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.create-box {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
padding: var(--space-4);
}
.create-box h2 {
margin-top: 0;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
}
.form-grid label {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.form-grid span {
font-size: 0.9rem;
color: var(--color-text-muted);
}
.form-grid input {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: var(--space-2);
}
.notes-field {
grid-column: 1 / -1;
}
.form-grid textarea {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: var(--space-2);
resize: vertical;
}
.create-actions {
margin-top: var(--space-3);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 1100px;
}
th,
td {
border-bottom: 1px solid var(--color-border);
padding: var(--space-2);
text-align: left;
}
.notes-cell {
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.actions {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.ghost {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.error {
color: var(--color-danger-500);
}
.success {
color: var(--color-success-500);
}
@media (max-width: 880px) {
.page-header {
flex-direction: column;
align-items: stretch;
}
.form-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,157 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
AdminCadInvoice,
AdminOperationsService,
} from '../services/admin-operations.service';
import { AdminOrdersService } from '../services/admin-orders.service';
@Component({
selector: 'app-admin-cad-invoices',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './admin-cad-invoices.component.html',
styleUrl: './admin-cad-invoices.component.scss',
})
export class AdminCadInvoicesComponent implements OnInit {
private readonly adminOperationsService = inject(AdminOperationsService);
private readonly adminOrdersService = inject(AdminOrdersService);
invoices: AdminCadInvoice[] = [];
loading = false;
creating = false;
errorMessage: string | null = null;
successMessage: string | null = null;
form = {
sessionId: '',
sourceRequestId: '',
cadHours: 1,
cadHourlyRateChf: '',
notes: '',
};
ngOnInit(): void {
this.loadCadInvoices();
}
loadCadInvoices(): void {
this.loading = true;
this.errorMessage = null;
this.adminOperationsService.listCadInvoices().subscribe({
next: (rows) => {
this.invoices = rows;
this.loading = false;
},
error: () => {
this.loading = false;
this.errorMessage = 'Impossibile caricare le fatture CAD.';
},
});
}
createCadInvoice(): void {
if (this.creating) {
return;
}
const cadHours = Number(this.form.cadHours);
if (!Number.isFinite(cadHours) || cadHours <= 0) {
this.errorMessage = 'Inserisci ore CAD valide (> 0).';
return;
}
this.creating = true;
this.errorMessage = null;
this.successMessage = null;
let payload: {
sessionId?: string;
sourceRequestId?: string;
cadHours: number;
cadHourlyRateChf?: number;
notes?: string;
};
try {
const sessionIdRaw = String(this.form.sessionId ?? '').trim();
const sourceRequestIdRaw = String(this.form.sourceRequestId ?? '').trim();
const cadRateRaw = String(this.form.cadHourlyRateChf ?? '').trim();
const notesRaw = String(this.form.notes ?? '').trim();
payload = {
sessionId: sessionIdRaw || undefined,
sourceRequestId: sourceRequestIdRaw || undefined,
cadHours,
cadHourlyRateChf:
cadRateRaw.length > 0 && Number.isFinite(Number(cadRateRaw))
? Number(cadRateRaw)
: undefined,
notes: notesRaw.length > 0 ? notesRaw : undefined,
};
} catch {
this.creating = false;
this.errorMessage = 'Valori form non validi.';
return;
}
this.adminOperationsService.createCadInvoice(payload).subscribe({
next: (created) => {
this.creating = false;
this.successMessage = `Fattura CAD pronta. Sessione: ${created.sessionId}`;
this.loadCadInvoices();
},
error: (err) => {
this.creating = false;
this.errorMessage =
err?.error?.message || 'Creazione fattura CAD non riuscita.';
},
});
}
openCheckout(path: string): void {
const url = this.toCheckoutUrl(path);
window.open(url, '_blank');
}
copyCheckout(path: string): void {
const url = this.toCheckoutUrl(path);
navigator.clipboard?.writeText(url);
this.successMessage = 'Link checkout CAD copiato negli appunti.';
}
downloadInvoice(orderId?: string): void {
if (!orderId) return;
this.adminOrdersService.downloadOrderInvoice(orderId).subscribe({
next: (blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `fattura-cad-${orderId}.pdf`;
a.click();
window.URL.revokeObjectURL(url);
},
error: () => {
this.errorMessage = 'Download fattura non riuscito.';
},
});
}
private toCheckoutUrl(path: string): string {
const safePath = path.startsWith('/') ? path : `/${path}`;
const lang = this.resolveLang();
return `${window.location.origin}/${lang}${safePath}`;
}
private resolveLang(): string {
const firstSegment = window.location.pathname
.split('/')
.filter(Boolean)
.shift();
if (firstSegment && ['it', 'en', 'de', 'fr'].includes(firstSegment)) {
return firstSegment;
}
return 'it';
}
}

View File

@@ -199,18 +199,20 @@ tbody tr.selected {
.request-id {
margin: var(--space-2) 0 0;
display: flex;
align-items: center;
align-items: flex-start;
flex-wrap: wrap;
gap: 8px;
font-size: 0.8rem;
color: var(--color-text-muted);
}
.request-id code {
display: inline-block;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
max-width: 100%;
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
color: var(--color-text);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);

View File

@@ -47,6 +47,13 @@
>
{{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }}
</button>
<button
type="button"
class="btn-secondary"
(click)="copySessionUuid(session.id)"
>
Copia UUID
</button>
<button
type="button"
class="btn-danger"
@@ -78,6 +85,11 @@
"
class="detail-box"
>
<div class="detail-session-id">
<strong>UUID sessione:</strong>
<code>{{ detail.session.id }}</code>
</div>
<div class="detail-summary">
<div>
<strong>Elementi:</strong> {{ detail.items.length }}

View File

@@ -103,6 +103,22 @@ td {
padding: var(--space-4);
}
.detail-session-id {
margin-bottom: var(--space-3);
display: grid;
gap: 4px;
}
.detail-session-id code {
display: block;
max-width: 100%;
padding: var(--space-2);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-neutral-100);
overflow-wrap: anywhere;
}
.detail-summary {
display: flex;
flex-wrap: wrap;

View File

@@ -135,6 +135,42 @@ export class AdminSessionsComponent implements OnInit {
return `${hours}h ${minutes}m`;
}
copySessionUuid(sessionId: string): void {
if (!sessionId) {
return;
}
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(sessionId).then(
() => {
this.errorMessage = null;
this.successMessage = 'UUID sessione copiato.';
},
() => {
this.errorMessage = 'Impossibile copiare UUID sessione.';
},
);
return;
}
// Fallback for older browsers.
const textarea = document.createElement('textarea');
textarea.value = sessionId;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
this.errorMessage = null;
this.successMessage = 'UUID sessione copiato.';
} catch {
this.errorMessage = 'Impossibile copiare UUID sessione.';
} finally {
document.body.removeChild(textarea);
}
}
private extractErrorMessage(error: unknown, fallback: string): string {
const err = error as { error?: { message?: string } };
return err?.error?.message || fallback;

View File

@@ -16,6 +16,9 @@
>Richieste contatto</a
>
<a routerLink="sessions" routerLinkActive="active">Sessioni</a>
<a routerLink="cad-invoices" routerLinkActive="active"
>Fatture CAD</a
>
</nav>
</div>

View File

@@ -115,6 +115,10 @@ export interface AdminQuoteSession {
createdAt: string;
expiresAt: string;
convertedOrderId?: string;
sourceRequestId?: string;
cadHours?: number;
cadHourlyRateChf?: number;
cadTotalChf?: number;
}
export interface AdminQuoteSessionDetailItem {
@@ -136,14 +140,45 @@ export interface AdminQuoteSessionDetail {
setupCostChf?: number;
supportsEnabled?: boolean;
notes?: string;
sourceRequestId?: string;
cadHours?: number;
cadHourlyRateChf?: number;
};
items: AdminQuoteSessionDetailItem[];
printItemsTotalChf: number;
cadTotalChf: number;
itemsTotalChf: number;
shippingCostChf: number;
globalMachineCostChf: number;
grandTotalChf: number;
}
export interface AdminCreateCadInvoicePayload {
sessionId?: string;
sourceRequestId?: string;
cadHours: number;
cadHourlyRateChf?: number;
notes?: string;
}
export interface AdminCadInvoice {
sessionId: string;
sessionStatus: string;
sourceRequestId?: string;
cadHours: number;
cadHourlyRateChf: number;
cadTotalChf: number;
printItemsTotalChf: number;
setupCostChf: number;
shippingCostChf: number;
grandTotalChf: number;
convertedOrderId?: string;
convertedOrderStatus?: string;
checkoutPath: string;
notes?: string;
createdAt: string;
}
@Injectable({
providedIn: 'root',
})
@@ -279,4 +314,20 @@ export class AdminOperationsService {
{ withCredentials: true },
);
}
listCadInvoices(): Observable<AdminCadInvoice[]> {
return this.http.get<AdminCadInvoice[]>(`${this.baseUrl}/cad-invoices`, {
withCredentials: true,
});
}
createCadInvoice(
payload: AdminCreateCadInvoicePayload,
): Observable<AdminCadInvoice> {
return this.http.post<AdminCadInvoice>(
`${this.baseUrl}/cad-invoices`,
payload,
{ withCredentials: true },
);
}
}

View File

@@ -24,6 +24,11 @@ export interface AdminOrder {
customerEmail: string;
totalChf: number;
createdAt: string;
isCadOrder?: boolean;
sourceRequestId?: string;
cadHours?: number;
cadHourlyRateChf?: number;
cadTotalChf?: number;
printMaterialCode?: string;
printNozzleDiameterMm?: number;
printLayerHeightMm?: number;

View File

@@ -23,14 +23,16 @@
<div
class="mode-option"
[class.active]="mode() === 'easy'"
(click)="mode.set('easy')"
[class.disabled]="cadSessionLocked()"
(click)="!cadSessionLocked() && mode.set('easy')"
>
{{ "CALC.MODE_EASY" | translate }}
</div>
<div
class="mode-option"
[class.active]="mode() === 'advanced'"
(click)="mode.set('advanced')"
[class.disabled]="cadSessionLocked()"
(click)="!cadSessionLocked() && mode.set('advanced')"
>
{{ "CALC.MODE_ADVANCED" | translate }}
</div>
@@ -39,6 +41,7 @@
<app-upload-form
#uploadForm
[mode]="mode()"
[lockedSettings]="cadSessionLocked()"
[loading]="loading()"
[uploadProgress]="uploadProgress()"
(submitRequest)="onCalculate($event)"

View File

@@ -80,6 +80,11 @@
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.benefits {

View File

@@ -48,6 +48,7 @@ export class CalculatorPageComponent implements OnInit {
loading = signal(false);
uploadProgress = signal(0);
result = signal<QuoteResult | null>(null);
cadSessionLocked = signal(false);
error = signal<boolean>(false);
errorKey = signal<string>('CALC.ERROR_GENERIC');
isZeroQuoteError = computed(
@@ -100,6 +101,8 @@ export class CalculatorPageComponent implements OnInit {
this.error.set(false);
this.errorKey.set('CALC.ERROR_GENERIC');
this.result.set(result);
const isCadSession = data?.session?.status === 'CAD_ACTIVE';
this.cadSessionLocked.set(isCadSession);
this.step.set('quote');
// 2. Determine Mode (Heuristic)
@@ -206,6 +209,7 @@ export class CalculatorPageComponent implements OnInit {
this.error.set(false);
this.errorKey.set('CALC.ERROR_GENERIC');
this.result.set(null);
this.cadSessionLocked.set(false);
this.orderSuccess.set(false);
// Auto-scroll on mobile to make analysis visible
@@ -270,10 +274,10 @@ export class CalculatorPageComponent implements OnInit {
onProceed() {
const res = this.result();
if (res && res.sessionId) {
this.router.navigate(
['/', this.languageService.selectedLang(), 'checkout'],
{ queryParams: { session: res.sessionId } },
);
const segments = this.cadSessionLocked()
? ['/', this.languageService.selectedLang(), 'checkout', 'cad']
: ['/', this.languageService.selectedLang(), 'checkout'];
this.router.navigate(segments, { queryParams: { session: res.sessionId } });
} else {
console.error('No session ID found in quote result');
// Fallback or error handling
@@ -343,6 +347,7 @@ export class CalculatorPageComponent implements OnInit {
onNewQuote() {
this.step.set('upload');
this.result.set(null);
this.cadSessionLocked.set(false);
this.orderSuccess.set(false);
this.mode.set('easy'); // Reset to default
}

View File

@@ -28,6 +28,12 @@
: { cost: (result().setupCost | currency: result().currency) }
}}</small
><br />
@if ((result().cadTotal || 0) > 0) {
<small class="shipping-note" style="color: #666">
Servizio CAD: {{ result().cadTotal | currency: result().currency }}
</small>
<br />
}
<small class="shipping-note" style="color: #666">{{
"CALC.SHIPPING_NOTE" | translate
}}</small>

View File

@@ -120,8 +120,9 @@ export class QuoteResultComponent implements OnDestroy {
totals = computed(() => {
const currentItems = this.items();
const setup = this.result().setupCost;
const cad = this.result().cadTotal || 0;
let price = setup;
let price = setup + cad;
let time = 0;
let weight = 0;

View File

@@ -118,6 +118,12 @@
</div>
<div class="grid">
@if (lockedSettings()) {
<p class="upload-privacy-note">
Parametri stampa bloccati per sessione CAD: materiale, nozzle, layer,
infill e supporti sono definiti dal back-office.
</p>
}
<app-select
formControlName="material"
[label]="'CALC.MATERIAL' | translate"

View File

@@ -5,6 +5,7 @@ import {
signal,
OnInit,
inject,
effect,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import {
@@ -57,6 +58,7 @@ interface FormItem {
})
export class UploadFormComponent implements OnInit {
mode = input<'easy' | 'advanced'>('easy');
lockedSettings = input<boolean>(false);
loading = input<boolean>(false);
uploadProgress = input<number>(0);
submitRequest = output<QuoteRequest>();
@@ -139,6 +141,10 @@ export class UploadFormComponent implements OnInit {
if (this.mode() !== 'easy' || this.isPatchingSettings) return;
this.applyAdvancedPresetFromQuality(quality);
});
effect(() => {
this.applySettingsLock(this.lockedSettings());
});
}
private applyAdvancedPresetFromQuality(quality: string | null | undefined) {
@@ -520,7 +526,7 @@ export class UploadFormComponent implements OnInit {
this.form.value,
);
this.submitRequest.emit({
...this.form.value,
...this.form.getRawValue(),
items: this.items(), // Pass the items array explicitly AFTER form value to prevent overwrite
mode: this.mode(),
});
@@ -554,4 +560,26 @@ export class UploadFormComponent implements OnInit {
private normalizeFileName(fileName: string): string {
return (fileName || '').split(/[\\/]/).pop()?.trim().toLowerCase() ?? '';
}
private applySettingsLock(locked: boolean): void {
const controlsToLock = [
'material',
'quality',
'nozzleDiameter',
'infillPattern',
'layerHeight',
'infillDensity',
'supportEnabled',
];
controlsToLock.forEach((name) => {
const control = this.form.get(name);
if (!control) return;
if (locked) {
control.disable({ emitEvent: false });
} else {
control.enable({ emitEvent: false });
}
});
}
}

View File

@@ -39,6 +39,8 @@ export interface QuoteResult {
items: QuoteItem[];
setupCost: number;
globalMachineCost: number;
cadHours?: number;
cadTotal?: number;
currency: string;
totalPrice: number;
totalTimeHours: number;
@@ -463,6 +465,8 @@ export class QuoteEstimatorService {
})),
setupCost: session.setupCostChf || 0,
globalMachineCost: sessionData.globalMachineCostChf || 0,
cadHours: session.cadHours || 0,
cadTotal: sessionData.cadTotalChf || 0,
currency: 'CHF', // Fixed for now
totalPrice:
(sessionData.itemsTotalChf || 0) +

View File

@@ -1,6 +1,12 @@
<div class="checkout-page">
<div class="container hero">
<h1 class="section-title">{{ "CHECKOUT.TITLE" | translate }}</h1>
<p class="cad-subtitle" *ngIf="isCadSession()">
Servizio CAD
<ng-container *ngIf="cadRequestId()">
riferito alla richiesta contatto #{{ cadRequestId() }}
</ng-container>
</p>
</div>
<div class="container">
@@ -260,6 +266,17 @@
</small>
</div>
</div>
<div class="summary-item cad-summary-item" *ngIf="cadTotal() > 0">
<div class="item-details">
<span class="item-name">Servizio CAD</span>
<div class="item-specs-sub">{{ cadHours() }}h</div>
</div>
<div class="item-price">
<span class="item-total-price">
{{ cadTotal() | currency: "CHF" }}
</span>
</div>
</div>
</div>
<div class="summary-totals" *ngIf="quoteSession() as session">

View File

@@ -8,6 +8,11 @@
}
}
.cad-subtitle {
margin: 0;
color: var(--color-text-muted);
}
.checkout-layout {
display: grid;
grid-template-columns: 1fr 420px;
@@ -260,6 +265,13 @@ app-toggle-selector.user-type-selector-compact {
}
}
.cad-summary-item {
background: var(--color-neutral-100);
border-radius: var(--radius-sm);
padding-left: var(--space-3);
padding-right: var(--space-3);
}
.summary-totals {
background: var(--color-neutral-100);
padding: var(--space-4);

View File

@@ -162,6 +162,22 @@ export class CheckoutComponent implements OnInit {
});
}
isCadSession(): boolean {
return this.quoteSession()?.session?.status === 'CAD_ACTIVE';
}
cadRequestId(): string | null {
return this.quoteSession()?.session?.sourceRequestId ?? null;
}
cadHours(): number {
return this.quoteSession()?.session?.cadHours ?? 0;
}
cadTotal(): number {
return this.quoteSession()?.cadTotalChf ?? 0;
}
onSubmit() {
if (this.checkoutForm.invalid) {
return;

View File

@@ -198,6 +198,10 @@
<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>