exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

checkout.component.ts (7356B)


      1 import { CommonModule } from '@angular/common';
      2 import { Component, signal, computed, ViewChild, inject } from '@angular/core';
      3 import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
      4 import { Router } from '@angular/router';
      5 import { CardInputComponent } from './credit-card.component';
      6 import { NotificationService } from '../services/notification.service';
      7 import { CartService } from '../services/cart.service';
      8 import { OrderService } from '../services/order.service';
      9 
     10 @Component({
     11   selector: 'section.checkout',
     12   templateUrl: './checkout.component.html',
     13   styleUrls: ['./checkout.component.scss'],
     14   standalone: true,
     15   imports: [CommonModule, ReactiveFormsModule, CardInputComponent],
     16 })
     17 export class CheckoutComponent {
     18   @ViewChild(CardInputComponent) cardInputComponent?: CardInputComponent;
     19   private notificationService = inject(NotificationService);
     20   private cartService = inject(CartService);
     21   private orderService = inject(OrderService);
     22 
     23   checkoutForm: FormGroup;
     24   submitted = signal(false);
     25   isSubmitting = signal(false);
     26 
     27   // Shipping costs for each option
     28   shippingOptions = [
     29     { value: 'ups', label: 'UPS', icon: 'bi-truck', cost: 5.99 },
     30     { value: 'fedex', label: 'FedEx', icon: 'bi-box-seam', cost: 7.99 },
     31     { value: 'dhl', label: 'DHL', icon: 'bi-mailbox', cost: 9.99 },
     32   ];
     33 
     34   // Tax rate (10%)
     35   readonly TAX_RATE = 0.10;
     36 
     37   // Computed signal to track if form is valid
     38   isFormValid = computed(() => {
     39     const formValid = this.checkoutForm?.valid ?? false;
     40     const cardValid = this.cardInputComponent?.isValidCard?.() ?? false;
     41     return formValid && cardValid;
     42   });
     43 
     44   constructor(
     45     private fb: FormBuilder,
     46     private router: Router,
     47   ) {
     48     this.checkoutForm = this.fb.group({
     49       firstName: ['', [Validators.required, Validators.minLength(2)]],
     50       lastName: ['', [Validators.required, Validators.minLength(2)]],
     51       email: ['', [Validators.required, Validators.email]],
     52       phoneNumber: [
     53         '',
     54         [
     55           Validators.required,
     56           Validators.pattern(/^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}$/),
     57         ],
     58       ],
     59       addressLine1: ['', [Validators.required, Validators.minLength(5)]],
     60       addressLine2: [''],
     61       city: ['', [Validators.required, Validators.minLength(2)]],
     62       state: ['', [Validators.required, Validators.minLength(2)]],
     63       zipCode: ['', [Validators.required, Validators.pattern(/^[0-9]{5}(-[0-9]{4})?$/)]],
     64       country: ['US', [Validators.required, Validators.minLength(2)]],
     65       shippingOption: ['', Validators.required],
     66       agreeToTerms: [false, Validators.requiredTrue],
     67     });
     68   }
     69 
     70   onSubmit(): void {
     71     this.submitted.set(true);
     72 
     73     // Mark all fields as touched to show validation errors
     74     Object.keys(this.checkoutForm.controls).forEach((key) => {
     75       this.checkoutForm.get(key)?.markAsTouched();
     76     });
     77 
     78     // Mark card input as touched
     79     if (this.cardInputComponent) {
     80       this.cardInputComponent.markAsTouched();
     81     }
     82 
     83     // Check if form and card are valid
     84     if (!this.isFormValid()) {
     85       this.notificationService.show('Please fix all errors before submitting', 'danger');
     86       return;
     87     }
     88 
     89     // Check if cart is empty
     90     if (this.cartService.isEmpty()) {
     91       this.notificationService.show('Your cart is empty', 'danger');
     92       return;
     93     }
     94 
     95     // Prevent double submission
     96     if (this.isSubmitting()) {
     97       return;
     98     }
     99 
    100     this.isSubmitting.set(true);
    101 
    102     // Build order request
    103     const formValue = this.checkoutForm.value;
    104     const cartItems = this.cartService.cartItems();
    105 
    106     // Build shipping address
    107     const shippingAddress: CreateShippingAddressRequest = {
    108       first_name: formValue.firstName,
    109       last_name: formValue.lastName,
    110       address_line1: formValue.addressLine1,
    111       address_line2: formValue.addressLine2 || null,
    112       city: formValue.city,
    113       state: formValue.state,
    114       postal_code: formValue.zipCode,
    115       country: formValue.country,
    116       phone: formValue.phoneNumber,
    117     };
    118 
    119     // Build order items with complete product details
    120     const items: CreateOrderItemRequest[] = cartItems.map(item => ({
    121       product_uuid: item.product.eid,
    122       product_name: item.product.product_name,
    123       product_sku: null,
    124       quantity: item.quantity,
    125       unit_price: item.product.final_price,
    126     }));
    127 
    128     // Build payment info
    129     const payment: CreatePaymentRequest = {
    130       payment_method: 'credit_card',
    131       card_last4: this.cardInputComponent?.getCardLast4() || null,
    132       card_brand: this.cardInputComponent?.getCardBrand() || null,
    133     };
    134 
    135     // Build complete order request
    136     const orderRequest: CreateOrderRequest = {
    137       customer_email: formValue.email,
    138       customer_phone: formValue.phoneNumber,
    139       shipping_address: shippingAddress,
    140       items: items,
    141       payment: payment,
    142     };
    143 
    144     // Submit order
    145     this.orderService.createOrder(orderRequest).subscribe({
    146       next: (response) => {
    147         this.isSubmitting.set(false);
    148 
    149         if (response.success) {
    150           // Clear cart on success
    151           this.cartService.clearCart();
    152 
    153           // Show success notification
    154           this.notificationService.show('Order placed successfully! Redirecting to products...', 'success');
    155 
    156           // Navigate to products page
    157           setTimeout(() => {
    158             this.router.navigate(['/products']);
    159           }, 1500);
    160         } else {
    161           // Handle error response
    162           const errorMessage = response.error || 'Failed to place order';
    163           this.notificationService.show(errorMessage, 'danger');
    164         }
    165       },
    166       error: (error) => {
    167         this.isSubmitting.set(false);
    168 
    169         // Handle HTTP errors
    170         const errorMessage = error.error?.error || error.message || 'An error occurred while placing the order';
    171         this.notificationService.show(errorMessage, 'danger');
    172       }
    173     });
    174   }
    175 
    176   // Helper methods to check field validity
    177   isFieldInvalid(fieldName: string): boolean {
    178     const field = this.checkoutForm.get(fieldName);
    179     return !!(field && field.invalid && (field.touched || this.submitted()));
    180   }
    181 
    182   getFieldError(fieldName: string): string {
    183     const field = this.checkoutForm.get(fieldName);
    184     if (!field || !field.errors) return '';
    185 
    186     if (field.errors['required']) return `${this.getFieldLabel(fieldName)} is required`;
    187     if (field.errors['email']) return 'Please enter a valid email address';
    188     if (field.errors['minlength']) return `${this.getFieldLabel(fieldName)} is too short`;
    189     if (field.errors['pattern']) {
    190       if (fieldName === 'phoneNumber') return 'Please enter a valid phone number';
    191       if (fieldName === 'zipCode') return 'Please enter a valid ZIP code (e.g., 12345 or 12345-6789)';
    192     }
    193 
    194     return 'Invalid value';
    195   }
    196 
    197   private getFieldLabel(fieldName: string): string {
    198     const labels: { [key: string]: string } = {
    199       firstName: 'First name',
    200       lastName: 'Last name',
    201       email: 'Email',
    202       phoneNumber: 'Phone number',
    203       addressLine1: 'Address line 1',
    204       addressLine2: 'Address line 2',
    205       zipCode: 'ZIP code',
    206       city: 'City',
    207       state: 'State/Province',
    208       country: 'Country',
    209       shippingOption: 'Shipping option',
    210       agreeToTerms: 'Agreement to terms',
    211     };
    212     return labels[fieldName] || fieldName;
    213   }
    214 }