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 }