diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 572fb70..69c62d8 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -7,6 +7,8 @@ export const serverRoutes: ServerRoute[] = [ { path: ':appId/usersettings', renderMode: RenderMode.Client }, { path: ':appId/add-carnet', renderMode: RenderMode.Client }, { path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client }, + { path: ':appId/add-holder', renderMode: RenderMode.Client }, + { path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client }, { path: '**', renderMode: RenderMode.Prerender diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d87ef85..22a957b 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-holder.component').then(m => m.AddHolderComponent) }, + { path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit-holder.component').then(m => m.EditHolderComponent) }, { 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..f997d16 100644 --- a/src/app/carnet/add/add-carnet.component.html +++ b/src/app/carnet/add/add-carnet.component.html @@ -7,7 +7,7 @@ Application Name - + diff --git a/src/app/carnet/add/add-carnet.component.ts b/src/app/carnet/add/add-carnet.component.ts index ecb8611..c2d6c9c 100644 --- a/src/app/carnet/add/add-carnet.component.ts +++ b/src/app/carnet/add/add-carnet.component.ts @@ -44,8 +44,10 @@ export class AddCarnetComponent { this.currentStep = event.selectedIndex; } - onApplicationDetailCreated(headerid: number): void { - this.headerid = headerid; + onApplicationDetailCreated(data: { headerid: number, applicationName: string }): void { + // this.store.headerid.set(data.headerid); + // this.store.applicationName.set(data.applicationName); + 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..299e904 100644 --- a/src/app/carnet/application/application.component.ts +++ b/src/app/carnet/application/application.component.ts @@ -20,7 +20,7 @@ export class ApplicationComponent { @Input() headerid: number = 0; @Input() applicationName: string = ''; - @Output() headerIdCreated = new EventEmitter(); + @Output() applicationCreated = new EventEmitter<{ headerid: number, applicationName: string }>(); applicationDetailsForm: FormGroup; isLoading = false; @@ -39,8 +39,6 @@ export class ApplicationComponent { } ngOnInit(): void { - this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || ''; - // Patch edit form data if (this.applicationName) { this.patchFormData(); @@ -102,7 +100,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.applicationCreated.emit({ headerid: +applicationData.HEADERID, applicationName: 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 f64a2d1..2d66e0c 100644 --- a/src/app/carnet/edit/edit-carnet.component.html +++ b/src/app/carnet/edit/edit-carnet.component.html @@ -7,7 +7,7 @@ Application Name - + @@ -15,7 +15,8 @@ Holder Selection - + diff --git a/src/app/carnet/edit/edit-carnet.component.ts b/src/app/carnet/edit/edit-carnet.component.ts index 64e3d50..13a32c2 100644 --- a/src/app/carnet/edit/edit-carnet.component.ts +++ b/src/app/carnet/edit/edit-carnet.component.ts @@ -26,6 +26,7 @@ export class EditCarnetComponent { isEditMode = true; // Set to true for edit mode headerid: number = 0; userPreferences: UserPreferences; + applicationName: string = ''; // Track completion of each step stepsCompleted = { @@ -45,6 +46,8 @@ export class EditCarnetComponent { ngOnInit(): void { const idParam = this.route.snapshot.paramMap.get('headerid'); this.headerid = idParam ? parseInt(idParam, 10) : 0; + + this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || ''; } onStepChange(event: StepperSelectionEvent): void { diff --git a/src/app/carnet/holder/holder.component.html b/src/app/carnet/holder/holder.component.html index 3c8513e..d152820 100644 --- a/src/app/carnet/holder/holder.component.html +++ b/src/app/carnet/holder/holder.component.html @@ -1 +1,2 @@ -

holder works!

+ \ No newline at end of file diff --git a/src/app/carnet/holder/holder.component.ts b/src/app/carnet/holder/holder.component.ts index 2542504..dee3a29 100644 --- a/src/app/carnet/holder/holder.component.ts +++ b/src/app/carnet/holder/holder.component.ts @@ -1,12 +1,49 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { UserPreferences } from '../../core/models/user-preference'; +import { SearchHolderComponent } from '../../holder/search/search-holder.component'; +import { HolderService } from '../../core/services/carnet/holder.service'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { Holder } from '../../core/models/carnet/holder'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; @Component({ selector: 'app-holder', - imports: [], + imports: [SearchHolderComponent], templateUrl: './holder.component.html', styleUrl: './holder.component.scss' }) export class HolderComponent { @Input() headerid: number = 0; + @Input() applicationName: string = ''; + @Input() userPreferences: UserPreferences = {}; + @Input() isEditMode: boolean = false; @Output() completed = new EventEmitter(); + + selectedHolderId: number = 0; + + private holdersService = inject(HolderService); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + + ngOnInit(): void { + if (this.headerid > 0) { + this.holdersService.getHolder(this.headerid).subscribe({ + next: (holder: Holder) => { + if (holder) { + this.selectedHolderId = holder.holderid; + this.completed.emit(true); + } + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get holder details'); + this.notificationService.showError(errorMessage); + console.error('Error getting holder details:', error); + } + }); + } + } + + onHolderSelectionSaved(completed: boolean): void { + this.completed.emit(completed); + } } diff --git a/src/app/core/models/carnet/holder.ts b/src/app/core/models/carnet/holder.ts index 4cbe4c5..e55c2e6 100644 --- a/src/app/core/models/carnet/holder.ts +++ b/src/app/core/models/carnet/holder.ts @@ -1,2 +1,15 @@ export interface Holder { + holderid: number; + holderName: string; + dbaName?: string | null; + holderNumber: string; + holderType: string; + uscibMember: boolean; + govAgency: boolean; + address1: string; + address2: string; + city: string; + state: string; + country: string; + zip: string; } diff --git a/src/app/core/models/holder/basic-detail.ts b/src/app/core/models/holder/basic-detail.ts new file mode 100644 index 0000000..a37598c --- /dev/null +++ b/src/app/core/models/holder/basic-detail.ts @@ -0,0 +1,18 @@ +export interface BasicDetail { + holderid?: number; + spid: number; + locationid: number; + holderName: string; + dbaName?: string | null; + holderNumber: string; + holderType: string; + uscibMember: boolean; + govAgency: boolean; + address1: string; + address2: string; + city: string; + state: string; + country: string; + zip: string; + isInactive?: boolean | null; +} \ No newline at end of file diff --git a/src/app/core/models/holder/contact.ts b/src/app/core/models/holder/contact.ts new file mode 100644 index 0000000..8c05ecb --- /dev/null +++ b/src/app/core/models/holder/contact.ts @@ -0,0 +1,19 @@ +export interface Contact { + holdercontactid: number; + holderid: number; + spid: number; + firstName: string; + lastName: string; + middleInitial: string | null; + title: string; + phone: string; + mobile: string; + fax?: string | null; + email: string; + dateCreated?: Date | null; + createdBy?: string | null; + lastUpdatedBy?: string | null; + lastUpdatedDate?: Date | null; + isInactive?: boolean | null; + inactivatedDate?: Date | null; +} \ No newline at end of file diff --git a/src/app/core/models/holder/holder-filter.ts b/src/app/core/models/holder/holder-filter.ts new file mode 100644 index 0000000..7397b2a --- /dev/null +++ b/src/app/core/models/holder/holder-filter.ts @@ -0,0 +1,3 @@ +export interface HolderFilter { + holderName?: string; +} diff --git a/src/app/core/services/carnet/holder.service.ts b/src/app/core/services/carnet/holder.service.ts index 302c787..40e25ca 100644 --- a/src/app/core/services/carnet/holder.service.ts +++ b/src/app/core/services/carnet/holder.service.ts @@ -1,9 +1,51 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { UserService } from '../common/user.service'; +import { Holder } from '../../models/carnet/holder'; +import { filter, map, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class HolderService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; - constructor() { } + private http = inject(HttpClient); + private userService = inject(UserService); + + getHolder(id: number): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetHolderstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${id}`).pipe( + filter(response => response.length > 0), + map(response => this.mapToHolder(response?.[0]))); + } + + private mapToHolder(holder: any): Holder { + return { + holderid: holder.HOLDERID, + holderNumber: holder.HOLDERNO, + holderType: holder.HOLDERTYPE, + uscibMember: holder.USCIBMEMBERFLAG === 'Y', + govAgency: holder.GOVAGENCYFLAG === 'Y', + holderName: holder.HOLDERNAME, + //NAMEQUALIFIER: holder.NAMEQUALIFIER || null, + dbaName: holder.ADDLNAME || null, + address1: holder.ADDRESS1, + address2: holder.ADDRESS2 || null, + city: holder.CITY, + state: holder.STATE, + zip: holder.ZIP, + country: holder.COUNTRY + }; + } + + saveApplicationHolder(headerId: number, holderId: number) { + const applicationHolder = { + P_HEADERID: headerId, + P_HOLDERID: holderId + } + + return this.http.patch(`${this.apiUrl}/${this.apiDb}/update-holder`, applicationHolder); + } } diff --git a/src/app/core/services/holder/basic-detail.service.ts b/src/app/core/services/holder/basic-detail.service.ts new file mode 100644 index 0000000..0b5a3f7 --- /dev/null +++ b/src/app/core/services/holder/basic-detail.service.ts @@ -0,0 +1,90 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { UserService } from '../common/user.service'; +import { environment } from '../../../../environments/environment'; +import { map, Observable, } from 'rxjs'; +import { BasicDetail } from '../../models/holder/basic-detail'; + +@Injectable({ + providedIn: 'root' +}) +export class BasicDetailService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + private userService = inject(UserService); + + getBasicDetailByHolderId(id: number): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetHolderRecord/${this.userService.getUserSpid()}/${id}`).pipe( + map(response => this.mapToBasicDetail(response))); + } + + private mapToBasicDetail(basicDetails: any): BasicDetail { + return { + holderid: basicDetails.HOLDERID, + spid: basicDetails.SPID, + locationid: basicDetails.LOCATIONID, + holderNumber: basicDetails.HOLDERNO, + holderType: basicDetails.HOLDERTYPE, + uscibMember: basicDetails.USCIBMEMBERFLAG === 'Y', + govAgency: basicDetails.GOVAGENCYFLAG === 'Y', + holderName: basicDetails.HOLDERNAME, + //NAMEQUALIFIER: basicDetails.NAMEQUALIFIER || null, + dbaName: basicDetails.ADDLNAME || null, + address1: basicDetails.ADDRESS1, + address2: basicDetails.ADDRESS2 || null, + city: basicDetails.CITY, + state: basicDetails.STATE, + zip: basicDetails.ZIP, + country: basicDetails.COUNTRY + }; + } + + createBasicDetail(data: BasicDetail): Observable { + const basicDetail = { + P_SPID: this.userService.getUserSpid(), + P_CLIENTLOCATIONID: this.userService.getUserLocationid(), + P_HOLDERNO: data.holderNumber, + P_HOLDERTYPE: data.holderType, + P_USCIBMEMBERFLAG: data.uscibMember ? 'Y' : 'N', + P_GOVAGENCYFLAG: data.govAgency ? 'Y' : 'N', + 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.post(`${this.apiUrl}/${this.apiDb}/CreateHolderData`, basicDetail); + } + + updateBasicDetails(id: number, data: BasicDetail): Observable { + const basicDetail = { + 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 ? 'Y' : 'N', + P_GOVAGENCYFLAG: data.govAgency ? 'Y' : 'N', + 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`, basicDetail); + } +} 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..c1cde68 --- /dev/null +++ b/src/app/core/services/holder/contact.service.ts @@ -0,0 +1,90 @@ +import { inject, 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 { Contact } from '../../models/holder/contact'; + +@Injectable({ + providedIn: 'root' +}) +export class ContactService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + private userService = inject(UserService); + + getContactsById(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[]): Contact[] { + 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, + email: contact.EMAILADDRESS, + createdBy: contact.CREATEDBY || null, + dateCreated: contact.DATECREATED || null, + lastUpdatedBy: contact.LASTUPDATEDBY || null, + lastUpdatedDate: contact.LASTUPDATEDDATE || null, + isInactive: contact.INACTIVEFLAG === 'Y' || false, + inactivatedDate: contact.INACTIVEDATE || null + })); + } + + createContact(holderid: number, data: Contact): 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); + } + + updateContact(holdercontactid: number, data: Contact): Observable { + const contact = { + P_HOLDERCONTACTID: 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() + } + + 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.service.ts b/src/app/core/services/holder/holder.service.ts new file mode 100644 index 0000000..2589656 --- /dev/null +++ b/src/app/core/services/holder/holder.service.ts @@ -0,0 +1,54 @@ +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 { BasicDetail } from '../../models/holder/basic-detail'; +import { HolderFilter } from '../../models/holder/holder-filter'; + +@Injectable({ + providedIn: 'root' +}) +export class HolderService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + private userService = inject(UserService); + + getHolders(filter: HolderFilter): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/SearchHolder/${this.userService.getUserSpid()}?P_HOLDERNAME=${filter.holderName}`).pipe( + map(response => this.mapToHolders(response)) + ) + } + + 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[]): BasicDetail[] { + return data.map((holderDetail) => ({ + holderid: holderDetail.HOLDERID, + spid: holderDetail.SPID, + locationid: holderDetail.LOCATIONID, + holderNumber: holderDetail.HOLDERNO, + holderType: holderDetail.HOLDERTYPE, + uscibMember: holderDetail.USCIBMEMBERFLAG === 'Y', + govAgency: holderDetail.GOVAGENCYFLAG === 'Y', + holderName: holderDetail.HOLDERNAME, + //: holderDetail.NAMEQUALIFIER, + dbaName: holderDetail.ADDLNAME, + address1: holderDetail.ADDRESS1, + address2: holderDetail.ADDRESS2, + city: holderDetail.CITY, + state: holderDetail.STATE, + zip: holderDetail.ZIP, + country: holderDetail.COUNTRY, + isInactive: holderDetail.INACTIVEFLAG === 'Y' + })) + } +} diff --git a/src/app/holder/add/add-holder.component.html b/src/app/holder/add/add-holder.component.html new file mode 100644 index 0000000..904e1b3 --- /dev/null +++ b/src/app/holder/add/add-holder.component.html @@ -0,0 +1,23 @@ + + +
+ + + + + + Holder Details + + + + + + + Holder Contacts + + + +
\ No newline at end of file diff --git a/src/app/holder/add/add-holder.component.scss b/src/app/holder/add/add-holder.component.scss new file mode 100644 index 0000000..8d7d2bb --- /dev/null +++ b/src/app/holder/add/add-holder.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-holder.component.ts b/src/app/holder/add/add-holder.component.ts new file mode 100644 index 0000000..4779a1b --- /dev/null +++ b/src/app/holder/add/add-holder.component.ts @@ -0,0 +1,66 @@ +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 { BasicDetailComponent } from '../basic-details/basic-details.component'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { ContactsComponent } from '../contacts/contacts.component'; +import { UserPreferences } from '../../core/models/user-preference'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { NavigationService } from '../../core/services/common/navigation.service'; + +@Component({ + selector: 'app-add-holder', + imports: [ + AngularMaterialModule, + CommonModule, + ReactiveFormsModule, + BasicDetailComponent, + MatStepperModule, + MatFormFieldModule, + MatInputModule, + ContactsComponent + ], + templateUrl: './add-holder.component.html', + styleUrl: './add-holder.component.scss' +}) +export class AddHolderComponent { + currentStep = 0; + isLinear = true; + isEditMode = false; + holderid: number = 0; + userPreferences: UserPreferences; + + stepsCompleted = { + holderDetails: false, + holderContacts: false + }; + + private navigationService = inject(NavigationService); + + constructor( + userPrefenceService: UserPreferencesService + ) { + this.userPreferences = 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/basic-details/basic-details.component.html b/src/app/holder/basic-details/basic-details.component.html new file mode 100644 index 0000000..865cb25 --- /dev/null +++ b/src/app/holder/basic-details/basic-details.component.html @@ -0,0 +1,158 @@ +
+ + +
+ +
+ +
+ + +
+ + Holder Name + + + Name is required + + + Maximum 100 characters allowed + + + + + DBA Name + + + DBA name is required + + + Maximum 20 characters allowed + + +
+
+ + Tax ID No + + + Tax ID No 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/basic-details/basic-details.component.scss b/src/app/holder/basic-details/basic-details.component.scss new file mode 100644 index 0000000..a26ddfb --- /dev/null +++ b/src/app/holder/basic-details/basic-details.component.scss @@ -0,0 +1,109 @@ +.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; + + .name { + grid-column: span 2; + } + + .lookup-code { + grid-column: span 1; + } + + .holder-number { + grid-column: span 1; + } + + .address1, + .address2 { + grid-column: span 3; + } + + .city, + .state, + .country, + .zip { + grid-column: span 1; + } + + mat-radio-group { + mat-label { + color: var(--mat-sys-on-surface); + font-family: var(--mat-sys-body-medium-font); + line-height: var(--mat-sys-body-medium-line-height); + font-size: var(--mat-sys-body-medium-size); + letter-spacing: var(--mat-sys-body-medium-tracking); + font-weight: var(--mat-sys-medium-font-weight); + } + } + + } + + .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 { + grid-column: span 1; + } + } + + + } + } +} \ No newline at end of file diff --git a/src/app/holder/basic-details/basic-details.component.ts b/src/app/holder/basic-details/basic-details.component.ts new file mode 100644 index 0000000..b1aa2a7 --- /dev/null +++ b/src/app/holder/basic-details/basic-details.component.ts @@ -0,0 +1,204 @@ +import { Component, EventEmitter, inject, 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 { CommonService } from '../../core/services/common/common.service'; +import { Subject, takeUntil } from 'rxjs'; +import { Country } from '../../core/models/country'; +import { BasicDetailService } from '../../core/services/holder/basic-detail.service'; +import { BasicDetail } from '../../core/models/holder/basic-detail'; + +@Component({ + selector: 'app-basic-detail', + imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule], + templateUrl: './basic-details.component.html', + styleUrl: './basic-details.component.scss' +}) +export class BasicDetailComponent implements OnInit, OnDestroy { + + @Input() isEditMode = false; + @Input() holderid: number = 0; + + @Output() holderIdCreated = new EventEmitter(); + @Output() holderName = new EventEmitter(); + + basicDetailsForm: FormGroup; + countries: Country[] = []; + regions: Region[] = []; + states: State[] = []; + + isLoading = false; + countriesHasStates = ['US', 'CA', 'MX']; + + private destroy$ = new Subject(); + + private fb = inject(FormBuilder); + private basicDetailService = inject(BasicDetailService); + private notificationService = inject(NotificationService); + private commonService = inject(CommonService); + private errorHandler = inject(ApiErrorHandlerService); + + constructor() { + this.basicDetailsForm = 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: [false, [Validators.required]], + govAgency: [false, [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.basicDetailService.getBasicDetailByHolderId(this.holderid).subscribe({ + next: (basicDetail: BasicDetail) => { + this.patchFormData(basicDetail); + this.holderName.emit(basicDetail.holderName); + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading basic details:', error); + } + }); + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + onCountryChange(country: string): void { + this.basicDetailsForm.get('state')?.reset(); + + if (country) { + this.loadStates(country); + } + + this.basicDetailsForm.get('zip')?.updateValueAndValidity(); + } + + saveBasicDetails() { + if (this.basicDetailsForm.invalid) { + this.basicDetailsForm.markAllAsTouched(); + return; + } + + const basicDetailData: BasicDetail = this.basicDetailsForm.value; + + const saveObservable = this.isEditMode && this.holderid > 0 + ? this.basicDetailService.updateBasicDetails(this.holderid, basicDetailData) + : this.basicDetailService.createBasicDetail(basicDetailData); + + saveObservable.subscribe({ + next: (basicData: any) => { + this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`); + + if (!this.isEditMode) { + this.holderIdCreated.emit(basicData.HOLDERID); + } + + if (this.isEditMode) { + this.holderName.emit(basicDetailData.holderName); + } + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} basic details`); + this.notificationService.showError(errorMessage); + console.error('Error saving basic details:', error); + } + }); + } + + updateStateControl(controlName: string, country: string): void { + const stateControl = this.basicDetailsForm.get(controlName); + if (this.countriesHasStates.includes(country)) { + stateControl?.enable(); + } else { + stateControl?.disable(); + stateControl?.setValue('FN'); + } + } + + patchFormData(data: BasicDetail): void { + + this.basicDetailsForm.patchValue({ + holderName: data.holderName, + dbaName: data.dbaName, + holderNumber: data.holderNumber, + holderType: data.holderType, + uscibMember: data.uscibMember, + govAgency: data.govAgency, + 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.basicDetailsForm.controls; + } + + 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; + } + }); + } + + 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/contacts/contacts.component.html b/src/app/holder/contacts/contacts.component.html new file mode 100644 index 0000000..3c2140d --- /dev/null +++ b/src/app/holder/contacts/contacts.component.html @@ -0,0 +1,260 @@ +
+
+ + Show Inactive Contacts + + + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
First Name{{ contact.firstName }}Last Name{{ contact.lastName }}Title{{ contact.title }}Phone{{ contact.phone | phone }}Mobile{{ contact.mobile | phone }}Email{{ contact.email }}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/contacts/contacts.component.scss b/src/app/holder/contacts/contacts.component.scss new file mode 100644 index 0000000..03674f5 --- /dev/null +++ b/src/app/holder/contacts/contacts.component.scss @@ -0,0 +1,181 @@ +.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; + } + + .top-divider { + padding-top: 0.5rem; + border-top: 1px solid #eee; + } + + .bottom-divider { + padding-bottom: 0.5rem; + border-bottom: 1px solid #eee; + } + + .readonly-section { + + .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/contacts/contacts.component.ts b/src/app/holder/contacts/contacts.component.ts new file mode 100644 index 0000000..f667a3e --- /dev/null +++ b/src/app/holder/contacts/contacts.component.ts @@ -0,0 +1,232 @@ +import { Component, EventEmitter, inject, 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, MatPaginatorIntl } 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 { ContactService } from '../../core/services/holder/contact.service'; +import { Contact } from '../../core/models/holder/contact'; +import { CustomPaginator } from '../../shared/custom-paginator'; +import { MatDialog } from '@angular/material/dialog'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; + +@Component({ + selector: 'app-contacts', + imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule], + templateUrl: './contacts.component.html', + styleUrl: './contacts.component.scss', + providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], +}) +export class ContactsComponent { + @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: Contact[] = []; + + contactReadOnlyFields: any = { + lastChangedDate: null, + lastChangedBy: null, + isInactive: null, + inactivatedDate: null + }; + + private fb = inject(FormBuilder); + private dialog = inject(MatDialog); + private contactService = inject(ContactService); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + + constructor() { + 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(); + } + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + loadContacts(): void { + this.isLoading = true; + + this.contactService.getContactsById(this.holderid).subscribe({ + next: (contacts: Contact[]) => { + 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: Contact): 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.email + }); + + this.contactReadOnlyFields.lastChangedDate = contact.lastUpdatedDate ?? contact.dateCreated; + this.contactReadOnlyFields.lastChangedBy = contact.lastUpdatedBy ?? contact.createdBy; + this.contactReadOnlyFields.isInactive = contact.isInactive; + this.contactReadOnlyFields.inactivatedDate = contact.inactivatedDate; + } + + saveContact(): void { + if (this.contactForm.invalid && !(this.holderid > 0)) { + this.contactForm.markAllAsTouched(); + return; + } + + const contactData: Contact = this.contactForm.value; + + const saveObservable = this.isEditing && (this.currentContactId! > 0) + ? this.contactService.updateContact(this.currentContactId!, contactData) + : this.contactService.createContact(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; + } else { + this.dataSource.data = this.contacts.filter(contact => !contact.isInactive); + } + } + + cancelEdit(): void { + this.showForm = false; + this.isEditing = false; + this.currentContactId = null; + this.contactForm.reset(); + } + + inactivateContact(contactId: number): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '350px', + data: { + title: 'Confirm Inactivation', + message: 'Are you sure you want to inactivate this contact?', + confirmText: 'Yes', + cancelText: 'Cancel' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.contactService.inactivateHolderContact(contactId).subscribe({ + next: () => { + this.notificationService.showSuccess('Contact inactivated successfully'); + this.loadContacts(); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to inactivate contact'); + this.notificationService.showError(errorMessage); + console.error('Error inactivating contact:', error); + } + }); + } + }); + } + + reactivateContact(contactId: number): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '350px', + data: { + title: 'Confirm Reactivation', + message: 'Are you sure you want to reactivate this contact?', + confirmText: 'Yes', + cancelText: 'Cancel' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.contactService.reactivateHolderContact(contactId).subscribe({ + next: () => { + this.notificationService.showSuccess('Contact reactivated successfully'); + this.loadContacts(); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to reactivate contact'); + this.notificationService.showError(errorMessage); + console.error('Error reactivating contact:', error); + } + }); + } + }); + } +} diff --git a/src/app/holder/edit/edit-holder.component.html b/src/app/holder/edit/edit-holder.component.html new file mode 100644 index 0000000..90986ab --- /dev/null +++ b/src/app/holder/edit/edit-holder.component.html @@ -0,0 +1,24 @@ + + +
+ + +
+ + + + Basic Details + + + + + + + Contacts + + + + + \ No newline at end of file diff --git a/src/app/holder/edit/edit-holder.component.scss b/src/app/holder/edit/edit-holder.component.scss new file mode 100644 index 0000000..abc9d06 --- /dev/null +++ b/src/app/holder/edit/edit-holder.component.scss @@ -0,0 +1,18 @@ +.page-header { + margin: 0.5rem 0px; + color: var(--mat-sys-primary); + font-weight: 500; +} + +.holder-action-buttons { + padding-bottom: 20px; +} + +.holder-headers-align .mat-expansion-panel-header-description { + justify-content: space-between; + align-items: center; +} + +.holder-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-holder.component.ts b/src/app/holder/edit/edit-holder.component.ts new file mode 100644 index 0000000..99bb944 --- /dev/null +++ b/src/app/holder/edit/edit-holder.component.ts @@ -0,0 +1,41 @@ +import { CommonModule } from '@angular/common'; +import { afterNextRender, Component, inject, 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 { BasicDetailComponent } from '../basic-details/basic-details.component'; +import { ContactsComponent } from '../contacts/contacts.component'; + +@Component({ + selector: 'app-edit-holder', + imports: [AngularMaterialModule, CommonModule, BasicDetailComponent, ContactsComponent], + templateUrl: './edit-holder.component.html', + styleUrl: './edit-holder.component.scss' +}) +export class EditHolderComponent { + accordion = viewChild.required(MatAccordion); + isEditMode = true; + holderid = 0; + holderName: string | null = null; + userPreferences: UserPreferences; + + private route = inject(ActivatedRoute); + + constructor(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; + } + + onHolderNameUpdate(event: string): void { + this.holderName = event; + } +} diff --git a/src/app/holder/search/search-holder.component.html b/src/app/holder/search/search-holder.component.html new file mode 100644 index 0000000..86f2034 --- /dev/null +++ b/src/app/holder/search/search-holder.component.html @@ -0,0 +1,122 @@ +
+ +
+
+
+
+ + Holder Name + + search + +
+
+ +
+ + + + +
+
+
+ +
+
+ +
+ +
+ + Show Inactive Holders + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Holder Name{{ holder.holderName }}DBA Name{{ holder.dbaName || '--'}}Address{{ getAddressLabel(holder) }}USCIB Member{{ holder.uscibMember ? 'Yes' : 'No' }}Holder Type{{ holder.holderType }}Actions + +
+ + + + + + + +
+
+ info + No records found matching your criteria +
+ + + +
+ +
+ +
+
\ No newline at end of file diff --git a/src/app/holder/search/search-holder.component.scss b/src/app/holder/search/search-holder.component.scss new file mode 100644 index 0000000..1ef0aad --- /dev/null +++ b/src/app/holder/search/search-holder.component.scss @@ -0,0 +1,105 @@ +.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-holder-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 { + grid-column: span 2; + } + } + } + + .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; + } + + .actions-icons { + display: flex; + } + + .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-holder-container { + .search-form { + .search-fields { + .form-row { + grid-template-columns: 1fr; + + .name { + grid-column: span 1; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/app/holder/search/search-holder.component.ts b/src/app/holder/search/search-holder.component.ts new file mode 100644 index 0000000..37e80cd --- /dev/null +++ b/src/app/holder/search/search-holder.component.ts @@ -0,0 +1,218 @@ +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 { 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, MatPaginatorIntl } from '@angular/material/paginator'; +import { HolderService as HolderService } from '../../core/services/holder/holder.service'; +import { HolderService as CarnetApplicationHolderService } from '../../core/services/carnet/holder.service'; +import { CustomPaginator } from '../../shared/custom-paginator'; +import { BasicDetail } from '../../core/models/holder/basic-detail'; +import { HolderFilter } from '../../core/models/holder/holder-filter'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'app-holder-search', + imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule], + templateUrl: './search-holder.component.html', + styleUrl: './search-holder.component.scss', + providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], +}) +export class SearchHolderComponent { + + @ViewChild(MatPaginator, { static: false }) + set paginator(value: MatPaginator) { + this.dataSource.paginator = value; + } + + @ViewChild(MatSort, { static: false }) + set sort(value: MatSort) { + this.dataSource.sort = value; + } + + @Input() headerid: number = 0; + @Input() selectedHolderId: number = 0; + @Input() applicationName: string = ''; + + @Output() holderSelectionCompleted = new EventEmitter(); + + showInactiveHolders: boolean = false; + holders: BasicDetail[] = [] + isLoading: boolean = false; + + userPreferences: UserPreferences; + searchForm: FormGroup; + + private fb = inject(FormBuilder); + private holderService = inject(HolderService); + private navigationService = inject(NavigationService); + private carnetHolderService = inject(CarnetApplicationHolderService); + private errorHandler = inject(ApiErrorHandlerService); + private notificationService = inject(NotificationService); + private dialog = inject(MatDialog); + + dataSource = new MatTableDataSource([]); + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + displayedColumns: string[] = ['holderName', 'dbaName', 'address', 'uscibMember', 'holderType', 'actions']; + + constructor( + userPrefenceService: UserPreferencesService + ) { + this.userPreferences = userPrefenceService.getPreferences(); + this.searchForm = this.createSearchForm(); + } + + createSearchForm(): FormGroup { + return this.fb.group({ + holderName: [''] + }); + } + + ngOnInit(): void { + this.searchHolders(); + } + + onSearch(): void { + this.searchHolders(); + } + + saveHolderSelection(): void { + const saveObservable = this.carnetHolderService.saveApplicationHolder(this.headerid, this.selectedHolderId ? Number(this.selectedHolderId) : 0); + + saveObservable.subscribe({ + next: (basicData: any) => { + this.notificationService.showSuccess(`Holder updated successfully`); + this.holderSelectionCompleted.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(); + } + + searchHolders(): void { + this.isLoading = true; + const filterData: HolderFilter = this.searchForm.value; + + this.holderService.getHolders(filterData).subscribe({ + next: (holders: BasicDetail[]) => { + this.dataSource.data = this.holders = holders; + this.renderHolders() + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to search holders'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading holders:', error); + } + }); + } + + renderHolders() { + if (this.showInactiveHolders) { + this.dataSource.data = this.holders; + } else { + this.dataSource.data = this.holders.filter((holder: any) => !holder?.isInactive); + } + } + + addNewHolder(): void { + this.navigationService.navigate(["add-holder"], { state: { isEditMode: false } }) + } + + onEdit(id: string) { + this.navigationService.navigate(['edit-holder', id]); + } + + inactivateHolder(holderid: number): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '350px', + data: { + title: 'Confirm Inactivation', + message: 'Are you sure you want to inactivate this holder?', + confirmText: 'Yes', + cancelText: 'Cancel' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.holderService.inactivateHolder(holderid).subscribe({ + next: () => { + this.notificationService.showSuccess('Holder inactivated successfully'); + this.searchHolders(); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to inactivate holder'); + this.notificationService.showError(errorMessage); + console.error('Error inactivating holder:', error); + } + }); + } + }); + } + + reactivateHolder(holderid: number): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '350px', + data: { + title: 'Confirm Reactivation', + message: 'Are you sure you want to reactivate this holder?', + confirmText: 'Yes', + cancelText: 'Cancel' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.holderService.reactivateHolder(holderid).subscribe({ + next: () => { + this.notificationService.showSuccess('Holder reactivated successfully'); + this.searchHolders(); + }, + error: (error) => { + 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.searchHolders(); + } + + onHolderSelection(holder: any): void { + this.selectedHolderId = holder.holderid; + } + + getAddressLabel(holder: BasicDetail): string { + + return `${holder.address1}, ${holder.address2} + ${holder.city}, ${holder.state}, ${holder.zip}, + ${holder.country}`; + } +}