dev #5
@@ -6,23 +6,28 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp
|
|||||||
import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component';
|
import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component';
|
||||||
import { UploadFormComponent } from './components/upload-form/upload-form.component';
|
import { UploadFormComponent } from './components/upload-form/upload-form.component';
|
||||||
import { QuoteResultComponent } from './components/quote-result/quote-result.component';
|
import { QuoteResultComponent } from './components/quote-result/quote-result.component';
|
||||||
|
import { UserDetailsComponent } from './components/user-details/user-details.component';
|
||||||
import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quote-estimator.service';
|
import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quote-estimator.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calculator-page',
|
selector: 'app-calculator-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent],
|
imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent, UserDetailsComponent],
|
||||||
templateUrl: './calculator-page.component.html',
|
templateUrl: './calculator-page.component.html',
|
||||||
styleUrl: './calculator-page.component.scss'
|
styleUrl: './calculator-page.component.scss'
|
||||||
})
|
})
|
||||||
export class CalculatorPageComponent {
|
export class CalculatorPageComponent {
|
||||||
mode = signal<any>('easy');
|
mode = signal<any>('easy');
|
||||||
|
step = signal<'upload' | 'quote' | 'details'>('upload');
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
uploadProgress = signal(0);
|
uploadProgress = signal(0);
|
||||||
result = signal<QuoteResult | null>(null);
|
result = signal<QuoteResult | null>(null);
|
||||||
error = signal<boolean>(false);
|
error = signal<boolean>(false);
|
||||||
|
|
||||||
|
orderSuccess = signal(false);
|
||||||
|
|
||||||
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
|
||||||
@ViewChild('resultCol') resultCol!: ElementRef;
|
@ViewChild('resultCol') resultCol!: ElementRef;
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ export class CalculatorPageComponent {
|
|||||||
this.uploadProgress.set(0);
|
this.uploadProgress.set(0);
|
||||||
this.error.set(false);
|
this.error.set(false);
|
||||||
this.result.set(null);
|
this.result.set(null);
|
||||||
|
this.orderSuccess.set(false);
|
||||||
|
|
||||||
// Auto-scroll on mobile to make analysis visible
|
// Auto-scroll on mobile to make analysis visible
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -51,6 +57,7 @@ export class CalculatorPageComponent {
|
|||||||
this.result.set(event as QuoteResult);
|
this.result.set(event as QuoteResult);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.uploadProgress.set(100);
|
this.uploadProgress.set(100);
|
||||||
|
this.step.set('quote');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -60,6 +67,25 @@ export class CalculatorPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onProceed() {
|
||||||
|
this.step.set('details');
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancelDetails() {
|
||||||
|
this.step.set('quote');
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitOrder(orderData: any) {
|
||||||
|
console.log('Order Submitted:', orderData);
|
||||||
|
this.orderSuccess.set(true);
|
||||||
|
this.step.set('upload'); // Reset to start, or show success page?
|
||||||
|
// For now, let's show success message and reset
|
||||||
|
setTimeout(() => {
|
||||||
|
this.orderSuccess.set(false);
|
||||||
|
}, 5000);
|
||||||
|
this.result.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
private currentRequest: QuoteRequest | null = null;
|
private currentRequest: QuoteRequest | null = null;
|
||||||
|
|
||||||
onConsult() {
|
onConsult() {
|
||||||
|
|||||||
@@ -56,7 +56,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<app-button variant="primary" [fullWidth]="true">{{ 'CALC.ORDER' | translate }}</app-button>
|
<app-button variant="outline" (click)="consult.emit()">
|
||||||
<app-button variant="outline" [fullWidth]="true" (click)="consult.emit()">{{ 'CALC.CONSULT' | translate }}</app-button>
|
{{ 'QUOTE.CONSULT' | translate }}
|
||||||
|
</app-button>
|
||||||
|
|
||||||
|
<app-button (click)="proceed.emit()">
|
||||||
|
{{ 'QUOTE.PROCEED_ORDER' | translate }}
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</app-card>
|
</app-card>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
|
|||||||
export class QuoteResultComponent {
|
export class QuoteResultComponent {
|
||||||
result = input.required<QuoteResult>();
|
result = input.required<QuoteResult>();
|
||||||
consult = output<void>();
|
consult = output<void>();
|
||||||
|
proceed = output<void>();
|
||||||
itemChange = output<{fileName: string, quantity: number}>();
|
itemChange = output<{fileName: string, quantity: number}>();
|
||||||
|
|
||||||
// Local mutable state for items to handle quantity changes
|
// Local mutable state for items to handle quantity changes
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<div class="user-details-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-card [title]="'USER_DETAILS.TITLE' | translate">
|
||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<!-- Name & Surname -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-input
|
||||||
|
formControlName="name"
|
||||||
|
label="USER_DETAILS.NAME"
|
||||||
|
placeholder="USER_DETAILS.NAME_PLACEHOLDER"
|
||||||
|
[error]="form.get('name')?.invalid && form.get('name')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-input
|
||||||
|
formControlName="surname"
|
||||||
|
label="USER_DETAILS.SURNAME"
|
||||||
|
placeholder="USER_DETAILS.SURNAME_PLACEHOLDER"
|
||||||
|
[error]="form.get('surname')?.invalid && form.get('surname')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email & Phone -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-input
|
||||||
|
formControlName="email"
|
||||||
|
label="USER_DETAILS.EMAIL"
|
||||||
|
type="email"
|
||||||
|
placeholder="USER_DETAILS.EMAIL_PLACEHOLDER"
|
||||||
|
[error]="form.get('email')?.invalid && form.get('email')?.touched ? ('COMMON.INVALID_EMAIL' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-input
|
||||||
|
formControlName="phone"
|
||||||
|
label="USER_DETAILS.PHONE"
|
||||||
|
type="tel"
|
||||||
|
placeholder="USER_DETAILS.PHONE_PLACEHOLDER"
|
||||||
|
[error]="form.get('phone')?.invalid && form.get('phone')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Address -->
|
||||||
|
<app-input
|
||||||
|
formControlName="address"
|
||||||
|
label="USER_DETAILS.ADDRESS"
|
||||||
|
placeholder="USER_DETAILS.ADDRESS_PLACEHOLDER"
|
||||||
|
[error]="form.get('address')?.invalid && form.get('address')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
|
||||||
|
<!-- Zip & City -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<app-input
|
||||||
|
formControlName="zip"
|
||||||
|
label="USER_DETAILS.ZIP"
|
||||||
|
placeholder="USER_DETAILS.ZIP_PLACEHOLDER"
|
||||||
|
[error]="form.get('zip')?.invalid && form.get('zip')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<app-input
|
||||||
|
formControlName="city"
|
||||||
|
label="USER_DETAILS.CITY"
|
||||||
|
placeholder="USER_DETAILS.CITY_PLACEHOLDER"
|
||||||
|
[error]="form.get('city')?.invalid && form.get('city')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||||
|
</app-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<app-button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
(click)="onCancel()">
|
||||||
|
{{ 'COMMON.BACK' | translate }}
|
||||||
|
</app-button>
|
||||||
|
<app-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="form.invalid || submitting()">
|
||||||
|
{{ 'USER_DETAILS.SUBMIT' | translate }}
|
||||||
|
</app-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</app-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Summary Column -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-card [title]="'USER_DETAILS.SUMMARY_TITLE' | translate">
|
||||||
|
|
||||||
|
<div class="summary-content" *ngIf="quote()">
|
||||||
|
<div class="summary-item" *ngFor="let item of quote()!.items">
|
||||||
|
<div class="item-info">
|
||||||
|
<span class="item-name">{{ item.fileName }}</span>
|
||||||
|
<span class="item-meta">{{ item.material }} - {{ item.color || 'Default' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-qty">x{{ item.quantity }}</div>
|
||||||
|
<div class="item-price">{{ (item.unitPrice * item.quantity) | currency:'CHF' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="total-row">
|
||||||
|
<span>{{ 'QUOTE.TOTAL' | translate }}</span>
|
||||||
|
<span class="total-price">{{ quote()!.totalPrice | currency:'CHF' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
.user-details-container {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -0.5rem;
|
||||||
|
|
||||||
|
> [class*='col-'] {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-md-6 {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-md-4 {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 33.333%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-md-8 {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 66.666%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary Styles
|
||||||
|
.summary-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-qty {
|
||||||
|
margin: 0 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
color: var(--primary-color, #00C853); // Fallback color
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Component, input, output, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
|
||||||
|
import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component';
|
||||||
|
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||||
|
import { QuoteResult } from '../../services/quote-estimator.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-details',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppCardComponent, AppInputComponent, AppButtonComponent],
|
||||||
|
templateUrl: './user-details.component.html',
|
||||||
|
styleUrl: './user-details.component.scss'
|
||||||
|
})
|
||||||
|
export class UserDetailsComponent {
|
||||||
|
quote = input<QuoteResult>();
|
||||||
|
submitOrder = output<any>();
|
||||||
|
cancel = output<void>();
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
submitting = signal(false);
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
name: ['', Validators.required],
|
||||||
|
surname: ['', Validators.required],
|
||||||
|
email: ['', [Validators.required, Validators.email]],
|
||||||
|
phone: ['', Validators.required],
|
||||||
|
address: ['', Validators.required],
|
||||||
|
zip: ['', Validators.required],
|
||||||
|
city: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.submitting.set(true);
|
||||||
|
|
||||||
|
const orderData = {
|
||||||
|
customer: this.form.value,
|
||||||
|
quote: this.quote()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate API delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.submitOrder.emit(orderData);
|
||||||
|
this.submitting.set(false);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ export interface QuoteItem {
|
|||||||
unitTime: number; // seconds
|
unitTime: number; // seconds
|
||||||
unitWeight: number; // grams
|
unitWeight: number; // grams
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
material?: string;
|
||||||
|
color?: string;
|
||||||
// Computed values for UI convenience (optional, can be done in component)
|
// Computed values for UI convenience (optional, can be done in component)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,114 +39,21 @@ export interface QuoteResult {
|
|||||||
totalTimeMinutes: number;
|
totalTimeMinutes: number;
|
||||||
totalWeight: number;
|
totalWeight: number;
|
||||||
}
|
}
|
||||||
|
// ... (skip down to calculate logic)
|
||||||
interface BackendResponse {
|
finalResponses.forEach((res, idx) => {
|
||||||
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 };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = wrapper.event;
|
|
||||||
if (event.type === 1) { // 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.type === 4) { // HttpEventType.Response
|
|
||||||
allProgress[idx] = 100;
|
|
||||||
finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity };
|
|
||||||
completedRequests++;
|
|
||||||
|
|
||||||
if (completedRequests === totalItems) {
|
|
||||||
// All done
|
|
||||||
observer.next(100);
|
|
||||||
|
|
||||||
// Calculate Results
|
|
||||||
// Base setup cost
|
|
||||||
let setupCost = 10;
|
|
||||||
|
|
||||||
// Surcharge for non-standard nozzle
|
|
||||||
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
|
|
||||||
setupCost += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: QuoteItem[] = [];
|
|
||||||
|
|
||||||
finalResponses.forEach(res => {
|
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
|
// Find original item to get color
|
||||||
|
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 // Use the requested quantity
|
quantity: res.originalQty,
|
||||||
|
material: request.material,
|
||||||
|
color: originalItem.color || 'Default'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,32 @@
|
|||||||
"NAV": {
|
"NAV": {
|
||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"CALCULATOR": "Calculator",
|
"CALCULATOR": "Calculator",
|
||||||
"SHOP": "Shop",
|
"SHOP": "Shop"
|
||||||
"ABOUT": "About",
|
},
|
||||||
"CONTACT": "Contact Us"
|
"QUOTE": {
|
||||||
|
"CONSULT": "Request Consultation",
|
||||||
|
"PROCEED_ORDER": "Proceed to Order",
|
||||||
|
"TOTAL": "Total Estimate"
|
||||||
|
},
|
||||||
|
"USER_DETAILS": {
|
||||||
|
"TITLE": "Shipping Details",
|
||||||
|
"SUMMARY_TITLE": "Order Summary",
|
||||||
|
"NAME": "First Name",
|
||||||
|
"NAME_PLACEHOLDER": "Enter your first name",
|
||||||
|
"SURNAME": "Last Name",
|
||||||
|
"SURNAME_PLACEHOLDER": "Enter your last name",
|
||||||
|
"EMAIL": "Email",
|
||||||
|
"EMAIL_PLACEHOLDER": "your@email.com",
|
||||||
|
"PHONE": "Phone",
|
||||||
|
"PHONE_PLACEHOLDER": "+41 79 123 45 67",
|
||||||
|
"ADDRESS": "Address",
|
||||||
|
"ADDRESS_PLACEHOLDER": "Street and Number",
|
||||||
|
"ZIP": "ZIP",
|
||||||
|
"ZIP_PLACEHOLDER": "8000",
|
||||||
|
"CITY": "City",
|
||||||
|
"CITY_PLACEHOLDER": "Zurich",
|
||||||
|
"SUBMIT": "Submit Order",
|
||||||
|
"ORDER_SUCCESS": "Order submitted successfully! We will contact you shortly."
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"PRIVACY": "Privacy",
|
"PRIVACY": "Privacy",
|
||||||
|
|||||||
Reference in New Issue
Block a user