Compare commits
2 Commits
78af87ac3c
...
44d99b0a68
| Author | SHA1 | Date | |
|---|---|---|---|
| 44d99b0a68 | |||
| 83b3008234 |
@@ -1,64 +1,79 @@
|
|||||||
<div class="container hero">
|
<div class="container hero">
|
||||||
<h1>{{ 'CALC.TITLE' | translate }}</h1>
|
<h1>{{ 'CALC.TITLE' | translate }}</h1>
|
||||||
<p class="subtitle">{{ 'CALC.SUBTITLE' | translate }}</p>
|
<p class="subtitle">{{ 'CALC.SUBTITLE' | translate }}</p>
|
||||||
|
|
||||||
|
@if (orderSuccess()) {
|
||||||
|
<app-alert type="success">{{ 'USER_DETAILS.ORDER_SUCCESS' | translate }}</app-alert>
|
||||||
|
}
|
||||||
|
@if (error()) {
|
||||||
|
<app-alert type="error">{{ 'CALC.ERROR_GENERIC' | translate }}</app-alert>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container content-grid">
|
@if (step() === 'details' && result()) {
|
||||||
<!-- Left Column: Input -->
|
<div class="container">
|
||||||
<div class="col-input">
|
<app-user-details
|
||||||
<app-card>
|
[quote]="result()!"
|
||||||
<div class="mode-selector">
|
(submitOrder)="onSubmitOrder($event)"
|
||||||
<div class="mode-option"
|
(cancel)="onCancelDetails()">
|
||||||
[class.active]="mode() === 'easy'"
|
</app-user-details>
|
||||||
(click)="mode.set('easy')">
|
</div>
|
||||||
{{ 'CALC.MODE_EASY' | translate }}
|
} @else {
|
||||||
</div>
|
<div class="container content-grid">
|
||||||
<div class="mode-option"
|
<!-- Left Column: Input -->
|
||||||
[class.active]="mode() === 'advanced'"
|
<div class="col-input">
|
||||||
(click)="mode.set('advanced')">
|
<app-card>
|
||||||
{{ 'CALC.MODE_ADVANCED' | translate }}
|
<div class="mode-selector">
|
||||||
</div>
|
<div class="mode-option"
|
||||||
</div>
|
[class.active]="mode() === 'easy'"
|
||||||
|
(click)="mode.set('easy')">
|
||||||
<app-upload-form
|
{{ 'CALC.MODE_EASY' | translate }}
|
||||||
#uploadForm
|
|
||||||
[mode]="mode()"
|
|
||||||
[loading]="loading()"
|
|
||||||
[uploadProgress]="uploadProgress()"
|
|
||||||
(submitRequest)="onCalculate($event)"
|
|
||||||
></app-upload-form>
|
|
||||||
</app-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Result or Info -->
|
|
||||||
<div class="col-result" #resultCol>
|
|
||||||
@if (error()) {
|
|
||||||
<app-alert type="error">Si è verificato un errore durante il calcolo del preventivo.</app-alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (loading()) {
|
|
||||||
<app-card class="loading-state">
|
|
||||||
<div class="loader-content">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<h3 class="loading-title">Analisi in corso...</h3>
|
|
||||||
<p class="loading-text">Stiamo analizzando la geometria e calcolando il percorso utensile.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mode-option"
|
||||||
|
[class.active]="mode() === 'advanced'"
|
||||||
|
(click)="mode.set('advanced')">
|
||||||
|
{{ 'CALC.MODE_ADVANCED' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-upload-form
|
||||||
|
#uploadForm
|
||||||
|
[mode]="mode()"
|
||||||
|
[loading]="loading()"
|
||||||
|
[uploadProgress]="uploadProgress()"
|
||||||
|
(submitRequest)="onCalculate($event)"
|
||||||
|
></app-upload-form>
|
||||||
</app-card>
|
</app-card>
|
||||||
} @else if (result()) {
|
</div>
|
||||||
<app-quote-result
|
|
||||||
[result]="result()!"
|
<!-- Right Column: Result or Info -->
|
||||||
(consult)="onConsult()"
|
<div class="col-result" #resultCol>
|
||||||
(itemChange)="uploadForm.updateItemQuantityByName($event.fileName, $event.quantity)"
|
|
||||||
></app-quote-result>
|
@if (loading()) {
|
||||||
} @else {
|
<app-card class="loading-state">
|
||||||
<app-card>
|
<div class="loader-content">
|
||||||
<h3>{{ 'CALC.BENEFITS_TITLE' | translate }}</h3>
|
<div class="spinner"></div>
|
||||||
<ul class="benefits">
|
<h3 class="loading-title">Analisi in corso...</h3>
|
||||||
<li>{{ 'CALC.BENEFITS_1' | translate }}</li>
|
<p class="loading-text">Stiamo analizzando la geometria e calcolando il percorso utensile.</p>
|
||||||
<li>{{ 'CALC.BENEFITS_2' | translate }}</li>
|
</div>
|
||||||
<li>{{ 'CALC.BENEFITS_3' | translate }}</li>
|
</app-card>
|
||||||
</ul>
|
} @else if (result()) {
|
||||||
</app-card>
|
<app-quote-result
|
||||||
}
|
[result]="result()!"
|
||||||
</div>
|
(consult)="onConsult()"
|
||||||
</div>
|
(proceed)="onProceed()"
|
||||||
|
(itemChange)="uploadForm.updateItemQuantityByName($event.fileName, $event.quantity)"
|
||||||
|
></app-quote-result>
|
||||||
|
} @else {
|
||||||
|
<app-card>
|
||||||
|
<h3>{{ 'CALC.BENEFITS_TITLE' | translate }}</h3>
|
||||||
|
<ul class="benefits">
|
||||||
|
<li>{{ 'CALC.BENEFITS_1' | translate }}</li>
|
||||||
|
<li>{{ 'CALC.BENEFITS_2' | translate }}</li>
|
||||||
|
<li>{{ 'CALC.BENEFITS_3' | translate }}</li>
|
||||||
|
</ul>
|
||||||
|
</app-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpEventType } from '@angular/common/http';
|
||||||
import { Observable, forkJoin, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map, catchError } from 'rxjs/operators';
|
import { map, catchError } from 'rxjs/operators';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
@@ -9,7 +9,6 @@ export interface QuoteRequest {
|
|||||||
material: string;
|
material: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
// color removed from global scope
|
|
||||||
infillDensity?: number;
|
infillDensity?: number;
|
||||||
infillPattern?: string;
|
infillPattern?: string;
|
||||||
supportEnabled?: boolean;
|
supportEnabled?: boolean;
|
||||||
@@ -26,32 +25,133 @@ export interface QuoteItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
material?: string;
|
material?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
// Computed values for UI convenience (optional, can be done in component)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuoteResult {
|
export interface QuoteResult {
|
||||||
items: QuoteItem[];
|
items: QuoteItem[];
|
||||||
setupCost: number;
|
setupCost: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
// The following are aggregations that can be re-calculated
|
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
totalTimeHours: number;
|
totalTimeHours: number;
|
||||||
totalTimeMinutes: number;
|
totalTimeMinutes: number;
|
||||||
totalWeight: number;
|
totalWeight: number;
|
||||||
}
|
}
|
||||||
// ... (skip down to calculate logic)
|
|
||||||
|
interface BackendResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
print_time_seconds: number;
|
||||||
|
material_grams: number;
|
||||||
|
cost: {
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class QuoteEstimatorService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||||
|
if (request.items.length === 0) return of();
|
||||||
|
|
||||||
|
return new Observable(observer => {
|
||||||
|
const totalItems = request.items.length;
|
||||||
|
const allProgress: number[] = new Array(totalItems).fill(0);
|
||||||
|
const finalResponses: any[] = [];
|
||||||
|
let completedRequests = 0;
|
||||||
|
|
||||||
|
const uploads = request.items.map((item, index) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', item.file);
|
||||||
|
formData.append('machine', 'bambu_a1');
|
||||||
|
formData.append('filament', this.mapMaterial(request.material));
|
||||||
|
formData.append('quality', this.mapQuality(request.quality));
|
||||||
|
|
||||||
|
// Send color for both modes if present, defaulting to Black
|
||||||
|
formData.append('material_color', item.color || 'Black');
|
||||||
|
|
||||||
|
if (request.mode === 'advanced') {
|
||||||
|
if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString());
|
||||||
|
if (request.infillPattern) formData.append('infill_pattern', request.infillPattern);
|
||||||
|
if (request.supportEnabled) formData.append('support_enabled', 'true');
|
||||||
|
if (request.layerHeight) formData.append('layer_height', request.layerHeight.toString());
|
||||||
|
if (request.nozzleDiameter) formData.append('nozzle_diameter', request.nozzleDiameter.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: any = {};
|
||||||
|
// @ts-ignore
|
||||||
|
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||||
|
|
||||||
|
return this.http.post<BackendResponse>(`${environment.apiUrl}/api/quote`, formData, {
|
||||||
|
headers,
|
||||||
|
reportProgress: true,
|
||||||
|
observe: 'events'
|
||||||
|
}).pipe(
|
||||||
|
map(event => ({ item, event, index })),
|
||||||
|
catchError(err => of({ item, error: err, index }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to all
|
||||||
|
uploads.forEach((obs) => {
|
||||||
|
obs.subscribe({
|
||||||
|
next: (wrapper: any) => {
|
||||||
|
const idx = wrapper.index;
|
||||||
|
|
||||||
|
if (wrapper.error) {
|
||||||
|
finalResponses[idx] = { success: false, fileName: wrapper.item.file.name };
|
||||||
|
// Even if error, we count as complete
|
||||||
|
// But we need to handle completion logic carefully.
|
||||||
|
// For simplicity, let's treat it as complete but check later.
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = wrapper.event;
|
||||||
|
if (event && event.type === HttpEventType.UploadProgress) {
|
||||||
|
if (event.total) {
|
||||||
|
const percent = Math.round((100 * event.loaded) / event.total);
|
||||||
|
allProgress[idx] = percent;
|
||||||
|
// Emit average progress
|
||||||
|
const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems);
|
||||||
|
observer.next(avg);
|
||||||
|
}
|
||||||
|
} else if ((event && event.type === HttpEventType.Response) || wrapper.error) {
|
||||||
|
// It's done (either response or error caught above)
|
||||||
|
if (!finalResponses[idx]) { // only if not already set by error
|
||||||
|
allProgress[idx] = 100;
|
||||||
|
if (wrapper.error) {
|
||||||
|
finalResponses[idx] = { success: false, fileName: wrapper.item.file.name };
|
||||||
|
} else {
|
||||||
|
finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity };
|
||||||
|
}
|
||||||
|
completedRequests++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completedRequests === totalItems) {
|
||||||
|
// All done
|
||||||
|
observer.next(100);
|
||||||
|
|
||||||
|
// Calculate Results
|
||||||
|
let setupCost = 10;
|
||||||
|
|
||||||
|
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
|
||||||
|
setupCost += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: QuoteItem[] = [];
|
||||||
|
|
||||||
finalResponses.forEach((res, idx) => {
|
finalResponses.forEach((res, idx) => {
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
// Find original item to get color
|
const originalItem = request.items[idx];
|
||||||
const originalItem = request.items[idx];
|
|
||||||
// Note: responses and request.items are index-aligned because we mapped them
|
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
fileName: res.fileName,
|
fileName: res.fileName,
|
||||||
unitPrice: res.data.cost.total,
|
unitPrice: res.data.cost.total,
|
||||||
unitTime: res.data.print_time_seconds,
|
unitTime: res.data.print_time_seconds,
|
||||||
unitWeight: res.data.material_grams,
|
unitWeight: res.data.material_grams,
|
||||||
quantity: res.originalQty,
|
quantity: res.originalQty, // Use the requested quantity
|
||||||
material: request.material,
|
material: request.material,
|
||||||
color: originalItem.color || 'Default'
|
color: originalItem.color || 'Default'
|
||||||
});
|
});
|
||||||
@@ -59,6 +159,8 @@ export interface QuoteResult {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
// If at least one failed? Or all?
|
||||||
|
// For now if NO items succeeded, error.
|
||||||
observer.error('All calculations failed.');
|
observer.error('All calculations failed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -93,9 +195,10 @@ export interface QuoteResult {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Error in request', err);
|
console.error('Error in request subscription', err);
|
||||||
|
// Should be caught by inner pipe, but safety net
|
||||||
completedRequests++;
|
completedRequests++;
|
||||||
if (completedRequests === totalItems) {
|
if (completedRequests === totalItems) {
|
||||||
observer.error('Requests failed');
|
observer.error('Requests failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user