From 78af87ac3cd54288e31480a1d4d25deb8d651d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:49:36 +0100 Subject: [PATCH] feat(web): new step for user details --- .../calculator/calculator-page.component.ts | 28 +++- .../quote-result/quote-result.component.html | 9 +- .../quote-result/quote-result.component.ts | 1 + .../user-details/user-details.component.html | 120 ++++++++++++++++++ .../user-details/user-details.component.scss | 102 +++++++++++++++ .../user-details/user-details.component.ts | 59 +++++++++ .../services/quote-estimator.service.ts | 113 ++--------------- frontend/src/assets/i18n/en.json | 29 ++++- 8 files changed, 353 insertions(+), 108 deletions(-) create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.html create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.scss create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.ts diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index 7122a4b..1982bbf 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -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 { UploadFormComponent } from './components/upload-form/upload-form.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 { Router } from '@angular/router'; @Component({ selector: 'app-calculator-page', standalone: true, - imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent], + imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent, UserDetailsComponent], templateUrl: './calculator-page.component.html', styleUrl: './calculator-page.component.scss' }) export class CalculatorPageComponent { mode = signal('easy'); + step = signal<'upload' | 'quote' | 'details'>('upload'); + loading = signal(false); uploadProgress = signal(0); result = signal(null); error = signal(false); + orderSuccess = signal(false); + @ViewChild('uploadForm') uploadForm!: UploadFormComponent; @ViewChild('resultCol') resultCol!: ElementRef; @@ -34,6 +39,7 @@ export class CalculatorPageComponent { this.uploadProgress.set(0); this.error.set(false); this.result.set(null); + this.orderSuccess.set(false); // Auto-scroll on mobile to make analysis visible setTimeout(() => { @@ -51,6 +57,7 @@ export class CalculatorPageComponent { this.result.set(event as QuoteResult); this.loading.set(false); this.uploadProgress.set(100); + this.step.set('quote'); } }, 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; onConsult() { diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index bc78d4e..f41ecb8 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -56,7 +56,12 @@
- {{ 'CALC.ORDER' | translate }} - {{ 'CALC.CONSULT' | translate }} + + {{ 'QUOTE.CONSULT' | translate }} + + + + {{ 'QUOTE.PROCEED_ORDER' | translate }} +
diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index eda5a6f..daeb3cd 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -17,6 +17,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; export class QuoteResultComponent { result = input.required(); consult = output(); + proceed = output(); itemChange = output<{fileName: string, quantity: number}>(); // Local mutable state for items to handle quantity changes diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.html b/frontend/src/app/features/calculator/components/user-details/user-details.component.html new file mode 100644 index 0000000..a080cd6 --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.html @@ -0,0 +1,120 @@ +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+ + +
+
+ + +
+
+ +
+ + {{ 'COMMON.BACK' | translate }} + + + {{ 'USER_DETAILS.SUBMIT' | translate }} + +
+ +
+
+
+ + +
+ + +
+
+
+ {{ item.fileName }} + {{ item.material }} - {{ item.color || 'Default' }} +
+
x{{ item.quantity }}
+
{{ (item.unitPrice * item.quantity) | currency:'CHF' }}
+
+ +
+ +
+ {{ 'QUOTE.TOTAL' | translate }} + {{ quote()!.totalPrice | currency:'CHF' }} +
+
+ +
+
+
+
diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.scss b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss new file mode 100644 index 0000000..734880a --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss @@ -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 + } +} diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.ts b/frontend/src/app/features/calculator/components/user-details/user-details.component.ts new file mode 100644 index 0000000..9656001 --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.ts @@ -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(); + submitOrder = output(); + cancel = output(); + + 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(); + } +} diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index e8e3e1c..3b4de2a 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -24,6 +24,8 @@ export interface QuoteItem { unitTime: number; // seconds unitWeight: number; // grams quantity: number; + material?: string; + color?: string; // Computed values for UI convenience (optional, can be done in component) } @@ -37,114 +39,21 @@ export interface QuoteResult { totalTimeMinutes: number; totalWeight: number; } - -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 { - 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(`${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 => { +// ... (skip down to calculate logic) + finalResponses.forEach((res, idx) => { 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({ fileName: res.fileName, unitPrice: res.data.cost.total, unitTime: res.data.print_time_seconds, unitWeight: res.data.material_grams, - quantity: res.originalQty // Use the requested quantity + quantity: res.originalQty, + material: request.material, + color: originalItem.color || 'Default' }); } }); diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 8091b49..938928f 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -2,9 +2,32 @@ "NAV": { "HOME": "Home", "CALCULATOR": "Calculator", - "SHOP": "Shop", - "ABOUT": "About", - "CONTACT": "Contact Us" + "SHOP": "Shop" + }, + "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": { "PRIVACY": "Privacy",