diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 5576a1b..0636455 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -6,6 +6,9 @@ import { HomeComponent } from './home/home.component'; import { AppIdGuard } from './guards/appid.guard'; import { EditServiceProviderComponent } from './service-provider/edit/edit-service-provider.component'; import { UserSettingsComponent } from './user-settings/user-settings.component'; +import { ManagePreparerComponent } from './preparer/manage/manage-preparer.component'; +import { EditPreparerComponent } from './preparer/edit/edit-preparer.component'; +import { AddPreparerComponent } from './preparer/add/add-preparer.component'; export const routes: Routes = [ { path: 'login', component: LoginComponent }, @@ -16,6 +19,9 @@ export const routes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'usersettings', component: UserSettingsComponent }, { path: 'service-provider/:id', component: EditServiceProviderComponent }, + { path: 'preparer', component: ManagePreparerComponent }, + { path: 'preparer/:id', component: EditPreparerComponent }, + { path: 'add-preparer', component: AddPreparerComponent }, { path: '', redirectTo: 'home', pathMatch: 'full' } ], canActivate: [AuthGuard, AppIdGuard] diff --git a/src/app/core/models/preparer/location.ts b/src/app/core/models/preparer/location.ts index fc32e8f..3ab7ef4 100644 --- a/src/app/core/models/preparer/location.ts +++ b/src/app/core/models/preparer/location.ts @@ -1,10 +1,17 @@ export interface Location { - id: number; - clientId: number; - locationName: string; + locationid: number; + clientid: number; + name: string; address1: string; address2?: string | null; city: string; state: string; country: string; + zip: string; + dateCreated?: Date | null; + createdBy?: string | null; + lastUpdatedBy?: string | null; + lastUpdatedDate?: Date | null; + isInactive?: boolean | null; // TODO + inactivatedDate?: Date | null; // TODO } diff --git a/src/app/core/services/preparer/basic-detail.service.ts b/src/app/core/services/preparer/basic-detail.service.ts index 5e2c39a..8d0332d 100644 --- a/src/app/core/services/preparer/basic-detail.service.ts +++ b/src/app/core/services/preparer/basic-detail.service.ts @@ -39,18 +39,18 @@ export class BasicDetailService { createBasicDetails(data: BasicDetail): Observable { const basicDetails = { - p_spid: this.userService.getUserSpid(), - p_clientname: data.name, - p_lookupcode: data.lookupCode, - p_address1: data.address1, - p_address2: data.address2, - p_city: data.city, - p_state: data.state, - p_country: data.country, - p_zip: data.zip, - p_issuingregion: data.carnetIssuingRegion, - p_revenuelocation: data.revenueLocation, - p_userid: this.userService.getUser(), + P_SPID: this.userService.getUserSpid(), + P_CLIENTNAME: data.name, + P_LOOKUPCODE: data.lookupCode, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_COUNTRY: data.country, + P_ZIP: data.zip, + P_ISSUINGREGION: data.carnetIssuingRegion, + P_REVENUELOCATION: data.revenueLocation, + P_USERID: this.userService.getUser(), } return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateNewClients`, basicDetails); @@ -58,18 +58,18 @@ export class BasicDetailService { updateBasicDetails(id: number, data: BasicDetail): Observable { const basicDetails = { - p_spid: this.userService.getUserSpid(), - p_clientid: id, - p_clientname: data.name, - p_lookupcode: data.lookupCode, - p_address1: data.address1, - p_address2: data.address2, - p_city: data.city, - p_state: data.state, - p_country: data.country, - p_zip: data.zip, - p_revenuelocation: data.revenueLocation, - p_userid: this.userService.getUser(), + P_SPID: this.userService.getUserSpid(), + P_CLIENTID: id, + P_PREPARERNAME: data.name, + // P_LOOKUPCODE: data.lookupCode, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_COUNTRY: data.country, + P_ZIP: data.zip, + P_REVENUELOCATION: data.revenueLocation, + P_USERID: this.userService.getUser(), } return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateClient`, basicDetails); diff --git a/src/app/core/services/preparer/client.service.ts b/src/app/core/services/preparer/client.service.ts index 2f534ce..2f48fe9 100644 --- a/src/app/core/services/preparer/client.service.ts +++ b/src/app/core/services/preparer/client.service.ts @@ -1,9 +1,46 @@ import { Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { UserService } from '../common/user.service'; +import { BasicDetail } from '../../models/preparer/basic-detail'; +import { map, Observable } from 'rxjs'; +import { PreparerFilter } from '../../models/preparer/preparer-filter'; @Injectable({ providedIn: 'root' }) export class ClientService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + constructor(private http: HttpClient, private userService: UserService) { } + + getPreparers(filter: PreparerFilter): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetPreparers?P_SPID=${this.userService.getUserSpid()}&P_STATUS=ACTIVE&P_NAME=${filter.name}&P_LOOKUPCODE=${filter.lookupCode}&P_CITY=${filter.city}&P_STATE=${filter.state}`).pipe( + map(response => this.mapToClients(response))); + } + + private mapToClients(data: any[]): BasicDetail[] { + return data.map(basicDetails => ({ + clientid: basicDetails.CLIENTID, + spid: basicDetails.SPID, + name: basicDetails.PREPARERNAME, + lookupCode: basicDetails.LOOKUPCODE, + address1: basicDetails.ADDRESS1, + address2: basicDetails.ADDRESS2, + city: basicDetails.CITY, + state: basicDetails.STATE, + country: basicDetails.COUNTRY, + carnetIssuingRegion: basicDetails.ISSUINGREGION, + revenueLocation: basicDetails.REVENUELOCATION, + zip: basicDetails.ZIP, + // createdBy: basicDetails.CREATEDBY || null, + // dateCreated: basicDetails.DATECREATED || null, + // lastUpdatedBy: basicDetails.LASTUPDATEDBY || null, + // lastUpdatedDate: basicDetails.LASTUPDATEDDATE || null, + // isInactive: basicDetails.INACTIVEFLAG === 'Y' || false, + // inactivatedDate: basicDetails.INACTIVEDATE || null + })); + } - constructor() { } } diff --git a/src/app/core/services/preparer/contact.service.ts b/src/app/core/services/preparer/contact.service.ts index 98aee71..0fe5c6a 100644 --- a/src/app/core/services/preparer/contact.service.ts +++ b/src/app/core/services/preparer/contact.service.ts @@ -44,20 +44,20 @@ export class ContactService { createContact(clientid: number, data: Contact): Observable { const contact = { - p_spid: this.userService.getUserSpid(), - p_clientid: clientid, - p_defcontactflag: data.defaultContact ? 'Y' : 'N', - p_contactstable: [{ - FirstName: data.firstName, - LastName: data.lastName, - MiddleInitial: data.middleInitial, - Title: data.title, - EmailAddress: data.email, - MobileNo: data.mobile, - PhoneNo: data.phone, - FaxNo: data.fax + P_SPID: this.userService.getUserSpid(), + P_CLIENTID: clientid, + P_DEFCONTACTFLAG: data.defaultContact ? 'Y' : 'N', + 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_user_id: this.userService.getUser() + P_USERID: this.userService.getUser() } return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateClientContacts`, contact); @@ -65,17 +65,17 @@ export class ContactService { updateContact(spContactId: number, data: Contact): Observable { const contact = { - p_spid: this.userService.getUserSpid(), - p_clientcontactid: spContactId, - p_firstname: data.firstName, - p_lastname: data.lastName, - P_middleinitial: data.middleInitial, - p_title: data.title, - p_phone: data.phone, - p_mobileno: data.mobile, - p_fax: data.fax, - p_emailaddress: data.email, - p_user_id: this.userService.getUser() + P_SPID: this.userService.getUserSpid(), + P_CLIENTCONTACTID: spContactId, + 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}/UpdateClientContacts`, contact); diff --git a/src/app/core/services/preparer/location.service.ts b/src/app/core/services/preparer/location.service.ts index 60661dd..706284d 100644 --- a/src/app/core/services/preparer/location.service.ts +++ b/src/app/core/services/preparer/location.service.ts @@ -1,9 +1,82 @@ import { 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 { Location } from '../../models/preparer/location'; @Injectable({ providedIn: 'root' }) export class LocationService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; - constructor() { } + constructor(private http: HttpClient, private userService: UserService) { } + + getLocationsById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetPreparerLocByClientid?p_spid=${this.userService.getUserSpid()}&p_clientid=${id}`).pipe( + map(response => this.mapToLocations(response))); + } + + private mapToLocations(data: any[]): Location[] { + return data.map(location => ({ + locationid: location.LOCATIONID, + spid: location.SPID, + clientid: location.CLIENTID, + name: location.NAMEOF, + address1: location.ADDRESS1, + address2: location.ADDRESS2, + city: location.CITY, + state: location.STATE, + country: location.COUNTRY, + zip: location.ZIP, + createdBy: location.CREATEDBY || null, + dateCreated: location.DATECREATED || null, + lastUpdatedBy: location.LASTUPDATEDBY || null, + lastUpdatedDate: location.LASTUPDATEDDATE || null, + isInactive: location.INACTIVEFLAG === 'Y' || false, + inactivatedDate: location.INACTIVEDATE || null + })); + } + + createLocation(clientid: number, data: Location): Observable { + const location = { + P_SPID: this.userService.getUserSpid(), + P_CLIENTID: clientid, + P_CLIENTLOCADDRESSTABLE: [{ + P_NAMEOF: data.name, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_COUNTRY: data.country, + P_ZIP: data.zip, + }], + P_USERID: this.userService.getUser() + } + + return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateClientLocations`, location); + } + + updateLocation(locationId: number, data: Location): Observable { + const location = { + P_SPID: this.userService.getUserSpid(), + P_CLIENTLOCATIONID: locationId, + P_LOCATIONNAME: data.name, + P_ADDRESS1: data.address1, + P_ADDRESS2: data.address2, + P_CITY: data.city, + P_STATE: data.state, + P_COUNTRY: data.country, + P_ZIP: data.zip, + P_USERID: this.userService.getUser() + } + + return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateClientLocations`, location); + } + + // deleteLocation(clientContactId: string): Observable { + // return this.http.post(`${this.apiUrl}/${this.apiDb}/InactivateSPContact?p_clientcontactid=${clientContactId}`, null); + // } } diff --git a/src/app/preparer/add/add-preparer.component.html b/src/app/preparer/add/add-preparer.component.html index 7f57dd3..6f8c2e9 100644 --- a/src/app/preparer/add/add-preparer.component.html +++ b/src/app/preparer/add/add-preparer.component.html @@ -20,13 +20,13 @@ - + \ No newline at end of file diff --git a/src/app/preparer/basic-details/basic-details.component.ts b/src/app/preparer/basic-details/basic-details.component.ts index 3e35de6..5bd7534 100644 --- a/src/app/preparer/basic-details/basic-details.component.ts +++ b/src/app/preparer/basic-details/basic-details.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Subject, takeUntil, zip } from 'rxjs'; +import { Subject, takeUntil } from 'rxjs'; import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { CommonModule } from '@angular/common'; import { Country } from '../../core/models/country'; @@ -75,10 +75,10 @@ export class BasicDetailsComponent implements OnInit, OnDestroy { createForm(): FormGroup { return this.fb.group({ name: ['', [Validators.required, Validators.maxLength(100)]], - lookupCode: ['', Validators.required, Validators.maxLength(20)], - address1: ['', Validators.required, Validators.maxLength(100)], - address2: ['', [Validators.maxLength(100)]], - city: ['', Validators.required, Validators.maxLength(50)], + lookupCode: ['', [Validators.required, Validators.maxLength(20)]], + 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')]], @@ -88,7 +88,7 @@ export class BasicDetailsComponent implements OnInit, OnDestroy { } loadLookupData(): void { - this.commonService.getCountries(this.clientid) + this.commonService.getCountries(0) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (countries) => { @@ -126,13 +126,8 @@ export class BasicDetailsComponent implements OnInit, OnDestroy { .subscribe({ next: (states) => { this.states = states; - const stateControl = this.basicDetailsForm.get('state'); - if (this.countriesHasStates.includes(country)) { - stateControl?.enable(); - } else { - stateControl?.disable(); - stateControl?.setValue('FN'); - } + this.updateStateControl('state', country); + this.updateStateControl('revenueLocation', country); this.isLoading = false; }, error: (error) => { @@ -142,6 +137,16 @@ export class BasicDetailsComponent implements OnInit, OnDestroy { }); } + 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({ name: data.name, diff --git a/src/app/preparer/contacts/contacts.component.html b/src/app/preparer/contacts/contacts.component.html index cac25f2..c963cfc 100644 --- a/src/app/preparer/contacts/contacts.component.html +++ b/src/app/preparer/contacts/contacts.component.html @@ -1,5 +1,9 @@
+ + Show Inactive Contacts + + @@ -35,6 +39,12 @@ {{ contact.phone | phone }} + + + Mobile + {{ contact.mobile | phone }} + + Email @@ -59,13 +69,17 @@ edit + + + \ No newline at end of file diff --git a/src/app/preparer/edit/edit-preparer.component.ts b/src/app/preparer/edit/edit-preparer.component.ts index c1c4f73..1e12cb0 100644 --- a/src/app/preparer/edit/edit-preparer.component.ts +++ b/src/app/preparer/edit/edit-preparer.component.ts @@ -7,10 +7,11 @@ import { UserPreferences } from '../../core/models/user-preference'; import { ActivatedRoute } from '@angular/router'; import { UserPreferencesService } from '../../core/services/user-preference.service'; import { LocationComponent } from '../location/location.component'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-edit-preparer', - imports: [AngularMaterialModule, BasicDetailsComponent, ContactsComponent, LocationComponent], + imports: [AngularMaterialModule, CommonModule, BasicDetailsComponent, ContactsComponent, LocationComponent], templateUrl: './edit-preparer.component.html', styleUrl: './edit-preparer.component.scss' }) diff --git a/src/app/preparer/location/location.component.html b/src/app/preparer/location/location.component.html index ebd0258..f214f54 100644 --- a/src/app/preparer/location/location.component.html +++ b/src/app/preparer/location/location.component.html @@ -1 +1,221 @@ -

location works!

+
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location Name{{ location.name }}Address{{ getAddressLabel(location.address1, location.address2, + location.zip) }}City{{ location.city }}State{{ location.state }}Country{{ location.country }}Actions + + +
+ info + No records available +
+ + +
+ + +
+
+
+

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

+
+
+ + Name + + + Name is required + + + Maximum 100 characters allowed + + +
+ + +
+ + 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) + + +
+ +
+
+
+ +
+ +
+ {{locationReadOnlyFields.lastChangedBy || 'N/A'}} +
+
+ +
+ +
+ {{locationReadOnlyFields.isInactive === true ? 'Yes' : 'No' }} +
+
+
+
+ + +
+ +
+ {{(locationReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}} +
+
+ + +
+ +
+ {{(locationReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}} +
+
+
+
+
+ +
+ + +
+
+
+
\ No newline at end of file diff --git a/src/app/preparer/location/location.component.scss b/src/app/preparer/location/location.component.scss index e69de29..73f20d3 100644 --- a/src/app/preparer/location/location.component.scss +++ b/src/app/preparer/location/location.component.scss @@ -0,0 +1,163 @@ +.locations-container { + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; + + .actions-bar { + clear: both; + margin-bottom: -16px; + + 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; + } + } + + .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) { + .location-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/preparer/location/location.component.ts b/src/app/preparer/location/location.component.ts index c80ec0e..929c2e1 100644 --- a/src/app/preparer/location/location.component.ts +++ b/src/app/preparer/location/location.component.ts @@ -1,11 +1,267 @@ -import { Component } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +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 { Location } from '../../core/models/preparer/location'; +import { LocationService } from '../../core/services/preparer/location.service'; +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 { CustomPaginator } from '../../shared/custom-paginator'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; +import { ZipCodeValidator } from '../../shared/validators/zipcode-validator'; +import { Country } from '../../core/models/country'; +import { Region } from '../../core/models/region'; +import { State } from '../../core/models/state'; +import { Subject, takeUntil } from 'rxjs'; +import { CommonService } from '../../core/services/common/common.service'; @Component({ selector: 'app-location', - imports: [], + imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule], templateUrl: './location.component.html', - styleUrl: './location.component.scss' + styleUrl: './location.component.scss', + providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], }) export class LocationComponent { + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort!: MatSort; + displayedColumns: string[] = ['name', 'address', 'city', 'state', 'country', 'actions']; + dataSource = new MatTableDataSource(); + locationForm: FormGroup; + isEditing = false; + currentLocationId: number | null = null; + isLoading = false; + showForm = false; + countries: Country[] = []; + states: State[] = []; + + locationReadOnlyFields: any = { + lastChangedDate: null, + lastChangedBy: null, + isInactive: null, + inactivatedDate: null + }; + + @Input() clientid: number = 0; + @Input() userPreferences: UserPreferences = {}; + @Output() hasLocations = new EventEmitter(); + + countriesHasStates = ['US', 'CA', 'MX']; + + private destroy$ = new Subject(); + + constructor( + private fb: FormBuilder, + private locationService: LocationService, + private notificationService: NotificationService, + private dialog: MatDialog, + private errorHandler: ApiErrorHandlerService, + private commonService: CommonService + ) { + this.locationForm = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(100)]], + 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.loadCountries(); + this.loadLocations(); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + loadLocations(): void { + this.isLoading = true; + + this.locationService.getLocationsById(this.clientid).subscribe({ + next: (locations: Location[]) => { + this.dataSource.data = locations; + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load locations'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading locations:', error); + } + }); + } + + onCountryChange(country: string): void { + this.locationForm.get('state')?.reset(); + + if (country) { + this.loadStates(country); + } + + this.locationForm.get('zip')?.updateValueAndValidity(); + } + + addNewLocation(): void { + this.showForm = true; + this.isEditing = false; + this.currentLocationId = null; + this.locationForm.reset(); + } + + editLocation(location: Location): void { + this.showForm = true; + this.isEditing = true; + this.currentLocationId = location.locationid; + this.locationForm.patchValue({ + name: location.name, + address1: location.address1, + address2: location.address2, + city: location.city, + country: location.country, + state: location.state, + zip: location.zip, + }); + + if (location.country) { + this.loadStates(location.country); + } + + this.locationReadOnlyFields.lastChangedDate = location.lastUpdatedDate ?? location.dateCreated; + this.locationReadOnlyFields.lastChangedBy = location.lastUpdatedBy ?? location.createdBy; + this.locationReadOnlyFields.isInactive = location.isInactive; + this.locationReadOnlyFields.inactivatedDate = location.inactivatedDate; + } + + saveLocation(): void { + if (this.locationForm.invalid) { + this.locationForm.markAllAsTouched(); + return; + } + + // default the first location + const locationData: Location = this.locationForm.value; + + const saveObservable = this.isEditing && (this.currentLocationId! > 0) + ? this.locationService.updateLocation(this.currentLocationId!, locationData) + : this.locationService.createLocation(this.clientid, locationData); + + saveObservable.subscribe({ + next: () => { + this.notificationService.showSuccess(`Location ${this.isEditing ? 'updated' : 'added'} successfully`); + this.loadLocations(); + this.cancelEdit(); + this.hasLocations.emit(true); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} location`); + this.notificationService.showError(errorMessage); + console.error('Error saving location:', error); + } + }); + } + + loadCountries(): void { + this.commonService.getCountries(this.clientid) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (countries) => { + this.countries = countries; + }, + 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.clientid) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (states) => { + this.states = states; + const stateControl = this.locationForm.get('state'); + if (this.countriesHasStates.includes(country)) { + stateControl?.enable(); + } else { + stateControl?.disable(); + stateControl?.setValue('FN'); + } + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load states', error); + this.isLoading = false; + } + }); + } + + // deleteLocation(locationId: string): void { + // const dialogRef = this.dialog.open(ConfirmDialogComponent, { + // width: '350px', + // data: { + // title: 'Confirm Delete', + // message: 'Are you sure you want to delete this location?', + // confirmText: 'Delete', + // cancelText: 'Cancel' + // } + // }); + + // dialogRef.afterClosed().subscribe(result => { + // if (result) { + // this.locationService.deleteLocation(locationId).subscribe({ + // next: () => { + // this.notificationService.showSuccess('Location deleted successfully'); + // this.loadLocations(); + // }, + // error: (error) => { + // let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete location'); + // this.notificationService.showError(errorMessage); + // console.error('Error deleting location:', error); + // } + // }); + // } + // }); + // } + + getAddressLabel(address1: string, address2?: string, zip?: string): string { + let addressLabel = address1; + if (address2) { + addressLabel += `, ${address2}`; + } + if (zip) { + addressLabel += `, ${zip}`; + } + return addressLabel; + } + + getCountryLabel(value: string): string { + const country = this.countries.find(c => c.value === value); + return country ? country.name : value; + } + + cancelEdit(): void { + this.showForm = false; + this.isEditing = false; + this.currentLocationId = null; + this.locationForm.reset(); + } } diff --git a/src/app/preparer/manage/manage-preparer.component.html b/src/app/preparer/manage/manage-preparer.component.html new file mode 100644 index 0000000..509b159 --- /dev/null +++ b/src/app/preparer/manage/manage-preparer.component.html @@ -0,0 +1,133 @@ +
+ + +
+
+
+ +
+ + Name + + search + + + + Address + + +
+ + +
+ + City + + + + + State + + + + + Lookup Code + + +
+
+ +
+ + + +
+
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{client.name}}Address{{ getAddressLabel(client.address1, client.address2, client.zip)}} + City{{client.city}}State{{client.state}}Country{{client.country}}Carnet Issuing region {{getRegionLabel(client.carnetIssuingRegion)}}Revenue Location{{client.revenueLocation}}Actions + +
+ info + No records found matching your criteria +
+ + +
+
\ No newline at end of file diff --git a/src/app/preparer/manage/manage-preparer.component.scss b/src/app/preparer/manage/manage-preparer.component.scss new file mode 100644 index 0000000..5b2c7f5 --- /dev/null +++ b/src/app/preparer/manage/manage-preparer.component.scss @@ -0,0 +1,128 @@ +.page-header { + margin: 0.5rem 0px; + color: var(--mat-sys-primary); + font-weight: 500; +} + +.manage-preparers-container { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + + .search-fields { + display: flex; + flex-direction: column; + gap: 16px; + + .form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + align-items: start; + + .name, + .address { + grid-column: span 2; + } + + .city, + .state, + .lookup-code { + grid-column: span 1; + } + } + } + + .search-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + + button { + display: flex; + align-items: center; + } + } + + .results-section { + position: relative; + overflow: auto; + border-radius: 8px; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + } + + mat-table { + width: 100%; + + mat-icon { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + + .mat-column-actions { + width: 180px; + text-align: center; + } + + .mat-column-defaultContact { + width: 80px; + text-align: center; + } + } + + .no-data-message { + text-align: center; + padding: 0.9rem; + color: rgba(0, 0, 0, 0.54); + + mat-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + margin-bottom: -3px; + } + } + + mat-paginator { + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 0 0 8px 8px; + padding-top: 4px; + } + } +} + +@media (max-width: 960px) { + .manage-preparers-container { + .search-form { + .search-fields { + .form-row { + grid-template-columns: 1fr; + + .name, + .address, + .city, + .state, + .lookup-code { + grid-column: span 1; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/app/preparer/manage/manage-preparer.component.ts b/src/app/preparer/manage/manage-preparer.component.ts new file mode 100644 index 0000000..3ed66f7 --- /dev/null +++ b/src/app/preparer/manage/manage-preparer.component.ts @@ -0,0 +1,183 @@ +import { AfterViewInit, Component, OnDestroy, OnInit, 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 { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { Subject, takeUntil } from 'rxjs'; +import { BasicDetail } from '../../core/models/preparer/basic-detail'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { NavigationService } from '../../core/services/common/navigation.service'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { UserPreferences } from '../../core/models/user-preference'; +import { ClientService } from '../../core/services/preparer/client.service'; +import { PreparerFilter } from '../../core/models/preparer/preparer-filter'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { CustomPaginator } from '../../shared/custom-paginator'; +import { CommonService } from '../../core/services/common/common.service'; +import { Region } from '../../core/models/region'; +import { Country } from '../../core/models/country'; + +@Component({ + selector: 'app-manage', + imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule], + templateUrl: './manage-preparer.component.html', + styleUrl: './manage-preparer.component.scss', + providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }] +}) +export class ManagePreparerComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild(MatPaginator, { static: false }) + set paginator(value: MatPaginator) { + this.dataSource.paginator = value; + } + + @ViewChild(MatSort, { static: false }) + set sort(value: MatSort) { + this.dataSource.sort = value; + } + + searchForm: FormGroup; + showResults = false; + isLoading = false; + userPreferences: UserPreferences; + countries: Country[] = []; + regions: Region[] = []; + + displayedColumns: string[] = ['name', 'address', 'city', 'state', 'country', 'carnetIssuingRegion', 'revenueLocation', 'actions']; + dataSource = new MatTableDataSource([]); + + private destroy$ = new Subject(); + + constructor( + private fb: FormBuilder, + private notificationService: NotificationService, + private userPrefenceService: UserPreferencesService, + private navigationService: NavigationService, + private clientService: ClientService, + private errorHandler: ApiErrorHandlerService, + private commonService: CommonService + ) { + this.userPreferences = userPrefenceService.getPreferences(); + this.searchForm = this.createSearchForm(); + } + + ngOnInit(): void { + this.loadCountries(); + this.loadRegions(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + createSearchForm(): FormGroup { + return this.fb.group({ + name: [''], + address: [''], + city: [''], + state: [''], + lookupCode: [''] + }); + } + + isSearchCriteriaProvided(): boolean { + const values = this.searchForm.value; + return !!values.name || !!values.address || !!values.city || !!values.state || !!values.lookupCode; + } + + onSearch(): void { + if (this.searchForm.invalid || !this.isSearchCriteriaProvided()) { + return; + } + + this.isLoading = true; + this.showResults = true; + const filterData: PreparerFilter = this.searchForm.value; + + this.clientService.getPreparers(filterData).subscribe({ + next: (clients: BasicDetail[]) => { + this.dataSource.data = clients; + 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); + } + }); + } + + onClear(): void { + this.searchForm.reset(); + this.showResults = false; + this.dataSource.data = []; + } + + navigateTo(route: string): void { + this.navigationService.navigate([route]); + } + + onEdit(clientid: number): void { + this.navigationService.navigate(['preparer', clientid]); + } + + + loadRegions(): void { + this.commonService.getRegions() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (regions) => { + this.regions = regions; + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load regions', error); + this.isLoading = false; + } + }); + } + + loadCountries(): void { + this.commonService.getCountries(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (countries) => { + this.countries = countries; + }, + error: (error) => { + console.error('Failed to load countries', error); + this.isLoading = false; + } + }); + } + + getAddressLabel(address1: string, address2?: string, zip?: string): string { + let addressLabel = address1; + if (address2) { + addressLabel += `, ${address2}`; + } + if (zip) { + addressLabel += `, ${zip}`; + } + return addressLabel; + } + + getRegionLabel(value: string): string { + const region = this.regions.find(r => r.region === value); + return region ? region.regionname : value; + } + + getCountryLabel(value: string): string { + const country = this.countries.find(c => c.value === value); + return country ? country.name : value; + } +} \ No newline at end of file diff --git a/src/app/shared/module/angular-material.module.ts b/src/app/shared/module/angular-material.module.ts index 0c15592..89964d8 100644 --- a/src/app/shared/module/angular-material.module.ts +++ b/src/app/shared/module/angular-material.module.ts @@ -24,6 +24,7 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatAccordion, MatExpansionModule } from '@angular/material/expansion'; import { MatStepperModule } from '@angular/material/stepper'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/material-moment-adapter'; @NgModule({ @@ -53,7 +54,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m MatExpansionModule, MatAccordion, MatStepperModule, - MatMomentDateModule + MatMomentDateModule, + MatSlideToggleModule ], exports: [ MatButtonModule, @@ -79,7 +81,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m MatExpansionModule, MatAccordion, MatStepperModule, - MatMomentDateModule + MatMomentDateModule, + MatSlideToggleModule ], providers: [ { provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } }