diff --git a/src/app/carnet/checkout/checkout.component.html b/src/app/carnet/checkout/checkout.component.html new file mode 100644 index 0000000..a31dca8 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.html @@ -0,0 +1,60 @@ +
+ + + Checkout + + + + +
+
+ Basic fee: + {{estimatedFees.basicFee | currency}} +
+ +
+ Counterfoil fee: + {{estimatedFees.counterFoilFee | currency}} +
+ +
+ Continuation sheet fee: + {{estimatedFees.continuationSheetFee | currency}} +
+ +
+ Expedited fee: + {{estimatedFees.expeditedFee | currency}} +
+ +
+ Shipping fee: + {{estimatedFees.shippingFee | currency}} +
+ +
+ Bond Premium: + {{estimatedFees.bondPremium | currency}} +
+ +
+ Cargo Premium: + {{estimatedFees.cargoPremium | currency}} +
+ +
+ LDI Premium: + {{estimatedFees.ldiPremium | currency}} +
+
+ Total: + {{ orderTotal | currency }} +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/carnet/checkout/checkout.component.scss b/src/app/carnet/checkout/checkout.component.scss new file mode 100644 index 0000000..bd89453 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.scss @@ -0,0 +1,36 @@ +.checkout-container { + max-width: 500px; + margin: 0 auto; + padding: 0 1rem; + + .mat-mdc-card-header { + justify-content: center; + padding: 1rem; + border-bottom: 1px solid #ddd; + margin-bottom: 0.5rem; + } + + mat-card-title { + color: var(--mat-sys-primary); + font-weight: 500; + } + + .order-summary { + .summary-item { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; + } + + .summary-item.total { + border-top: 1px solid #ddd; + padding-top: 0.5rem; + margin-top: 0.5rem; + font-size: 1.1rem; + } + } + + .paypal-container { + margin-top: 2rem; + } +} \ No newline at end of file diff --git a/src/app/carnet/checkout/checkout.component.ts b/src/app/carnet/checkout/checkout.component.ts new file mode 100644 index 0000000..7572591 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.ts @@ -0,0 +1,162 @@ +import { Component, ElementRef, inject, ViewChild } from '@angular/core'; +import { PaymentService } from '../../core/services/carnet/payment.service'; +import { CommonModule } from '@angular/common'; +import { Fees } from '../../core/models/carnet/fee'; +import { CarnetService } from '../../core/services/carnet/carnet.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { NavigationService } from '../../core/services/common/navigation.service'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { StorageService } from '../../core/services/common/storage.service'; +import { finalize } from 'rxjs'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; + +// This is necessary to tell TypeScript that a global 'paypal' object exists. +declare var paypal: any; + +@Component({ + selector: 'app-checkout', + imports: [CommonModule, AngularMaterialModule], + templateUrl: './checkout.component.html', + styleUrl: './checkout.component.scss' +}) +export class CheckoutComponent { + paymentCompleted: boolean = false; + paymentError: boolean = false; + isLoading: boolean = false; + paymentDetails: any = null; + currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null; + changeInProgress: boolean = false; + estimatedFees: Fees = {}; + + @ViewChild('paypalcontrol', { static: true }) paypalElement!: ElementRef; + + orderDetails = { + price: '', + description: '', + }; + + orderTotal = 0.00; + currency = 'USD'; + + private paymentService = inject(PaymentService); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + private storageService = inject(StorageService); + private navigationService = inject(NavigationService); + private carnetService = inject(CarnetService); + + constructor() { + this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication') + this.orderDetails.description = ` 'Carnet Application - ${this.currentApplicationDetails?.applicationName}` + } + + ngOnInit(): void { + if (this.currentApplicationDetails?.headerid) { + this.carnetService.getEstimatedFees(this.currentApplicationDetails?.headerid).subscribe({ + next: (data: Fees) => { + if (data) { + this.estimatedFees = data; + this.calculateTotal(); + } + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get estimated fees'); + this.notificationService.showError(errorMessage); + console.error('Error getting estimated fees:', error); + } + }); + } + } + + ngAfterViewInit(): void { + this.initConfig(); + } + + initConfig(): void { + paypal + .Buttons({ + style: { + layout: 'vertical', // or 'horizontal' + color: 'gold', + shape: 'pill', + label: 'pay', + height: 35 // Height in pixels (default is ~35-45px) + + // 'paypal' – Standard PayPal button [defoult] + // 'checkout' – "Checkout with PayPal" + // 'buynow' – "Buy Now with PayPal" + // 'pay' – "Pay with PayPal" + // 'installment' – Displays installment information + // 'subscribe' – For subscriptions + // 'donate' – For donations + // pay : Pay with [PayPal] + // buynow : [PayPal] Buy Now + // Error: Expected style.height to be between 25px and 55px - got 10px + // width width usually doesn’t need to be set or must be reasonable. + }, + // 1. Call your server to set up the transaction + createOrder: async (data: any, actions: any) => { + try { + // Call the createOrder method from your service + const orderId = await this.paymentService.createOrder(this.orderDetails); + // console.log('Order ID created by server:', orderId); + return orderId; + } catch (error) { + this.notificationService.showError('Failed to initiate the payment'); + console.error('Error creating order:', error); + return; + } + }, + + // 2. Call your server to finalize the transaction + onApprove: async (data: any, actions: any) => { + try { + // data.orderID is the ID of the transaction from PayPal + const captureDetails = await this.paymentService.completePayment(data.orderID); + + // console.log('Capture details:', captureDetails); + + // You can now redirect the user or update the UI + this.changeInProgress = true; + this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { + this.changeInProgress = false; + })).subscribe({ + next: () => { + this.notificationService.showSuccess('Application submitted successfully'); + this.storageService.removeItem('currentapplication'); + this.navigationService.navigate(["home"]); + }, + error: (error) => { + this.notificationService.showError('Failed to submit application'); + console.error('Error submitting the application', error); + } + }); + } catch (error) { + this.notificationService.showError('Failed to complete the payment'); + console.error('Error completing the payment:', error); + return; + } + }, + + // 3. Handle errors + onError: (error: any) => { + this.notificationService.showError('Payment transaction failed'); + console.error('An error occurred during the transaction:', error); + }, + + // 4. Handle cancellation + onCancel: (data: any) => { + } + }).render(this.paypalElement.nativeElement) + .catch((error: any) => { + console.error('Failed to render PayPal Buttons:', error); + }); + } + + calculateTotal(): void { + const validFees = Object.values(this.estimatedFees).filter( + (fee): fee is number => typeof fee === 'number' + ); + this.orderTotal = validFees.reduce((total, fee) => total + fee, 0); + } +} \ No newline at end of file diff --git a/src/app/carnet/terms-conditions/terms-conditions.component.ts b/src/app/carnet/terms-conditions/terms-conditions.component.ts index 22291d1..aa1e93d 100644 --- a/src/app/carnet/terms-conditions/terms-conditions.component.ts +++ b/src/app/carnet/terms-conditions/terms-conditions.component.ts @@ -13,6 +13,8 @@ import { StorageService } from '../../core/services/common/storage.service'; import { CarnetService } from '../../core/services/carnet/carnet.service'; import { Fees } from '../../core/models/carnet/fee'; import { finalize } from 'rxjs'; +import { ShippingService } from '../../core/services/carnet/shipping.service'; +import { Shipping } from '../../core/models/carnet/shipping'; @Component({ selector: 'app-terms-conditions', @@ -26,18 +28,20 @@ import { finalize } from 'rxjs'; styleUrl: './terms-conditions.component.scss' }) export class TermsConditionsComponent { - + isLoading: boolean = false; currentDate = new Date(); hasReadAllTerms = false; currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null; changeInProgress: boolean = false; estimatedFees: Fees = {}; + needCheckOut: boolean = false; private notificationService = inject(NotificationService); private errorHandler = inject(ApiErrorHandlerService); private storageService = inject(StorageService); private navigationService = inject(NavigationService); private carnetService = inject(CarnetService); + private shippingService = inject(ShippingService); constructor() { this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication') @@ -57,25 +61,31 @@ export class TermsConditionsComponent { console.error('Error getting estimated fees:', error); } }); + + this.getPaymentType(this.currentApplicationDetails?.headerid); } } onAccept(): void { - this.changeInProgress = true; - this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { - this.changeInProgress = false; - })).subscribe({ - next: () => { - this.notificationService.showSuccess('Application submitted successfully'); - this.storageService.removeItem('currentapplication'); - this.navigationService.navigate(["home"]); - }, - error: (error) => { - let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application'); - this.notificationService.showError(errorMessage); - console.error('Error submitting the application', error); - } - }); + if (this.needCheckOut) { + this.navigationService.navigate(['checkout']); + } else { + this.changeInProgress = true; + this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { + this.changeInProgress = false; + })).subscribe({ + next: () => { + this.notificationService.showSuccess('Application submitted successfully'); + this.storageService.removeItem('currentapplication'); + this.navigationService.navigate(["home"]); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application'); + this.notificationService.showError(errorMessage); + console.error('Error submitting the application', error); + } + }); + } } onDecline(): void { @@ -100,4 +110,24 @@ export class TermsConditionsComponent { } }) } + + getPaymentType(headerid: number): void { + this.isLoading = true; + this.shippingService.getShippingData(headerid).pipe( + finalize(() => { + this.isLoading = false; + }) + ).subscribe({ + next: (shippingData: Shipping) => { + if (!shippingData) { // do nothing if empty + return; + } + this.needCheckOut = shippingData.paymentMethod !== 'ACH' && shippingData.paymentMethod !== 'INV'; + }, + error: (error: any) => { + console.error('Error loading shipping data', error); + this.errorHandler.handleApiError(error, 'Failed to load shipping data'); + } + }); + } } \ No newline at end of file diff --git a/src/app/carnet/travel-plan/travel-plan.component.ts b/src/app/carnet/travel-plan/travel-plan.component.ts index ca032fe..e399f3d 100644 --- a/src/app/carnet/travel-plan/travel-plan.component.ts +++ b/src/app/carnet/travel-plan/travel-plan.component.ts @@ -304,7 +304,7 @@ export class TravelPlanComponent { if (this.applicationType === 'additional' || this.applicationType === 'extend') { const usentriesControl = this.travelForm.get('usaEntries'); - usentriesControl?.setValidators([Validators.min(1), Validators.max(99)]); + usentriesControl?.setValidators([Validators.min(0), Validators.max(99)]); usentriesControl?.updateValueAndValidity(); } diff --git a/src/app/core/services/carnet/holder.service.ts b/src/app/core/services/carnet/holder.service.ts index caba5b9..52a69f5 100644 --- a/src/app/core/services/carnet/holder.service.ts +++ b/src/app/core/services/carnet/holder.service.ts @@ -28,7 +28,7 @@ export class HolderService { holderType: holder.HOLDERTYPE, uscibMember: holder.USCIBMEMBERFLAG === 'Y', // govAgency: holder.GOVAGENCYFLAG === 'Y', - holderName: holder.HOLDERNAME, + holderName: holder.NAMEOF, //NAMEQUALIFIER: holder.NAMEQUALIFIER || null, dbaName: holder.ADDLNAME || null, address1: holder.ADDRESS1, diff --git a/src/app/core/services/carnet/payment.service.ts b/src/app/core/services/carnet/payment.service.ts new file mode 100644 index 0000000..1218014 --- /dev/null +++ b/src/app/core/services/carnet/payment.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PaymentService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + + createOrder(orderDetails: any): Promise { + return firstValueFrom( + this.http.post<{ id: string }>(`${this.apiUrl}/${this.apiDb}/InitiatePayment`, {}) + ).then(order => order.id); + } + + completePayment(orderId: string): Promise { + const data = { + P_ORDERID: orderId + } + + return firstValueFrom( + this.http.post(`${this.apiUrl}/oracle/CompletePayment`, data)); + } +} \ No newline at end of file diff --git a/src/app/core/services/carnet/replacement.service.ts b/src/app/core/services/carnet/replacement.service.ts index 0743f5e..573b90f 100644 --- a/src/app/core/services/carnet/replacement.service.ts +++ b/src/app/core/services/carnet/replacement.service.ts @@ -21,7 +21,7 @@ export class ReplacementService { } private mapToExtensionData(extensionDataCollection: any): Replacement { - let extensionData = extensionDataCollection?.[0]; + let extensionData = extensionDataCollection; let replacementData: Replacement = { replacementCarnetNumber: extensionData.REPLACECARNETNO, diff --git a/src/app/core/services/home.service.ts b/src/app/core/services/home.service.ts index 2b002c4..1eb1c83 100644 --- a/src/app/core/services/home.service.ts +++ b/src/app/core/services/home.service.ts @@ -41,6 +41,7 @@ export class HomeService { headerid: item.HEADERID, clientid: item.CLIENTID, locationid: item.LOCATIONID, + printGLStatus: item.PRINTGLSTATUS })); } } diff --git a/src/app/holder/search/search-holder.component.html b/src/app/holder/search/search-holder.component.html index 79ac7da..3c585d4 100644 --- a/src/app/holder/search/search-holder.component.html +++ b/src/app/holder/search/search-holder.component.html @@ -106,7 +106,7 @@ info - No records found matching your criteria + No records found diff --git a/src/app/holder/search/search-holder.component.ts b/src/app/holder/search/search-holder.component.ts index 90f2b12..585e366 100644 --- a/src/app/holder/search/search-holder.component.ts +++ b/src/app/holder/search/search-holder.component.ts @@ -29,21 +29,9 @@ import { environment } from '../../../environments/environment'; providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], }) export class SearchHolderComponent { - private _paginator!: MatPaginator; - @ViewChild(MatPaginator, { static: false }) - set paginator(value: MatPaginator) { - this._paginator = value; - this.dataSource.paginator = value; - } - get paginator(): MatPaginator { - return this._paginator; - } - - @ViewChild(MatSort, { static: false }) - set sort(value: MatSort) { - this.dataSource.sort = value; - } + @ViewChild(MatPaginator, { static: false }) paginator!: MatPaginator; + @ViewChild(MatSort, { static: false }) sort!: MatSort; @Input() isViewMode = false; @Input() headerid: number = 0; @@ -73,8 +61,7 @@ export class SearchHolderComponent { dataSource = new MatTableDataSource([]); ngAfterViewInit() { - // This is different from other pages to show the item selected in the table. - //this.dataSource.paginator = this.paginator; + this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } @@ -93,19 +80,10 @@ export class SearchHolderComponent { }); } - ngOnInit(): void { - if (this.allowActions) { - this.searchHolders(); - } - } ngOnChanges(changes: SimpleChanges): void { if (changes['selectedHolderId']) { - if (this.selectedHolderId && this.paginator && this.dataSource.data.length && this.allowActions) { - this.goToItemPage(this.selectedHolderId); - } - - if (!this.allowActions && this.selectedHolderId) { + if (this.selectedHolderId) { this.showSelectedHolder(this.selectedHolderId); } } @@ -168,6 +146,11 @@ export class SearchHolderComponent { this.isLoading = true; const filterData: HolderFilter = this.searchForm.value; + if (!this.searchForm.value.holderName && this.selectedHolderId) { + this.showSelectedHolder(this.selectedHolderId); + return; + } + this.holderService.getHolders(filterData).pipe(finalize(() => { this.isLoading = false; })).subscribe({ @@ -290,13 +273,4 @@ export class SearchHolderComponent { // Filter out any empty, null, or undefined parts and then join them. return parts.filter(part => part).join(', '); } - - goToItemPage(holderid: number): void { - const itemIndex = this.dataSource.data.findIndex(dataItem => dataItem.holderid === holderid); - if (itemIndex > -1 && this.paginator) { - const targetPageIndex = Math.floor(itemIndex / this.paginator.pageSize); - this.paginator.pageIndex = targetPageIndex; - this.dataSource.paginator = this.paginator; - } - } } diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 6ad6ba7..2c56dd8 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -87,16 +87,22 @@ Actions
- + --> + -