From ad32f5c1ff7adfad02b1cda31962758176c62fd1 Mon Sep 17 00:00:00 2001 From: Kallesh B S Date: Tue, 15 Jul 2025 18:52:36 +0530 Subject: [PATCH] holder selection flow --- src/app/app.routes.ts | 2 + src/app/carnet/add/add-carnet.component.html | 13 +- src/app/carnet/add/add-carnet.component.ts | 11 +- .../application/application.component.ts | 5 +- .../carnet/edit/edit-carnet.component.html | 5 +- src/app/carnet/edit/edit-carnet.component.ts | 3 +- src/app/core/models/holder/contact.ts | 30 ++ src/app/core/models/holder/holder-details.ts | 40 ++ .../carnet/carnet-application.service.ts | 16 + .../core/services/holder/contact.service.ts | 92 +++++ .../services/holder/holder-detail.service.ts | 101 +++++ .../core/services/holder/holder.service.ts | 68 ++++ src/app/holder/add/add.component.html | 26 ++ src/app/holder/add/add.component.scss | 4 + src/app/holder/add/add.component.ts | 67 ++++ src/app/holder/edit/edit.component.html | 24 ++ src/app/holder/edit/edit.component.scss | 18 + src/app/holder/edit/edit.component.ts | 39 ++ .../holder-contacts.component.html | 264 +++++++++++++ .../holder-contacts.component.scss | 354 ++++++++++++++++++ .../holder-contacts.component.ts | 203 ++++++++++ .../holder-detail.component.html | 166 ++++++++ .../holder-detail.component.scss | 125 +++++++ .../holder-detail/holder-detail.component.ts | 205 ++++++++++ src/app/holder/search/search.component.html | 131 +++++++ src/app/holder/search/search.component.scss | 135 +++++++ src/app/holder/search/search.component.ts | 201 ++++++++++ src/app/home/home.component.ts | 3 + 28 files changed, 2341 insertions(+), 10 deletions(-) create mode 100644 src/app/core/models/holder/contact.ts create mode 100644 src/app/core/models/holder/holder-details.ts create mode 100644 src/app/core/services/carnet/carnet-application.service.ts create mode 100644 src/app/core/services/holder/contact.service.ts create mode 100644 src/app/core/services/holder/holder-detail.service.ts create mode 100644 src/app/core/services/holder/holder.service.ts create mode 100644 src/app/holder/add/add.component.html create mode 100644 src/app/holder/add/add.component.scss create mode 100644 src/app/holder/add/add.component.ts create mode 100644 src/app/holder/edit/edit.component.html create mode 100644 src/app/holder/edit/edit.component.scss create mode 100644 src/app/holder/edit/edit.component.ts create mode 100644 src/app/holder/holder-contacts/holder-contacts.component.html create mode 100644 src/app/holder/holder-contacts/holder-contacts.component.scss create mode 100644 src/app/holder/holder-contacts/holder-contacts.component.ts create mode 100644 src/app/holder/holder-detail/holder-detail.component.html create mode 100644 src/app/holder/holder-detail/holder-detail.component.scss create mode 100644 src/app/holder/holder-detail/holder-detail.component.ts create mode 100644 src/app/holder/search/search.component.html create mode 100644 src/app/holder/search/search.component.scss create mode 100644 src/app/holder/search/search.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d87ef85..ac41281 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -15,6 +15,8 @@ export const routes: Routes = [ { path: 'usersettings', loadComponent: () => import('./user-settings/user-settings.component').then(m => m.UserSettingsComponent) }, { path: 'add-carnet', loadComponent: () => import('./carnet/add/add-carnet.component').then(m => m.AddCarnetComponent) }, { path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) }, + { path: 'add-holder', loadComponent: () => import('./holder/add/add.component').then(m => m.AddComponent) }, + { path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit.component').then(m => m.EditComponent) }, { path: '', redirectTo: 'home', pathMatch: 'full' } ], canActivate: [AuthGuard, AppIdGuard] diff --git a/src/app/carnet/add/add-carnet.component.html b/src/app/carnet/add/add-carnet.component.html index cb8a6fe..d8f8a6a 100644 --- a/src/app/carnet/add/add-carnet.component.html +++ b/src/app/carnet/add/add-carnet.component.html @@ -15,8 +15,17 @@ Holder Selection - - + + + + + + diff --git a/src/app/carnet/add/add-carnet.component.ts b/src/app/carnet/add/add-carnet.component.ts index ecb8611..3136bb7 100644 --- a/src/app/carnet/add/add-carnet.component.ts +++ b/src/app/carnet/add/add-carnet.component.ts @@ -10,16 +10,19 @@ import { TravelPlanComponent } from '../travel-plan/travel-plan.component'; import { ShippingComponent } from '../shipping/shipping.component'; import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferences } from '../../core/models/user-preference'; +import { SearchComponent } from '../../holder/search/search.component'; +import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service'; @Component({ selector: 'app-add-carnet', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, - ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, + ApplicationComponent, SearchComponent, GoodsComponent, TravelPlanComponent, ShippingComponent], templateUrl: './add-carnet.component.html', styleUrl: './add-carnet.component.scss' }) export class AddCarnetComponent { + store = inject(CarnetStateStore); currentStep = 0; isLinear = true; applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; @@ -44,8 +47,10 @@ export class AddCarnetComponent { this.currentStep = event.selectedIndex; } - onApplicationDetailCreated(headerid: number): void { - this.headerid = headerid; + onApplicationDetailCreated(data: { HEADERID: number, APPLICATION_NAME: string }): void { + this.store.headerid.set(data.HEADERID); + this.store.applicationName.set(data.APPLICATION_NAME); + this.headerid = data.HEADERID; this.stepsCompleted.applicationDetail = true; this.isLinear = false; // Disable linear mode after application detail is created } diff --git a/src/app/carnet/application/application.component.ts b/src/app/carnet/application/application.component.ts index 19ede44..010fd09 100644 --- a/src/app/carnet/application/application.component.ts +++ b/src/app/carnet/application/application.component.ts @@ -20,7 +20,8 @@ export class ApplicationComponent { @Input() headerid: number = 0; @Input() applicationName: string = ''; - @Output() headerIdCreated = new EventEmitter(); + // @Output() headerIdCreated = new EventEmitter(); + @Output() headerIdCreated = new EventEmitter<{ HEADERID: number, APPLICATION_NAME: string }>(); applicationDetailsForm: FormGroup; isLoading = false; @@ -102,7 +103,7 @@ export class ApplicationComponent { this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({ next: (applicationData: any) => { this.notificationService.showSuccess(`Application details added successfully`); - this.headerIdCreated.emit(+applicationData.HEADERID); + this.headerIdCreated.emit({ HEADERID: +applicationData.HEADERID, APPLICATION_NAME: applicationDetailData.name }); this.applicationDetailsForm.get('name')?.disable(); this.disableSaveButton = true; this.isLoading = false; diff --git a/src/app/carnet/edit/edit-carnet.component.html b/src/app/carnet/edit/edit-carnet.component.html index 49d6cfa..da89d57 100644 --- a/src/app/carnet/edit/edit-carnet.component.html +++ b/src/app/carnet/edit/edit-carnet.component.html @@ -15,8 +15,9 @@ Holder Selection - - + + diff --git a/src/app/carnet/edit/edit-carnet.component.ts b/src/app/carnet/edit/edit-carnet.component.ts index 64e3d50..6307515 100644 --- a/src/app/carnet/edit/edit-carnet.component.ts +++ b/src/app/carnet/edit/edit-carnet.component.ts @@ -11,11 +11,12 @@ import { ShippingComponent } from '../shipping/shipping.component'; import { TravelPlanComponent } from '../travel-plan/travel-plan.component'; import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { SearchComponent } from '../../holder/search/search.component'; @Component({ selector: 'app-edit-carnet', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, - ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent], + ApplicationComponent, SearchComponent, GoodsComponent, TravelPlanComponent, ShippingComponent], templateUrl: './edit-carnet.component.html', styleUrl: './edit-carnet.component.scss' }) diff --git a/src/app/core/models/holder/contact.ts b/src/app/core/models/holder/contact.ts new file mode 100644 index 0000000..09b3dc8 --- /dev/null +++ b/src/app/core/models/holder/contact.ts @@ -0,0 +1,30 @@ +export interface HolderContact { + HOLDERCONTACTID: number; + HOLDERID: number; + SPID: number; + FIRSTNAME: string; + LASTNAME: string; + MIDDLEINITIAL: string | null; + TITLE: string; + PHONE: string; + MOBILE: string; + FAX: string | null; + EMAILADDRESS: string; + DATECREATED: string; + CREATEDBY: string; + INACTIVEFLAG: string; + INACTIVEDATE: string | null; + LASTUPDATEDBY: string | null; + LASTUPDATEDDATE: string | null; +} + +export interface ContactFormModel { + firstName: string; + lastName: string; + middleInitial: string; + title: string; + phone: string; + mobile: string; + fax: string; + email: string; +}; diff --git a/src/app/core/models/holder/holder-details.ts b/src/app/core/models/holder/holder-details.ts new file mode 100644 index 0000000..830ebab --- /dev/null +++ b/src/app/core/models/holder/holder-details.ts @@ -0,0 +1,40 @@ +export interface HolderDetail { + HOLDERID?: number; + SPID: number; + LOCATIONID: number; + HOLDERNO: string; + HOLDERTYPE: string; + USCIBMEMBERFLAG: string; + GOVAGENCYFLAG: string; + HOLDERNAME: string; + NAMEQUALIFIER: string | null; + ADDLNAME: string | null; + ADDRESS1: string; + ADDRESS2: string | null; + CITY: string; + STATE: string; + ZIP: string; + COUNTRY: string; + DATECREATED: string; + CREATEDBY: string; + INACTIVEFLAG: string; + INACTIVEDATE: string | null; + LASTUPDATEDBY: string | null; + LASTUPDATEDDATE: string | null; +} + + +export interface HolderDetailFormType { + holderName: string; + DBAName: string; + HolderNumber: string; + holderType: string; + USCIBMember: string; + govAgency: string; + address1: string; + address2: string; + city: string; + state: string; + country: string; + zip: string; +} \ No newline at end of file diff --git a/src/app/core/services/carnet/carnet-application.service.ts b/src/app/core/services/carnet/carnet-application.service.ts new file mode 100644 index 0000000..c6d2646 --- /dev/null +++ b/src/app/core/services/carnet/carnet-application.service.ts @@ -0,0 +1,16 @@ +// appli.service.ts +import { Injectable, signal } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CarnetStateStore { + headerid = signal(0); + applicationName = signal(''); + + resetState() { + this.headerid.set(0); + this.applicationName.set(''); + } + +} diff --git a/src/app/core/services/holder/contact.service.ts b/src/app/core/services/holder/contact.service.ts new file mode 100644 index 0000000..9bfd030 --- /dev/null +++ b/src/app/core/services/holder/contact.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { UserService } from '../common/user.service'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../../environments/environment'; +import { map, Observable, of } from 'rxjs'; +import { ContactFormModel, HolderContact } from '../../models/holder/contact'; + +@Injectable({ + providedIn: 'root' +}) +export class ContactService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + constructor(private http: HttpClient, private userService: UserService) { } + + getHolderContactsByHolderId(id: number): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetHolderContacts/${this.userService.getUserSpid()}/${id}`).pipe( + map(response => this.mapToContacts(response))); + } + + private mapToContacts(data: any[]): HolderContact[] { + return data.map(contact => ({ + HOLDERCONTACTID: contact.HOLDERCONTACTID, + HOLDERID: contact.HOLDERID, + SPID: contact.SPID, + FIRSTNAME: contact.FIRSTNAME, + LASTNAME: contact.LASTNAME, + MIDDLEINITIAL: contact.MIDDLEINITIAL || null, + TITLE: contact.TITLE, + PHONE: contact.PHONE, + MOBILE: contact.MOBILE, + FAX: contact.FAX || null, + EMAILADDRESS: contact.EMAILADDRESS, + DATECREATED: contact.DATECREATED || null, + CREATEDBY: contact.CREATEDBY || null, + INACTIVEFLAG: contact.INACTIVEFLAG || 'N', + INACTIVEDATE: contact.INACTIVEDATE || null, + LASTUPDATEDBY: contact.LASTUPDATEDBY || null, + LASTUPDATEDDATE: contact.LASTUPDATEDDATE || null + })); + } + + createHolderContact(HOLDERID: number, data: ContactFormModel): Observable { + const contact = { + P_SPID: this.userService.getUserSpid(), + P_HOLDERID: HOLDERID, + P_CONTACTSTABLE: [{ + P_FIRSTNAME: data.firstName, + P_LASTNAME: data.lastName, + P_MIDDLEINITIAL: data.middleInitial, + P_TITLE: data.title, + P_EMAILADDRESS: data.email, + P_MOBILENO: data.mobile, + P_PHONENO: data.phone, + P_FAXNO: data.fax + }], + P_USERID: this.userService.getUser() + } + + return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateHoldercontact`, contact); + } + + updateHolderContact(P_HOLDERCONTACTID: number, data: ContactFormModel): Observable { + const contact = { + P_HOLDERCONTACTID, + P_SPID: this.userService.getUserSpid(), + P_FIRSTNAME: data.firstName, + P_LASTNAME: data.lastName, + P_MIDDLEINITIAL: data.middleInitial, + P_TITLE: data.title, + P_EMAILADDRESS: data.email, + P_MOBILENO: data.mobile, + P_PHONENO: data.phone, + P_FAXNO: data.fax, + P_USERID: this.userService.getUser() + } + + console.log("update body : ", contact); + + + return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateHolderContact`, contact); + } + + InactivateHolderContact(HOLDERCONTACTID: number) { + return this.http.patch(`${this.apiUrl}/${this.apiDb}/InactivateHolderContact/${this.userService.getUserSpid()}/${HOLDERCONTACTID}/${this.userService.getSafeUser()}`, {}); + } + + ReactivateHolderContact(HOLDERCONTACTID: number) { + return this.http.patch(`${this.apiUrl}/${this.apiDb}/ReactivateHolderContact/${this.userService.getUserSpid()}/${HOLDERCONTACTID}/${this.userService.getSafeUser()}`, {}); + } +} diff --git a/src/app/core/services/holder/holder-detail.service.ts b/src/app/core/services/holder/holder-detail.service.ts new file mode 100644 index 0000000..8b4bc5f --- /dev/null +++ b/src/app/core/services/holder/holder-detail.service.ts @@ -0,0 +1,101 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { UserService } from '../common/user.service'; +import { environment } from '../../../../environments/environment'; +import { HolderDetail, HolderDetailFormType } from '../../models/holder/holder-details'; +import { filter, map, Observable, of, tap } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class HolderDetailService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + constructor(private http: HttpClient, private userService: UserService) { } + + getHolderDetailByHolderId(id: number): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetHolderRecord/${this.userService.getUserSpid()}/${id}`).pipe( + tap(response => console.log('Raw response got is :', response)), + // filter(response => response.length > 0), + map(response => this.mapToBasicDetail(response))); + } + + private mapToBasicDetail(basicDetails: any): HolderDetail { + return { + HOLDERID: basicDetails.HOLDERID, + SPID: basicDetails.SPID, + LOCATIONID: basicDetails.LOCATIONID, + HOLDERNO: basicDetails.HOLDERNO, + HOLDERTYPE: basicDetails.HOLDERTYPE, + USCIBMEMBERFLAG: basicDetails.USCIBMEMBERFLAG, + GOVAGENCYFLAG: basicDetails.GOVAGENCYFLAG, + HOLDERNAME: basicDetails.HOLDERNAME, + NAMEQUALIFIER: basicDetails.NAMEQUALIFIER || null, + ADDLNAME: basicDetails.ADDLNAME || null, + ADDRESS1: basicDetails.ADDRESS1, + ADDRESS2: basicDetails.ADDRESS2 || null, + CITY: basicDetails.CITY, + STATE: basicDetails.STATE, + ZIP: basicDetails.ZIP, + COUNTRY: basicDetails.COUNTRY, + DATECREATED: basicDetails.DATECREATED, + CREATEDBY: basicDetails.CREATEDBY, + INACTIVEFLAG: basicDetails.INACTIVEFLAG, + INACTIVEDATE: basicDetails.INACTIVEDATE || null, + LASTUPDATEDBY: basicDetails.LASTUPDATEDBY || null, + LASTUPDATEDDATE: basicDetails.LASTUPDATEDDATE || null + }; + } + + createHolderDetail(data: HolderDetailFormType): Observable { + const holderDetail = { + P_SPID: this.userService.getUserSpid(), + P_CLIENTLOCATIONID: this.userService.getUserLocationid(), + P_HOLDERNO: data.HolderNumber, + P_HOLDERTYPE: data.holderType, + P_USCIBMEMBERFLAG: data.USCIBMember, + P_GOVAGENCYFLAG: data.govAgency, + P_HOLDERNAME: data.holderName, + P_NAMEQUALIFIER: null, + P_ADDLNAME: data.DBAName, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_ZIP: data.zip, + P_COUNTRY: data.country, + P_USERID: this.userService.getUser() + }; + + console.log("body : ",holderDetail); + + + + return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateHolderData`, holderDetail); + } + + updateHolderDetails(id: number, data: HolderDetailFormType): Observable { + const holderDetail = { + P_HOLDERID: id, + P_SPID: this.userService.getUserSpid(), + P_LOCATIONID: this.userService.getUserLocationid(), + P_HOLDERNO: data.HolderNumber, + P_HOLDERTYPE: data.holderType, + P_USCIBMEMBERFLAG: data.USCIBMember, + P_GOVAGENCYFLAG: data.govAgency, + P_HOLDERNAME: data.holderName, + P_NAMEQUALIFIER: null, + P_ADDLNAME: data.DBAName, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_ZIP: data.zip, + P_COUNTRY: data.country, + P_USERID: this.userService.getUser() + }; + + return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateHolder`, holderDetail); + } +} diff --git a/src/app/core/services/holder/holder.service.ts b/src/app/core/services/holder/holder.service.ts new file mode 100644 index 0000000..c3ed3d1 --- /dev/null +++ b/src/app/core/services/holder/holder.service.ts @@ -0,0 +1,68 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { UserService } from '../common/user.service'; +import { map, Observable } from 'rxjs'; +import { HolderDetail } from '../../models/holder/holder-details'; + +@Injectable({ + providedIn: 'root' +}) +export class HolderService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + private userService = inject(UserService); + + searchHolders(filter: { holderName: string }): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/SearchHolder/${this.userService.getUserSpid()}/${filter.holderName}`).pipe( + map(response => this.mapToHolders(response)) + ) + } + + updateApplicationHolder(headerId: number, holderId: number) { + const applicationDetail = { + P_HEADERID: headerId, + P_HOLDERID: holderId + } + + return this.http.patch(`${this.apiUrl}/${this.apiDb}/update-holder`, applicationDetail); + } + + InactivateHolder(HOLDERID: number) { + return this.http.patch(`${this.apiUrl}/${this.apiDb}/InactivateHolder/${this.userService.getUserSpid()}/${HOLDERID}/${this.userService.getSafeUser()}`, {}); + } + + ReactivateHolder(HOLDERID: number) { + return this.http.patch(`${this.apiUrl}/${this.apiDb}/ReactivateHolder/${this.userService.getUserSpid()}/${HOLDERID}/${this.userService.getSafeUser()}`, {}); + } + + + private mapToHolders(data: any[]): HolderDetail[] { + return data.map((holderDetail: HolderDetail) => ({ + HOLDERID: holderDetail.HOLDERID, + SPID: holderDetail.SPID, + LOCATIONID: holderDetail.LOCATIONID, + HOLDERNO: holderDetail.HOLDERNO, + HOLDERTYPE: holderDetail.HOLDERTYPE, + USCIBMEMBERFLAG: holderDetail.USCIBMEMBERFLAG, + GOVAGENCYFLAG: holderDetail.GOVAGENCYFLAG, + HOLDERNAME: holderDetail.HOLDERNAME, + NAMEQUALIFIER: holderDetail.NAMEQUALIFIER, + ADDLNAME: holderDetail.ADDLNAME, + ADDRESS1: holderDetail.ADDRESS1, + ADDRESS2: holderDetail.ADDRESS2, + CITY: holderDetail.CITY, + STATE: holderDetail.STATE, + ZIP: holderDetail.ZIP, + COUNTRY: holderDetail.COUNTRY, + DATECREATED: holderDetail.DATECREATED, + CREATEDBY: holderDetail.CREATEDBY, + INACTIVEFLAG: holderDetail.INACTIVEFLAG, + INACTIVEDATE: holderDetail.INACTIVEDATE, + LASTUPDATEDBY: holderDetail.LASTUPDATEDBY, + LASTUPDATEDDATE: holderDetail.LASTUPDATEDDATE + })) + } +} diff --git a/src/app/holder/add/add.component.html b/src/app/holder/add/add.component.html new file mode 100644 index 0000000..1d67efe --- /dev/null +++ b/src/app/holder/add/add.component.html @@ -0,0 +1,26 @@ + + +
+ + + + + + Holder Details + + + + + + + Holder Contacts + + + + +
\ No newline at end of file diff --git a/src/app/holder/add/add.component.scss b/src/app/holder/add/add.component.scss new file mode 100644 index 0000000..eb6ca90 --- /dev/null +++ b/src/app/holder/add/add.component.scss @@ -0,0 +1,4 @@ +.back-btn{ + padding: 0 10px !important; + margin-bottom: 10px !important; +} \ No newline at end of file diff --git a/src/app/holder/add/add.component.ts b/src/app/holder/add/add.component.ts new file mode 100644 index 0000000..a87c1e3 --- /dev/null +++ b/src/app/holder/add/add.component.ts @@ -0,0 +1,67 @@ +import { Component, inject } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { HolderDetailComponent } from '../holder-detail/holder-detail.component'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { HolderContactsComponent } from '../holder-contacts/holder-contacts.component'; +import { UserPreferences } from '../../core/models/user-preference'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service'; +import { NavigationService } from '../../core/services/common/navigation.service'; + +@Component({ + selector: 'app-add', + imports: [ + AngularMaterialModule, + CommonModule, + ReactiveFormsModule, + HolderDetailComponent, + MatStepperModule, + MatFormFieldModule, + MatInputModule, + HolderContactsComponent + ], + templateUrl: './add.component.html', + styleUrl: './add.component.scss' +}) +export class AddComponent { + store = inject(CarnetStateStore); + currentStep = 0; + isLinear = true; + applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; + isEditMode = false; + holderid: number = 0; + userPreferences: UserPreferences; + stepsCompleted = { + holderDetails: false, + holderContacts: false + }; + constructor( + private userPrefenceService: UserPreferencesService, + private navigationService: NavigationService, + ) { + this.userPreferences = this.userPrefenceService.getPreferences(); + } + + onStepChange(event: StepperSelectionEvent): void { + this.currentStep = event.selectedIndex; + } + + onHolderIdCreated(holderid: number): void { + this.holderid = holderid; + this.stepsCompleted.holderDetails = true; + } + + goToEditCarnet(): void { + this.navigationService.navigate(["edit-carnet", this.store.headerid()], + { + state: { isEditMode: true } , + queryParams: { applicationname: this.store.applicationName() } + }) + } + +} diff --git a/src/app/holder/edit/edit.component.html b/src/app/holder/edit/edit.component.html new file mode 100644 index 0000000..2e6f0e4 --- /dev/null +++ b/src/app/holder/edit/edit.component.html @@ -0,0 +1,24 @@ + + +
+ + +
+ + + + Basic Details + + + + + + + + holder Contacts + + + + + \ No newline at end of file diff --git a/src/app/holder/edit/edit.component.scss b/src/app/holder/edit/edit.component.scss new file mode 100644 index 0000000..67a7cfc --- /dev/null +++ b/src/app/holder/edit/edit.component.scss @@ -0,0 +1,18 @@ +.page-header { + margin: 0.5rem 0px; + color: var(--mat-sys-primary); + font-weight: 500; +} + +.preparer-action-buttons { + padding-bottom: 20px; +} + +.preparer-headers-align .mat-expansion-panel-header-description { + justify-content: space-between; + align-items: center; +} + +.preparer-headers-align .mat-mdc-form-field+.mat-mdc-form-field { + margin-left: 8px; +} \ No newline at end of file diff --git a/src/app/holder/edit/edit.component.ts b/src/app/holder/edit/edit.component.ts new file mode 100644 index 0000000..14c2db8 --- /dev/null +++ b/src/app/holder/edit/edit.component.ts @@ -0,0 +1,39 @@ +import { CommonModule } from '@angular/common'; +import { afterNextRender, Component, viewChild } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { MatAccordion } from '@angular/material/expansion'; +import { UserPreferences } from '../../core/models/user-preference'; +import { ActivatedRoute } from '@angular/router'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { HolderDetailComponent } from '../holder-detail/holder-detail.component'; +import { HolderContactsComponent } from '../holder-contacts/holder-contacts.component'; + +@Component({ + selector: 'app-edit', + imports: [AngularMaterialModule, CommonModule, HolderDetailComponent, HolderContactsComponent], + templateUrl: './edit.component.html', + styleUrl: './edit.component.scss' +}) +export class EditComponent { + accordion = viewChild.required(MatAccordion); + isEditMode = true; + holderid = 0; + holderName: string | null = null; + userPreferences: UserPreferences; + + constructor(private route: ActivatedRoute, private userPrefenceService: UserPreferencesService) { + this.userPreferences = userPrefenceService.getPreferences(); + afterNextRender(() => { + this.accordion().openAll(); + }); + } + + ngOnInit(): void { + const idParam = this.route.snapshot.paramMap.get('holderid'); + this.holderid = idParam ? parseInt(idParam, 10) : 0; + } + + onClientNameUpdate(event: string): void { + this.holderName = event; + } +} diff --git a/src/app/holder/holder-contacts/holder-contacts.component.html b/src/app/holder/holder-contacts/holder-contacts.component.html new file mode 100644 index 0000000..0a1468d --- /dev/null +++ b/src/app/holder/holder-contacts/holder-contacts.component.html @@ -0,0 +1,264 @@ +
+
+ + Show Inactive Contacts + + + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
First Name{{ contact.FIRSTNAME }}Last Name{{ contact.LASTNAME }}Title{{ contact.TITLE }}Phone{{ contact.PHONE | phone }}Mobile{{ contact.MOBILE | phone }}Email{{ contact.EMAILADDRESS }}Actions +
+ + + + +
+ +
+ info + No records available +
+ + +
+ + +
+
+
+

{{ isEditing ? 'Edit Contact' : 'Add New Contact' }}

+
+ +
+ + First Name + + person + + First name is required + + + Maximum 50 characters allowed + + + + + Middle Initial + + + Only 1 character allowed + + + + + Last Name + + person + + Last name is required + + + Maximum 50 characters allowed + + +
+ +
+ + + Title + + work + + Title is required + + + Maximum 100 characters allowed + + +
+ +
+ + Phone + + phone + + Phone is required + + + Please enter a valid phone number (10-15 digits) + + + + + Mobile + + smartphone + + Mobile is required + + + Please enter a valid mobile number (10-15 digits) + + +
+ +
+ + Fax + + fax + + Please enter a valid fax number (10-15 digits) + + + + + Email + + email + + Email is required + + + Please enter a valid email address + + + Maximum 100 characters allowed + + +
+ +
+
+
+ +
+ +
+ {{contactReadOnlyFields.lastChangedBy || 'N/A'}} +
+
+ +
+ +
+ {{contactReadOnlyFields.isInactive === true ? 'Yes' : 'No' }} +
+
+
+
+ + +
+ +
+ {{(contactReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}} +
+
+ + +
+ +
+ {{(contactReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}} +
+
+
+
+
+ +
+ + +
+
+
+
\ No newline at end of file diff --git a/src/app/holder/holder-contacts/holder-contacts.component.scss b/src/app/holder/holder-contacts/holder-contacts.component.scss new file mode 100644 index 0000000..eed276f --- /dev/null +++ b/src/app/holder/holder-contacts/holder-contacts.component.scss @@ -0,0 +1,354 @@ +.contacts-container { + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; + + .actions-bar { + clear: both; + margin-bottom: -16px; + + mat-slide-toggle { + transform: scale(0.8); + margin-left: -0.5rem; + } + + button { + float: right; + } + } + + .table-container { + position: relative; + overflow: auto; + border-radius: 8px; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + } + + mat-table { + width: 100%; + + mat-icon { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + + .mat-column-actions { + width: 180px; + text-align: center; + } + + .mat-column-defaultContact { + width: 80px; + text-align: center; + } + } + + .no-data-message { + text-align: center; + padding: 0.9rem; + color: rgba(0, 0, 0, 0.54); + + mat-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + margin-bottom: -3px; + } + } + + mat-paginator { + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 0 0 8px 8px; + padding-top: 4px; + } + } + + .form-container { + background-color: white; + padding: 24px; + border-radius: 8px; + margin-top: 16px; + + .form-header { + margin-bottom: 24px; + + h3 { + margin: 0; + color: var(--mat-sys-primary); + font-weight: 500; + } + } + + form { + display: flex; + flex-direction: column; + gap: 16px; + + .form-row { + display: flex; + gap: 16px; + + mat-form-field { + flex: 1; + } + + .small-field { + max-width: 120px; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 16px; + } + + .readonly-section { + padding-top: 0.5rem; + border-top: 1px solid #eee; + + .readonly-fields { + display: flex; + gap: 2rem; + + .field-column { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.5rem; + } + } + + .readonly-field { + label { + display: block; + font-size: 0.875rem; + color: #666; + margin-bottom: 0.25rem; + } + + .readonly-value { + padding: 0.25rem; + font-size: 0.9375rem; + display: flex; + align-items: center; + } + } + } + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .contacts-container { + padding: 16px; + + .form-row { + flex-direction: column; + gap: 16px !important; + + .small-field { + max-width: 100% !important; + } + } + } +} + +.contacts-container { + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; + + .actions-bar { + clear: both; + margin-bottom: -16px; + + mat-slide-toggle { + transform: scale(0.8); + margin-left: -0.5rem; + } + + button { + float: right; + } + } + + .table-container { + position: relative; + overflow: auto; + border-radius: 8px; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + } + + mat-table { + width: 100%; + + .rowActions { + display: flex !important; + align-items: center; + justify-content: center; + gap: 8px; + } + + mat-icon { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + + .mat-column-actions { + width: 180px; + text-align: center; + } + + .mat-column-defaultContact { + width: 80px; + text-align: center; + } + } + + .no-data-message { + text-align: center; + padding: 0.9rem; + color: rgba(0, 0, 0, 0.54); + + mat-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + margin-bottom: -3px; + } + } + + mat-paginator { + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 0 0 8px 8px; + padding-top: 4px; + } + } + + .form-container { + background-color: white; + padding: 24px; + border-radius: 8px; + margin-top: 16px; + + .form-header { + margin-bottom: 24px; + + h3 { + margin: 0; + color: var(--mat-sys-primary); + font-weight: 500; + } + } + + form { + display: flex; + flex-direction: column; + gap: 16px; + + .form-row { + display: flex; + gap: 16px; + + mat-form-field { + flex: 1; + } + + .small-field { + max-width: 120px; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 16px; + } + + .readonly-section { + padding-top: 0.5rem; + border-top: 1px solid #eee; + + .readonly-fields { + display: flex; + gap: 2rem; + + .field-column { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.5rem; + } + } + + .readonly-field { + label { + display: block; + font-size: 0.875rem; + color: #666; + margin-bottom: 0.25rem; + } + + .readonly-value { + padding: 0.25rem; + font-size: 0.9375rem; + display: flex; + align-items: center; + } + } + } + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .contacts-container { + padding: 16px; + + .form-row { + flex-direction: column; + gap: 16px !important; + + .small-field { + max-width: 100% !important; + } + } + } +} \ No newline at end of file diff --git a/src/app/holder/holder-contacts/holder-contacts.component.ts b/src/app/holder/holder-contacts/holder-contacts.component.ts new file mode 100644 index 0000000..a93915f --- /dev/null +++ b/src/app/holder/holder-contacts/holder-contacts.component.ts @@ -0,0 +1,203 @@ +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { PhonePipe } from '../../shared/pipes/phone.pipe'; +import { CommonModule } from '@angular/common'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { UserPreferences } from '../../core/models/user-preference'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { ContactFormModel, HolderContact } from '../../core/models/holder/contact'; +import { ContactService } from '../../core/services/holder/contact.service'; + +@Component({ + selector: 'app-holder-contacts', + imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule], + templateUrl: './holder-contacts.component.html', + styleUrl: './holder-contacts.component.scss' +}) +export class HolderContactsComponent { + @Input() isEditMode: boolean = false; + @Input() holderid: number = 0; + @Input() userPreferences: UserPreferences = {}; + + @Output() hasContacts = new EventEmitter(); + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort!: MatSort; + + displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'mobile', 'email', 'actions']; + dataSource = new MatTableDataSource(); + contactForm: FormGroup; + + isEditing = false; + currentContactId: number | null = null; + isLoading = false; + showForm = false; + showInactiveContacts = false; + contacts: HolderContact[] = []; + + contactReadOnlyFields: any = { + lastChangedDate: null, + lastChangedBy: null, + isInactive: null, + inactivatedDate: null + }; + + constructor( + private fb: FormBuilder, + private contactService: ContactService, + private notificationService: NotificationService, + private errorHandler: ApiErrorHandlerService + ) { + this.contactForm = this.fb.group({ + firstName: ['', [Validators.required, Validators.maxLength(50)]], + lastName: ['', [Validators.required, Validators.maxLength(50)]], + middleInitial: ['', [Validators.maxLength(1)]], + title: ['', [Validators.required, Validators.maxLength(100)]], + phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], + mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], + fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]], + email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]], + }); + } + + ngOnInit(): void { + if (this.holderid > 0) { + this.loadContacts(); + } + } + + InactivateHolderContact(HOLDERCONTACTID: number): void { + this.contactService.InactivateHolderContact(HOLDERCONTACTID).subscribe( + { + next: (basicData: any) => { + this.notificationService.showSuccess(`HolderContact Inactivated successfully`); + this.loadContacts() + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to Inactivate HolderContact`); + this.notificationService.showError(errorMessage); + console.error('Error Inactivating HolderContact:', error); + } + } + ); + } + + ReactivateHolderContact(HOLDERCONTACTID: number): void { + this.contactService.ReactivateHolderContact(HOLDERCONTACTID).subscribe( + { + next: (basicData: any) => { + this.notificationService.showSuccess(`HolderContact Reactivated successfully`); + this.loadContacts() + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to Reactivate HolderContact`); + this.notificationService.showError(errorMessage); + console.error('Error Reactivating HolderContact:', error); + } + } + ); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + loadContacts(): void { + this.isLoading = true; + + this.contactService.getHolderContactsByHolderId(this.holderid).subscribe({ + next: (contacts: HolderContact[]) => { + this.contacts = contacts; + this.renderContacts(); + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading contacts:', error); + } + }); + } + + addNewContact(): void { + this.showForm = true; + this.isEditing = false; + this.currentContactId = null; + this.contactForm.reset(); + } + + editContact(contact: HolderContact): void { + this.showForm = true; + this.isEditing = true; + this.currentContactId = contact.HOLDERCONTACTID; + + this.contactForm.patchValue({ + firstName: contact.FIRSTNAME, + lastName: contact.LASTNAME, + middleInitial: contact.MIDDLEINITIAL, + title: contact.TITLE, + phone: contact.PHONE, + mobile: contact.MOBILE, + fax: contact.FAX, + email: contact.EMAILADDRESS + }); + + this.contactReadOnlyFields.lastChangedDate = contact.LASTUPDATEDDATE ?? contact.DATECREATED; + this.contactReadOnlyFields.lastChangedBy = contact.LASTUPDATEDBY ?? contact.CREATEDBY; + this.contactReadOnlyFields.isInactive = contact.INACTIVEFLAG === 'Y'; + this.contactReadOnlyFields.inactivatedDate = contact.INACTIVEDATE; + } + + saveContact(): void { + if (this.contactForm.invalid && !(this.holderid > 0)) { + this.contactForm.markAllAsTouched(); + return; + } + + const contactData: ContactFormModel = this.contactForm.value; + + const saveObservable = this.isEditing && (this.currentContactId! > 0) + ? this.contactService.updateHolderContact(this.currentContactId!, contactData) + : this.contactService.createHolderContact(this.holderid, contactData); + + saveObservable.subscribe({ + next: () => { + this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`); + this.loadContacts(); + this.cancelEdit(); + this.hasContacts.emit(true); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} contact`); + this.notificationService.showError(errorMessage); + console.error('Error saving contact:', error); + } + }); + } + + toggleShowInactiveContacts(): void { + this.showInactiveContacts = !this.showInactiveContacts; + this.renderContacts(); + } + + renderContacts(): void { + if (this.showInactiveContacts) { + this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'Y'); + } else { + this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'N'); + } + } + + cancelEdit(): void { + this.showForm = false; + this.isEditing = false; + this.currentContactId = null; + this.contactForm.reset(); + } +} diff --git a/src/app/holder/holder-detail/holder-detail.component.html b/src/app/holder/holder-detail/holder-detail.component.html new file mode 100644 index 0000000..d61bd3f --- /dev/null +++ b/src/app/holder/holder-detail/holder-detail.component.html @@ -0,0 +1,166 @@ +
+ + +
+ +
+ +
+ + +
+ + Holder Name + + + Name is required + + + Maximum 100 characters allowed + + + + + DBA Name + + + Lookup code is required + + + Maximum 20 characters allowed + + +
+
+ + Tax ID No + + + Lookup code is required + + + Maximum 20 characters allowed + + +
+
+
+ + Holder Type : + Corporation + Individual + Government Agency + +
+ +
+ +
+
+ + Are you a member of USCIB ? + Yes + No + +
+
+ +
+
+ + Are you belong to Government Agency ? + Yes + No + +
+
+ + +
+ + Address Line 1 + + + Address is required + + + Maximum 100 characters allowed + + +
+ +
+ + Address Line 2 (Optional) + + + Maximum 100 characters allowed + + +
+ + +
+ + City + + + City is required + + + Maximum 50 characters allowed + + + + + Country + + + {{ country.name }} + + + + Country is required + + + + + State/Province + + + {{ state.name }} + + + + State is required + + + + + ZIP/Postal Code + + + ZIP/Postal code is required + + + Please enter a valid 5-digit US ZIP code + + + Please enter a valid postal code (e.g., A1B2C3) + + +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/app/holder/holder-detail/holder-detail.component.scss b/src/app/holder/holder-detail/holder-detail.component.scss new file mode 100644 index 0000000..e27cd65 --- /dev/null +++ b/src/app/holder/holder-detail/holder-detail.component.scss @@ -0,0 +1,125 @@ +.basic-details-container { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + + .details-card { + overflow: hidden; + transition: all 0.3s ease; + position: relative; + background: none; + box-shadow: none; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .details-form { + display: flex; + flex-direction: column; + gap: 16px; + } + + .form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + align-items: start; + + .question-row { + margin-bottom: 1rem; + // padding: 1rem; + // background: #ffff; + border-radius: 4px; + // box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + mat-radio-group { + display: flex; + // flex-direction: column; + align-items: center; + gap: 8px; + + mat-radio-button { + margin-right: 1rem; + } + } + } + + .name { + grid-column: span 2; + } + + .lookup-code { + grid-column: span 1; + } + + .HolderNumber{ + grid-column: span 1; + // background-color: azure; + margin: 0; + } + + .address1, + .address2 { + grid-column: span 3; + } + + .city, + .state, + .country, + .zip { + grid-column: span 1; + } + + .carnet-issuing-region, + .revenue-location { + grid-column: span 1; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + } + + mat-form-field { + width: 100%; + } + } +} + +@media (max-width: 960px) { + .basic-details-container { + .details-card { + .form-row { + grid-template-columns: 1fr; + + .name, + .lookup-code, + .address1, + .address2, + .city, + .state, + .zip, + .country, + .carnet-issuing-region, + .revenue-location { + grid-column: span 1; + } + } + + + } + } +} \ No newline at end of file diff --git a/src/app/holder/holder-detail/holder-detail.component.ts b/src/app/holder/holder-detail/holder-detail.component.ts new file mode 100644 index 0000000..a231c18 --- /dev/null +++ b/src/app/holder/holder-detail/holder-detail.component.ts @@ -0,0 +1,205 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { State } from '../../core/models/state'; +import { Region } from '../../core/models/region'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { ZipCodeValidator } from '../../shared/validators/zipcode-validator'; +import { HolderDetail, HolderDetailFormType } from '../../core/models/holder/holder-details'; +import { HolderDetailService } from '../../core/services/holder/holder-detail.service'; +import { CommonService } from '../../core/services/common/common.service'; +import { Subject, takeUntil } from 'rxjs'; +import { Country } from '../../core/models/country'; + +@Component({ + selector: 'app-holder-detail', + imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule], + templateUrl: './holder-detail.component.html', + styleUrl: './holder-detail.component.scss' +}) +export class HolderDetailComponent implements OnInit, OnDestroy { + + @Input() isEditMode = false; + @Input() holderid: number = 0; + + @Output() holderIdCreated = new EventEmitter(); + @Output() clientName = new EventEmitter(); + + holderDetailsForm: FormGroup; + countries: Country[] = []; + regions: Region[] = []; + states: State[] = []; + + isLoading = false; + countriesHasStates = ['US', 'CA', 'MX']; + + private destroy$ = new Subject(); + + constructor( + private fb: FormBuilder, + private holderDetailService: HolderDetailService, + private notificationService: NotificationService, + private commonService: CommonService, + private errorHandler: ApiErrorHandlerService) { + this.holderDetailsForm = this.createForm(); + } + + createForm(): FormGroup { + return this.fb.group({ + holderName: ['', [Validators.required, Validators.maxLength(100)]], + DBAName: ['', [Validators.maxLength(20)]], + HolderNumber: ['', [Validators.required, Validators.maxLength(20)]], + holderType: ['', [Validators.required, Validators.maxLength(20)]], + USCIBMember: ['', [Validators.required]], + govAgency: ['', [Validators.required]], + address1: ['', [Validators.required, Validators.maxLength(100)]], + address2: ['', Validators.maxLength(100)], + city: ['', [Validators.required, Validators.maxLength(50)]], + state: ['', Validators.required], + country: ['', Validators.required], + zip: ['', [Validators.required, ZipCodeValidator('country')]] + }); + } + + ngOnInit(): void { + this.loadLookupData(); + + if (this.holderid > 0) { + this.isLoading = true; + this.holderDetailService.getHolderDetailByHolderId(this.holderid).subscribe({ + next: (basicDetail: HolderDetail) => { + this.patchFormData(basicDetail); + this.clientName.emit(basicDetail.HOLDERNAME); + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load holder details'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading holder details:', error); + } + }); + + } + } + + patchFormData(data: any): void { + + this.holderDetailsForm.patchValue({ + holderName: data.HOLDERNAME, + DBAName: data.ADDLNAME, + HolderNumber: data.HOLDERNO, + holderType: data.HOLDERTYPE, + USCIBMember: data.USCIBMEMBERFLAG, + govAgency: data.GOVAGENCYFLAG, + address1: data.ADDRESS1, + address2: data.ADDRESS2, + city: data.CITY, + state: data.STATE, + country: data.COUNTRY, + zip: data.ZIP + }) + + if (data.COUNTRY) { + this.loadStates(data.COUNTRY); + } + } + + get f() { + return this.holderDetailsForm.controls; + } + + onCountryChange(country: string): void { + this.holderDetailsForm.get('state')?.reset(); + + if (country) { + this.loadStates(country); + } + + this.holderDetailsForm.get('zip')?.updateValueAndValidity(); + } + + saveHolderDetails() { + if (this.holderDetailsForm.invalid) { + this.holderDetailsForm.markAllAsTouched(); + return; + } + + const holderDetailData: HolderDetailFormType = this.holderDetailsForm.value; + + const saveObservable = this.isEditMode && this.holderid > 0 + ? this.holderDetailService.updateHolderDetails(this.holderid, holderDetailData) + : this.holderDetailService.createHolderDetail(holderDetailData); + + saveObservable.subscribe({ + next: (basicData: any) => { + this.notificationService.showSuccess(`Holder details ${this.isEditMode ? 'updated' : 'added'} successfully`); + + if (!this.isEditMode) { + this.holderIdCreated.emit(basicData.HOLDERID); + } + + if (this.isEditMode) { + this.clientName.emit(holderDetailData.holderName); + } + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} Holder details`); + this.notificationService.showError(errorMessage); + console.error('Error saving basic details:', error); + } + }); + + } + + updateStateControl(controlName: string, country: string): void { + const stateControl = this.holderDetailsForm.get(controlName); + if (this.countriesHasStates.includes(country)) { + stateControl?.enable(); + } else { + stateControl?.disable(); + stateControl?.setValue('FN'); + } + } + + loadLookupData(): void { + + this.commonService.getCountries(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (countries) => { + this.countries = countries; + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load countries', error); + this.isLoading = false; + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + loadStates(country: string): void { + this.isLoading = true; + country = this.countriesHasStates.includes(country) ? country : 'FN'; + this.commonService.getStates(country, this.holderid) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (states) => { + this.states = states; + this.updateStateControl('state', country); + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load states', error); + this.isLoading = false; + } + }); + } +} diff --git a/src/app/holder/search/search.component.html b/src/app/holder/search/search.component.html new file mode 100644 index 0000000..e67fefb --- /dev/null +++ b/src/app/holder/search/search.component.html @@ -0,0 +1,131 @@ +
+ + +
+
+
+
+ + Holder Name + + search + +
+
+ +
+ + + + +
+
+
+ +
+
+ +
+ +
+ + Show Inactive Holders + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Holder Name{{ client.HOLDERNAME }}DBA Name{{ client.ADDLNAME || '--'}}Address{{ client.ADDRESS1 }}USCIB Member{{ client.USCIBMEMBERFLAG === 'Y' ? 'Yes' : 'No' }}Holder Type{{ client.HOLDERTYPE }}Actions + + + + + + + + +
+ info + No records found matching your criteria +
+ + + +
+ +
+ + +
+
\ No newline at end of file diff --git a/src/app/holder/search/search.component.scss b/src/app/holder/search/search.component.scss new file mode 100644 index 0000000..90cb630 --- /dev/null +++ b/src/app/holder/search/search.component.scss @@ -0,0 +1,135 @@ +.page-header { + margin: 0.5rem 0px; + color: var(--mat-sys-primary); + font-weight: 500; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 16px; +} + +.manage-preparers-container { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + + .search-fields { + display: flex; + flex-direction: column; + gap: 16px; + + .form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + align-items: start; + + .name, + .address { + grid-column: span 2; + } + + .city, + .state, + .lookup-code { + grid-column: span 1; + } + } + } + + .search-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + + button { + display: flex; + align-items: center; + } + } + + .results-section { + position: relative; + overflow: auto; + border-radius: 8px; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + } + + mat-table { + width: 100%; + + mat-icon { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + + .mat-column-actions { + width: 180px; + text-align: center; + } + + .mat-column-defaultContact { + width: 80px; + text-align: center; + } + } + + .no-data-message { + text-align: center; + padding: 0.9rem; + color: rgba(0, 0, 0, 0.54); + + mat-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + margin-bottom: -3px; + } + } + + mat-paginator { + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 0 0 8px 8px; + padding-top: 4px; + } + } +} + +@media (max-width: 960px) { + .manage-preparers-container { + .search-form { + .search-fields { + .form-row { + grid-template-columns: 1fr; + + .name, + .address, + .city, + .state, + .lookup-code { + grid-column: span 1; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/app/holder/search/search.component.ts b/src/app/holder/search/search.component.ts new file mode 100644 index 0000000..c361e43 --- /dev/null +++ b/src/app/holder/search/search.component.ts @@ -0,0 +1,201 @@ +import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { HolderDetail } from '../../core/models/holder/holder-details'; +import { NavigationService } from '../../core/services/common/navigation.service'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { MatTableDataSource } from '@angular/material/table'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { UserPreferences } from '../../core/models/user-preference'; +import { MatSort } from '@angular/material/sort'; +import { MatPaginator } from '@angular/material/paginator'; +import { HolderService } from '../../core/services/holder/holder.service'; + +@Component({ + selector: 'app-search', + imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule], + templateUrl: './search.component.html', + styleUrl: './search.component.scss' +}) +export class SearchComponent { + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort!: MatSort; + + @Output() selectHolderCompleted = new EventEmitter(); + + @Input() headerid: number = 0; + @Input() HOLDERID: number = 0 + + selectedHolder: any = null; + selectedHolderId: number = 0; + HolderSaved: boolean = false; + showInactiveHolders: boolean = false; + holdersSearched: any = [] + isLoading: boolean = false; + + userPreferences: UserPreferences; + searchForm: FormGroup; + + private holderService = inject(HolderService); + private errorHandler = inject(ApiErrorHandlerService); + private notificationService = inject(NotificationService); + dataSource = new MatTableDataSource([]); + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + private fb = inject(FormBuilder); + + displayedColumns: string[] = ['HolderName', 'DBAName', 'Address', 'USCIBMember', 'HolderType', 'actions']; + + constructor( + private userPrefenceService: UserPreferencesService, + private navigationService: NavigationService, + ) { + this.userPreferences = this.userPrefenceService.getPreferences(); + this.searchForm = this.createSearchForm(); + } + + createSearchForm(): FormGroup { + return this.fb.group({ + holderName: [''] + }); + } + + ngOnInit(): void { + if (this.HOLDERID > 0) { + this.selectedHolderId = this.HOLDERID; + } + } + + searchHolders(): void { + + this.isLoading = true; + + const filterData: { holderName: string } = this.searchForm.value; + + this.holderService.searchHolders(filterData).subscribe({ + next: (holders: HolderDetail[]) => { + this.holdersSearched = holders + this.dataSource.data = this.holdersSearched; + this.renderHolders() + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to search preparers'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading preparers:', error); + } + }); + } + + isSearchCriteriaProvided(): boolean { + const values = this.searchForm.value; + return !!values.holderName + } + + onSearch(): void { + if (this.searchForm.invalid || !this.isSearchCriteriaProvided()) { + return; + } + this.searchHolders(); + } + + completeSelection() { + this.saveApplication(); + } + + saveApplication(): void { + const saveObservable = this.holderService.updateApplicationHolder(this.headerid, this.selectedHolderId ? Number(this.selectedHolderId) : 0); + + saveObservable.subscribe({ + next: (basicData: any) => { + this.notificationService.showSuccess(`Holder updated successfully`); + this.selectHolderCompleted.emit(true); + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to update holder details`); + this.notificationService.showError(errorMessage); + console.error('Error saving holder details:', error); + } + }); + } + + toggleShowInactiveHolders(): void { + this.showInactiveHolders = !this.showInactiveHolders; + this.renderHolders(); + } + + renderHolders() { + if (this.showInactiveHolders) { + this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'Y'); + } else { + this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'N'); + } + } + + addNewHolder(): void { + this.navigationService.navigate(["add-holder"], { state: { isEditMode: false } }) + } + + onEdit(id: string) { + this.navigationService.navigate(['edit-holder', id]); + } + + cancelEdit() { + this.clearSelection(); + } + + clearSelection(): void { + this.selectedHolder = null; + this.selectedHolderId = 0; + } + + InactivateHolder(HOLDERID: number): void { + this.holderService.InactivateHolder(HOLDERID).subscribe( + { + next: (basicData: any) => { + this.notificationService.showSuccess(`Holder Inactivated successfully`); + this.searchHolders(); + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to Inactivate Holder`); + this.notificationService.showError(errorMessage); + console.error('Error Inactivating Holder:', error); + } + } + ); + } + + ReactivateHolder(HOLDERID: number): void { + this.holderService.ReactivateHolder(HOLDERID).subscribe( + { + next: (basicData: any) => { + this.notificationService.showSuccess(`Holder Reactivated successfully`); + this.searchHolders(); + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to Reactivate Holder`); + this.notificationService.showError(errorMessage); + console.error('Error Reactivating Holder:', error); + } + } + ); + } + + onClearSearch(): void { + this.searchForm.reset(); + this.dataSource.data = [] + this.holdersSearched = []; + } + + onRadioSelect(holder: any): void { + this.selectedHolder = holder; + this.selectedHolderId = holder.HOLDERID; + } +} diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index be98861..4c417ca 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -16,6 +16,7 @@ import { NotificationService } from '../core/services/common/notification.servic import { NavigationService } from '../core/services/common/navigation.service'; import { MatRadioChange } from '@angular/material/radio'; import * as XLSX from 'xlsx'; +import { CarnetStateStore } from '../core/services/carnet/carnet-application.service'; @Component({ selector: 'app-home', @@ -25,6 +26,7 @@ import * as XLSX from 'xlsx'; providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], }) export class HomeComponent { + store = inject(CarnetStateStore); carnetData: any[] = []; isLoading = false; showTable = false; @@ -149,6 +151,7 @@ export class HomeComponent { newCarnet(event: MatRadioChange): void { if (event.value) { + this.store.resetState(); this.navigateTo(['add-carnet']); } }