diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3b1b0e6..0095db4 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -14,6 +14,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 ab7f433..1a493e7 100644 --- a/src/app/carnet/add/add-carnet.component.html +++ b/src/app/carnet/add/add-carnet.component.html @@ -4,64 +4,69 @@ [selectedIndex]="currentStep"> - + Application Name - + - + Holder Selection - + + - + Goods Section - + - + Travel Plan - + - + Insurance - + - + Shipping - + - + Delivery Method - + - + Payment - + diff --git a/src/app/carnet/add/add-carnet.component.ts b/src/app/carnet/add/add-carnet.component.ts index d8d44eb..b95e7b5 100644 --- a/src/app/carnet/add/add-carnet.component.ts +++ b/src/app/carnet/add/add-carnet.component.ts @@ -13,6 +13,11 @@ import { ShippingComponent } from '../shipping/shipping.component'; import { DeliveryComponent } from '../delivery/delivery.component'; import { SearchComponent } from '../../holder/search/search.component'; +import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service'; +import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; +import { filter, Subscription } from 'rxjs'; +import { UserService } from '../../core/services/common/user.service'; + @Component({ selector: 'app-add-carnet', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, @@ -22,59 +27,123 @@ import { SearchComponent } from '../../holder/search/search.component'; styleUrl: './add-carnet.component.scss' }) export class AddCarnetComponent { + store = inject(CarnetStateStore); + private router = inject(Router); + private route = inject(ActivatedRoute); + private navigationSub!: Subscription; + + private previousUrl: string = ''; + currentStep = 0; isLinear = true; - applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; - isEditMode = false; - headerid: number = 0; - // Track completion of each step - stepsCompleted = { - applicationDetail: false, - holderSelection: false, - goodsSection: false, - travelPlan: false, - insurance: false, - shipping: false, - deliveryMethod: false, - payment: false - }; + stepsCompleted = this.store.stepsCompleted(); + + constructor( + private readonly userService: UserService, + ) { } + + ngOnInit() { + + this.previousUrl = this.router.url; // Capture initial + + console.log("onit url : ", this.previousUrl); + + console.log(this.store.stepsCompleted().payment); + console.log(this.store.stepsCompleted().goodsSection); + + this.navigationSub = this.router.events.pipe( + filter(e => e instanceof NavigationStart) + ).subscribe((event: NavigationStart) => { + const nextUrl = event.url; + + const from = this.previousUrl; + const to = nextUrl; + + const isFromCreateFlow = this.isCreateFlowUrl(from); + const isToCreateFlow = this.isCreateFlowUrl(to); + + // Only reset if we're entering the create flow from outside it + if (!isFromCreateFlow && isToCreateFlow) { + this.store.resetState(); + } + + // Update previous URL for the next nav + this.previousUrl = nextUrl; + }); + } + + ngOnDestroy() { + this.navigationSub?.unsubscribe(); + } + + private isCreateFlowUrl(url: string): boolean { + const segments = url.split('?')[0].split('/').filter(Boolean); // Remove query and empty strings + + // You want to check the SECOND segment, i.e., segments[1] + const secondSegment = segments[1] || ''; // Avoid undefined + + return ['add-carnet', 'add-holder', 'edit-holder'].includes(secondSegment.toLowerCase()); + } + onStepChange(event: StepperSelectionEvent): void { this.currentStep = event.selectedIndex; } - onApplicationDetailCreated(headerid: number): void { - this.headerid = headerid; - this.stepsCompleted.applicationDetail = true; + onSearchedHolder(searchedHolder: string): void { + this.store.searchedHolder.set(searchedHolder); + } + + onApplicationDetailCreated(data: { HEADERID: number, APPLICATION_NAME: string }): void { + // this.headerid = headerid; + this.store.headerId.set(data.HEADERID); + this.store.applicationName.set(data.APPLICATION_NAME) + + console.log("logging application emits : ", data); + console.log("logging application name after setting : ", this.store.applicationName()); + + // this.stepsCompleted.applicationDetail = true; + this.store.stepsCompleted.update(s => ({ ...s, applicationDetail: true })) this.isLinear = false; // Disable linear mode after application detail is created } - onHolderSelectionSaved(completed: boolean): void { - this.stepsCompleted.holderSelection = completed; + onHolderSelectionSaved(HOLDERID: number): void { + // this.stepsCompleted.holderSelection = true; + this.store.holderid.set(HOLDERID); + this.store.stepsCompleted.update(s => ({ ...s, holderSelection: true })) } onGoodsSectionSaved(completed: boolean): void { - this.stepsCompleted.goodsSection = completed; + // this.stepsCompleted.goodsSection = completed; + this.store.stepsCompleted.update(s => ({ ...s, goodsSection: completed })) + } onTravelPlanSaved(completed: boolean): void { - this.stepsCompleted.travelPlan = completed; + // this.stepsCompleted.travelPlan = completed; + this.store.stepsCompleted.update(s => ({ ...s, travelPlan: completed })) + } onInsuranceSaved(completed: boolean): void { - this.stepsCompleted.insurance = completed; + // this.stepsCompleted.insurance = completed; + this.store.stepsCompleted.update(s => ({ ...s, insurance: completed })) + } onShippingSaved(completed: boolean): void { - this.stepsCompleted.shipping = completed; + // this.stepsCompleted.shipping = completed; + this.store.stepsCompleted.update(s => ({ ...s, shipping: completed })) } onDeliveryMethodSaved(completed: boolean): void { - this.stepsCompleted.deliveryMethod = completed; + // this.stepsCompleted.deliveryMethod = completed; + this.store.stepsCompleted.update(s => ({ ...s, deliveryMethod: completed })) } onPaymentSaved(completed: boolean): void { - this.stepsCompleted.payment = completed; + // this.stepsCompleted.payment = completed; + this.store.stepsCompleted.update(s => ({ ...s, payment: completed })) } } diff --git a/src/app/carnet/application/application.component.ts b/src/app/carnet/application/application.component.ts index d8b07d4..725e151 100644 --- a/src/app/carnet/application/application.component.ts +++ b/src/app/carnet/application/application.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { Component, EventEmitter, inject, Input, Output, SimpleChanges } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ApplicationDetailService } from '../../core/services/carnet/application-detail.service'; import { NotificationService } from '../../core/services/common/notification.service'; @@ -20,7 +20,7 @@ export class ApplicationComponent { @Input() headerid: number = 0; @Input() applicationName: string = ''; - @Output() headerIdCreated = new EventEmitter(); + @Output() headerIdCreated = new EventEmitter<{ HEADERID: number, APPLICATION_NAME: string }>(); applicationDetailsForm: FormGroup; isLoading = false; @@ -38,8 +38,20 @@ export class ApplicationComponent { this.applicationDetailsForm = this.createForm(); } + // ngOnChanges(changes: SimpleChanges) { + // if (changes['applicationName']) { + // console.log('applicationName input:', this.applicationName); + // } + // if (changes['headerid']) { + // console.log('headerid input:', this.headerid); + // } + // } + ngOnInit(): void { - this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || ''; + // this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || ''; + + console.log("a-------- : ", this.applicationName); + // Patch edit form data if (this.applicationName) { @@ -78,7 +90,7 @@ export class ApplicationComponent { name: this.applicationName, }); - if (this.isEditMode) { + if (this.isEditMode || this.applicationName) { this.applicationDetailsForm.get('name')?.disable(); this.disableSaveButton = true; } @@ -101,8 +113,10 @@ export class ApplicationComponent { this.isLoading = true; this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({ next: (applicationData: any) => { + console.log("app data : ",applicationData); + 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; @@ -114,6 +128,14 @@ export class ApplicationComponent { this.isLoading = false; } }); + + console.log("application details : ", applicationDetailData); + + + // this.headerIdCreated.emit({ HEADERID: 1001, APPLICATION_NAME: applicationDetailData.name }); + // this.applicationDetailsForm.get('name')?.disable(); + // this.disableSaveButton = true; + // this.isLoading = false; } } } 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-detail.ts b/src/app/core/models/holder/holder-detail.ts index c0f9499..feb1239 100644 --- a/src/app/core/models/holder/holder-detail.ts +++ b/src/app/core/models/holder/holder-detail.ts @@ -1,5 +1,5 @@ export interface HolderDetail { - HOLDERID: number; + HOLDERID?: number; SPID: number; LOCATIONID: number; HOLDERNO: string; @@ -22,3 +22,19 @@ export interface HolderDetail { 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; +} + 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..952da41 --- /dev/null +++ b/src/app/core/services/carnet/carnet-application.service.ts @@ -0,0 +1,46 @@ +// appli.service.ts +import { Injectable, signal } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CarnetStateStore { + headerId = signal(0); + applicationName = signal(''); + searchedHolder = signal(''); + holderid = signal(0); + applicationType = signal<'new' | 'additional' | 'duplicate' | 'extend' | null>('new'); + isEditMode = signal(false); + stepsCompleted = signal({ + applicationDetail: false, + holderSelection: false, + goodsSection: false, + travelPlan: false, + insurance: false, + shipping: false, + deliveryMethod: false, + payment: false + }); + + resetState() { + console.log("CALLED RESET STATE"); + + this.headerId.set(0); + this.applicationName.set(''); + this.searchedHolder.set(''); + this.holderid.set(0); + this.applicationType.set('new'); + this.isEditMode.set(false); + this.stepsCompleted.set({ + applicationDetail: false, // this is intentionally true on reset? + holderSelection: false, + goodsSection: false, + travelPlan: false, + insurance: false, + shipping: false, + deliveryMethod: false, + payment: false + }); + } + +} 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..836aca7 --- /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-detail'; +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 index b1f132d..016148a 100644 --- a/src/app/core/services/holder/holder.service.ts +++ b/src/app/core/services/holder/holder.service.ts @@ -32,6 +32,14 @@ export class HolderService { 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()}`, {}); + } + // getPreparers(filter: PreparerFilter): Observable { // return this.http.get(`${this.apiUrl}/${this.apiDb}/GetPreparers/${this.userService.getUserSpid()}/ACTIVE?P_NAME=${filter.name}&P_LOOKUPCODE=${filter.lookupCode}&P_CITY=${filter.city}&P_STATE=${filter.state}`).pipe( diff --git a/src/app/holder/add/add.component.html b/src/app/holder/add/add.component.html new file mode 100644 index 0000000..77911e1 --- /dev/null +++ b/src/app/holder/add/add.component.html @@ -0,0 +1,24 @@ +
+ + + + + + 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..e69de29 diff --git a/src/app/holder/add/add.component.ts b/src/app/holder/add/add.component.ts new file mode 100644 index 0000000..c3fcfee --- /dev/null +++ b/src/app/holder/add/add.component.ts @@ -0,0 +1,57 @@ +import { Component } 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'; + +@Component({ + selector: 'app-add', + imports: [ + AngularMaterialModule, + CommonModule, + ReactiveFormsModule, + HolderDetailComponent, + MatStepperModule, + MatFormFieldModule, + MatInputModule, + HolderContactsComponent + ], + templateUrl: './add.component.html', + styleUrl: './add.component.scss' +}) +export class AddComponent { + 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, + ){ + this.userPreferences = this.userPrefenceService.getPreferences(); + } + + onStepChange(event: StepperSelectionEvent): void { + this.currentStep = event.selectedIndex; + } + + onHolderIdCreated(holderid: number): void { + this.holderid = holderid; + this.stepsCompleted.holderDetails = true; + console.log("holderid got successfully :: ", holderid); + + } + +} 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..671e779 --- /dev/null +++ b/src/app/holder/edit/edit.component.ts @@ -0,0 +1,40 @@ +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(() => { + // Open all panels + 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..8b11734 --- /dev/null +++ b/src/app/holder/holder-contacts/holder-contacts.component.ts @@ -0,0 +1,373 @@ +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 { MatDialog } from '@angular/material/dialog'; +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 dialog: MatDialog, + 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)]], + // defaultContact: [false] + }); + } + + ngOnInit(): void { + if (this.holderid > 0) { + this.loadContacts(); + } + + // 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; + } + + loadContactsX(): void { + this.isLoading = true; + + this.contacts = [ + { + HOLDERCONTACTID: 101, + HOLDERID: 3001, + SPID: 5001, + FIRSTNAME: "Alice", + LASTNAME: "Johnson", + MIDDLEINITIAL: "M", + TITLE: "Product Manager", + PHONE: "1234567890", + MOBILE: "0987654321", + FAX: "1112223333", + EMAILADDRESS: "alice.johnson@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "admin", + INACTIVEFLAG: "Y", + INACTIVEDATE: new Date().toISOString(), + LASTUPDATEDBY: "alice", + LASTUPDATEDDATE: new Date().toISOString() + }, + { + HOLDERCONTACTID: 102, + HOLDERID: 3001, + SPID: 5002, + FIRSTNAME: "Bob", + LASTNAME: "Smith", + MIDDLEINITIAL: null, + TITLE: "Sales Associate", + PHONE: "2345678901", + MOBILE: "8765432109", + FAX: null, + EMAILADDRESS: "bob.smith@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "hr_user", + INACTIVEFLAG: "Y", + INACTIVEDATE: new Date().toISOString(), + LASTUPDATEDBY: "hr_user", + LASTUPDATEDDATE: new Date().toISOString() + }, + { + HOLDERCONTACTID: 103, + HOLDERID: 3002, + SPID: 5003, + FIRSTNAME: "Charlie", + LASTNAME: "Brown", + MIDDLEINITIAL: "R", + TITLE: "Developer", + PHONE: "3456789012", + MOBILE: "7654321098", + FAX: "4445556666", + EMAILADDRESS: "charlie.brown@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "it_admin", + INACTIVEFLAG: "N", + INACTIVEDATE: null, + LASTUPDATEDBY: "charlie", + LASTUPDATEDDATE: new Date().toISOString() + }, + { + HOLDERCONTACTID: 104, + HOLDERID: 3003, + SPID: 5004, + FIRSTNAME: "Diana", + LASTNAME: "Evans", + MIDDLEINITIAL: "K", + TITLE: "Marketing Director", + PHONE: "4567890123", + MOBILE: "6543210987", + FAX: "7778889999", + EMAILADDRESS: "diana.evans@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "marketing_admin", + INACTIVEFLAG: "N", + INACTIVEDATE: null, + LASTUPDATEDBY: "diana", + LASTUPDATEDDATE: new Date().toISOString() + }, + { + HOLDERCONTACTID: 105, + HOLDERID: 3003, + SPID: 5005, + FIRSTNAME: "Edward", + LASTNAME: "Norton", + MIDDLEINITIAL: null, + TITLE: "Finance Analyst", + PHONE: "5678901234", + MOBILE: "5432109876", + FAX: null, + EMAILADDRESS: "edward.norton@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "finance_team", + INACTIVEFLAG: "N", + INACTIVEDATE: null, + LASTUPDATEDBY: "edward", + LASTUPDATEDDATE: new Date().toISOString() + }, + { + HOLDERCONTACTID: 106, + HOLDERID: 3004, + SPID: 5006, + FIRSTNAME: "Fiona", + LASTNAME: "Griffin", + MIDDLEINITIAL: "L", + TITLE: "HR Manager", + PHONE: "6789012345", + MOBILE: "4321098765", + FAX: "3334445555", + EMAILADDRESS: "fiona.griffin@example.com", + DATECREATED: new Date().toISOString(), + CREATEDBY: "hr_admin", + INACTIVEFLAG: "N", + INACTIVEDATE: null, + LASTUPDATEDBY: "fiona", + LASTUPDATEDDATE: new Date().toISOString() + } + ]; + + this.renderContacts(); + + this.isLoading = false; + + } + + 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(); + // this.contactForm.patchValue({ defaultContact: false }); + } + + 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.contactForm.markAllAsTouched(); + return; + } + + // default the first contact + const contactData: ContactFormModel = this.contactForm.value; + // contactData.defaultContact = this.dataSource?.data?.length === 0; + + console.log("Contact details : ", contactData); + + + 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; + this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'Y'); + } else { + this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'N'); + } + } + + deleteContact(contactId: string): void { + // const dialogRef = this.dialog.open(ConfirmDialogComponent, { + // width: '350px', + // data: { + // title: 'Confirm Delete', + // message: 'Are you sure you want to delete this contact?', + // confirmText: 'Delete', + // cancelText: 'Cancel' + // } + // }); + + // dialogRef.afterClosed().subscribe(result => { + // if (result) { + // this.contactService.deleteContact(contactId).subscribe({ + // next: () => { + // this.notificationService.showSuccess('Contact deleted successfully'); + // this.loadContacts(); + // }, + // error: (error) => { + // let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete contact'); + // this.notificationService.showError(errorMessage); + // console.error('Error deleting contact:', error); + // } + // }); + // } + // }); + } + + createLogin(): void { + } + + 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..332378a --- /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..a4b06f5 --- /dev/null +++ b/src/app/holder/holder-detail/holder-detail.component.ts @@ -0,0 +1,239 @@ +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-detail'; +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'; + +// interface Country { +// name: string; +// id: number; +// alpha2: string; +// alpha3: string; +// } + +@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() headerIdCreated = 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.isEditMode) { + // this.patchFormData(this.holder); + // } + + if (this.holderid > 0) { + console.log('ngOnInit - holderid:', this.holderid); + this.isLoading = true; + this.holderDetailService.getHolderDetailByHolderId(this.holderid).subscribe({ + next: (basicDetail: HolderDetail) => { + // alert(basicDetail.HOLDERNAME) + + // if (basicDetail?.HOLDERID ? basicDetail.HOLDERID > 0 : false) { + 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 { + + console.log("patch data : -- ",data); + + const selectedCountry = this.countries.find( + c => c.name === data.COUNTRY + ); + + 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: selectedCountry?.value || null, + country: data.COUNTRY, + zip: data.ZIP + }) + + // this.loadStates(selectedCountry?.name || ''); + + if (data.COUNTRY) { + this.loadStates(data.COUNTRY); + } + + // ✅ Ensure form validity is updated + // this.holderDetailsForm.updateValueAndValidity(); + + // ✅ Optional: mark pristine to track edits properly + // this.holderDetailsForm.markAsPristine(); + + this.holderDetailsForm.markAsPristine(); + this.holderDetailsForm.markAsUntouched(); + } + + 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.headerIdCreated.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 index 8e2be27..b67ad5a 100644 --- a/src/app/holder/search/search.component.html +++ b/src/app/holder/search/search.component.html @@ -46,6 +46,12 @@ +
+ + Show Inactive Holders + +
+ @@ -94,14 +100,26 @@ @@ -122,15 +140,16 @@ showFirstLastButtons> --> - +
-
- + \ No newline at end of file diff --git a/src/app/holder/search/search.component.ts b/src/app/holder/search/search.component.ts index ecf22df..65c88c2 100644 --- a/src/app/holder/search/search.component.ts +++ b/src/app/holder/search/search.component.ts @@ -9,9 +9,9 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { HolderService } from '../../core/services/holder/holder.service'; -import { HolderDetail } from '../../core/models/holder/holder-detail'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { NotificationService } from '../../core/services/common/notification.service'; +import { HolderDetail } from '../../core/models/holder/holder-detail'; @Component({ selector: 'app-search', @@ -21,72 +21,49 @@ import { NotificationService } from '../../core/services/common/notification.ser }) export class SearchComponent implements OnInit, AfterViewInit { - @Output() selectHolderCompleted = new EventEmitter(); + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort!: MatSort; + + @Output() selectHolderCompleted = new EventEmitter(); + @Output() searchedHolderEmit = new EventEmitter(); + + @Input() searchedHolder: string = '' @Input() headerid: number = 0; + @Input() HOLDERID: number = 0 + isEditing: boolean = false selectedHolder: any = null; selectedHolderId: number = 0; + HolderSaved: boolean = false; + showInactiveHolders: boolean = false; + holdersSearched: any = [] + isLoading: boolean = false; + userPreferences: UserPreferences; searchForm: FormGroup; - HolderSaved: boolean = false; 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 cdr: ChangeDetectorRef, private navigationService: NavigationService, ) { - this.userPreferences = userPrefenceService.getPreferences(); + this.userPreferences = this.userPrefenceService.getPreferences(); this.searchForm = this.createSearchForm(); } - // Call this method when the holder selection is completed - completeSelection() { - console.log("Headerid : ", this.headerid); - console.log("Headerid : ", this.selectedHolderId); - - this.saveApplication(); - } - - addNewHolder(): void { - this.navigationService.navigate(["holder"], { state: { isEditMode: false } }) - } - - cancelEdit() { - this.clearSelection(); - } - - - findHolderById(id: number) { - return this.dataSource.data.reduce((found: any, current: any) => { - return found || (current.HolderID === id ? current : null); - }, null); - } - - onEdit(id: string) { - // this.selectedHolder = this.findHolderById(Number(id)); - // console.log("Holder id: ", id, this.selectedHolder); - // this.navigationService.navigate(['holder', id], { state: { holder: this.selectedHolder } }); - } - - isLoading = false; - - displayedColumns: string[] = [ - 'HolderName', - 'DBAName', - 'Address', - 'USCIBMember', - 'HolderType', - 'actions', - ]; - - dataSource = new MatTableDataSource([]); - - private fb = inject(FormBuilder); - createSearchForm(): FormGroup { return this.fb.group({ holderName: [''] @@ -94,7 +71,43 @@ export class SearchComponent implements OnInit, AfterViewInit { } ngOnInit(): void { + if (this.searchedHolder) { + this.patchData() + this.searchHolders(); + } + if (this.HOLDERID > 0) { + this.selectedHolderId = this.HOLDERID; + } + } + + patchData() { + this.searchForm.patchValue( + { holderName: this.searchedHolder } + ) + } + + searchHolders(): void { + + this.isLoading = true; + console.log("search form : ", this.searchForm.value); + + 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 { @@ -109,26 +122,11 @@ export class SearchComponent implements OnInit, AfterViewInit { this.searchHolders(); } - searchHolders(): void { - - this.isLoading = true; - console.log("search form : ", this.searchForm.value); - - const filterData: { holderName: string } = this.searchForm.value; - - this.holderService.searchHolders(filterData).subscribe({ - next: (holders: HolderDetail[]) => { - this.dataSource.data = holders; - 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); - } - }); + completeSelection() { + console.log("Headerid : ", this.headerid); + console.log("Headerid : ", this.selectedHolderId); + this.saveApplication(); } saveApplication(): void { @@ -137,9 +135,9 @@ export class SearchComponent implements OnInit, AfterViewInit { saveObservable.subscribe({ next: (basicData: any) => { this.notificationService.showSuccess(`Holder updated successfully`); - this.selectHolderCompleted.emit(true); - this.searchForm.get('holderName')?.disable(); - this.HolderSaved = true + this.selectHolderCompleted.emit(this.selectedHolderId); + // this.searchForm.get('holderName')?.disable(); + // this.HolderSaved = true }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, `Failed to update holder details`); @@ -147,32 +145,38 @@ export class SearchComponent implements OnInit, AfterViewInit { console.error('Error saving holder details:', error); } }); + + // this.selectHolderCompleted.emit(this.selectedHolderId); } - onClearSearch(): void { - this.searchForm.reset(); - this.dataSource.data = [] + toggleShowInactiveHolders(): void { + this.showInactiveHolders = !this.showInactiveHolders; + this.renderHolders(); } - - @ViewChild(MatPaginator) paginator!: MatPaginator; - @ViewChild(MatSort) sort!: MatSort; - - ngAfterViewInit() { - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; + renderHolders() { + if (this.showInactiveHolders) { + // this.dataSource.data = this.contacts; + this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'Y'); + } else { + // let holders = this.dataSource.data; + this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'N'); + } } - onCheckboxChange(client: any) { - console.log('Checkbox changed:', client); + addNewHolder(): void { + this.navigationService.navigate(["add-holder"], { state: { isEditMode: false } }) } + onEdit(id: string) { + // this.selectedHolder = this.findHolderById(Number(id)); + // console.log("Holder id: ", id, this.selectedHolder); + // this.navigationService.navigate(['holder', id], { state: { holder: this.selectedHolder } }); + this.navigationService.navigate(['edit-holder', id]); + } - - onRadioSelect(holder: any): void { - this.selectedHolder = holder; - this.selectedHolderId = holder.HOLDERID; - console.log('Selected row:', holder); + cancelEdit() { + this.clearSelection(); } clearSelection(): void { @@ -180,4 +184,51 @@ export class SearchComponent implements OnInit, AfterViewInit { this.selectedHolderId = 0; } + InactivateHolder(HOLDERID: number): void { + console.log("hello called InactivateHolder ............"); + + 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 = []; + this.searchedHolderEmit.emit(''); + } + + onRadioSelect(holder: any): void { + this.selectedHolder = holder; + this.selectedHolderId = holder.HOLDERID; + console.log('Selected row:', holder); + } + } diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 4c155d6..a649bcb 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -15,6 +15,7 @@ import { CommonService } from '../core/services/common/common.service'; import { NotificationService } from '../core/services/common/notification.service'; import { NavigationService } from '../core/services/common/navigation.service'; import { MatRadioChange } from '@angular/material/radio'; +import { CarnetStateStore } from '../core/services/carnet/carnet-application.service'; @Component({ selector: 'app-home', @@ -29,6 +30,7 @@ export class HomeComponent { showTable = false; userPreferences: UserPreferences; carnetStatuses: CarnetStatus[] = []; + store = inject(CarnetStateStore); dataSource = new MatTableDataSource(); @@ -114,6 +116,7 @@ export class HomeComponent { newCarnet(event: MatRadioChange): void { if (event.value) { + this.store.resetState(); this.navigateTo('add-carnet'); } } diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index d146153..34910cd 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -68,6 +68,9 @@ export class LoginComponent { console.error('Login error:', error); } }); + + // this.userService.setUser(username); + // this.setUserData(); } } @@ -91,5 +94,22 @@ export class LoginComponent { console.error('Error retrieving app data:', error); } }); + + // const data = { + // "userDetails": { + // "SPID": 187, + // "ENCURLKEY": "F55CBF557FEE5BC6065CF0DA74E48E98", + // "PRIMARYCOLOR": "#607c7c", + // "SECONDARYCOLOR": "#9cb4b8", + // "TERTIARYCOLOR": null, + // "NEUTRALCOLOR": null, + // "LOGONAME": "USCIBLogo.jpeg", + // "THEMENAME": "Theme187" + // } + // } + + // this.navigationService.setCurrentAppId(data.userDetails.ENCURLKEY); + // this.themeService.setTheme(data.userDetails.THEMENAME); + // this.navigationService.navigate(['home']); } }
Actions - + + + + class="selectHolder" [value]="client.HOLDERID" [checked]="selectedHolderId === client.HOLDERID" + (change)="onRadioSelect(client)" aria-label="Select row" [hidden]="client.INACTIVEFLAG === 'Y'"> + + +