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

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