diff --git a/src/app/core/mock-data/commute-details-response.data.ts b/src/app/core/mock-data/commute-details-response.data.ts new file mode 100644 index 0000000000..fe38f2e998 --- /dev/null +++ b/src/app/core/mock-data/commute-details-response.data.ts @@ -0,0 +1,34 @@ +import { CommuteDetailsResponse } from '../models/platform/commute-details-response.model'; +import { PlatformApiResponse } from '../models/platform/platform-api-response.model'; + +export const commuteDetailsResponseData: PlatformApiResponse = { + count: 1, + offset: 0, + data: [ + { + user_id: 'uswr93Wqcfjv', + full_name: 'John Doe', + email: 'ajain@fyle.in', + commute_details: { + distance: 10, + distance_unit: 'KM', + home_location: { + formatted_address: 'Home', + latitude: 12.9715987, + longitude: 77.5945667, + country: 'India', + state: 'Karnataka', + city: 'Bangalore', + }, + work_location: { + formatted_address: 'Work', + latitude: 12.9715987, + longitude: 77.5945667, + country: 'India', + state: 'Karnataka', + city: 'Bangalore', + }, + }, + }, + ], +}; diff --git a/src/app/core/models/task-event.enum.ts b/src/app/core/models/task-event.enum.ts index 892635adf9..1a5ba2bb40 100644 --- a/src/app/core/models/task-event.enum.ts +++ b/src/app/core/models/task-event.enum.ts @@ -8,4 +8,5 @@ export enum TASKEVENT { openPotentialDuplicates = 6, openSentBackAdvance = 7, mobileNumberVerification = 8, + commuteDetails = 9, } diff --git a/src/app/core/models/task-icon.enum.ts b/src/app/core/models/task-icon.enum.ts index e7b8d88f87..e26d16d7c9 100644 --- a/src/app/core/models/task-icon.enum.ts +++ b/src/app/core/models/task-icon.enum.ts @@ -3,4 +3,5 @@ export enum TaskIcon { WARNING = 'warning-outline', ADVANCE = 'wallet', MOBILE = 'phone', + LOCATION = 'location', } diff --git a/src/app/core/services/platform/v1/spender/employees.service.ts b/src/app/core/services/platform/v1/spender/employees.service.ts index f1cb8ad02a..4babd376a8 100644 --- a/src/app/core/services/platform/v1/spender/employees.service.ts +++ b/src/app/core/services/platform/v1/spender/employees.service.ts @@ -3,6 +3,8 @@ import { SpenderService } from './spender.service'; import { Observable } from 'rxjs'; import { CommuteDetails } from 'src/app/core/models/platform/v1/commute-details.model'; import { CommuteDetailsResponse } from 'src/app/core/models/platform/commute-details-response.model'; +import { PlatformApiResponse } from 'src/app/core/models/platform/platform-api-response.model'; +import { ExtendedOrgUser } from 'src/app/core/models/extended-org-user.model'; @Injectable({ providedIn: 'root', @@ -10,6 +12,14 @@ import { CommuteDetailsResponse } from 'src/app/core/models/platform/commute-det export class EmployeesService { constructor(private spenderService: SpenderService) {} + getCommuteDetails(eou: ExtendedOrgUser): Observable> { + return this.spenderService.get('/employees', { + params: { + user_id: `eq.${eou.us.id}`, + }, + }); + } + postCommuteDetails(commuteDetails: CommuteDetails): Observable<{ data: CommuteDetailsResponse }> { return this.spenderService.post('/employees/commute_details', { data: { diff --git a/src/app/core/services/tasks.service.spec.ts b/src/app/core/services/tasks.service.spec.ts index c7ea2880ea..57893dcf4d 100644 --- a/src/app/core/services/tasks.service.spec.ts +++ b/src/app/core/services/tasks.service.spec.ts @@ -42,6 +42,9 @@ import { OrgSettingsService } from './org-settings.service'; import { ExpensesService } from './platform/v1/spender/expenses.service'; import { expenseDuplicateSets } from '../mock-data/platform/v1/expense-duplicate-sets.data'; import { completeStats, incompleteStats } from '../mock-data/platform/v1/expenses-stats.data'; +import { EmployeesService } from './platform/v1/spender/employees.service'; +import { orgSettingsRes } from '../mock-data/org-settings.data'; +import { commuteDetailsResponseData } from '../mock-data/commute-details-response.data'; describe('TasksService', () => { let tasksService: TasksService; @@ -55,6 +58,7 @@ describe('TasksService', () => { let humanizeCurrencyPipe: jasmine.SpyObj; let orgSettingsService: jasmine.SpyObj; let expensesService: jasmine.SpyObj; + let employeesService: jasmine.SpyObj; const mockTaskClearSubject = new Subject(); const homeCurrency = 'INR'; @@ -76,6 +80,7 @@ describe('TasksService', () => { const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']); const humanizeCurrencyPipeSpy = jasmine.createSpyObj('HumanizeCurrencyPipe', ['transform']); const orgSettingsServiceSpy = jasmine.createSpyObj('OrgSettingsService', ['get']); + const employeesServiceSpy = jasmine.createSpyObj('EmployeesService', ['getCommuteDetails']); TestBed.configureTestingModule({ providers: [ @@ -120,6 +125,10 @@ describe('TasksService', () => { provide: ExpensesService, useValue: expensesServiceSpy, }, + { + provide: EmployeesService, + useValue: employeesServiceSpy, + }, ], }); tasksService = TestBed.inject(TasksService); @@ -136,6 +145,7 @@ describe('TasksService', () => { humanizeCurrencyPipe = TestBed.inject(HumanizeCurrencyPipe) as jasmine.SpyObj; orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj; expensesService = TestBed.inject(ExpensesService) as jasmine.SpyObj; + employeesService = TestBed.inject(EmployeesService) as jasmine.SpyObj; }); it('should be created', () => { @@ -700,6 +710,8 @@ describe('TasksService', () => { .and.returnValue(of(incompleteStats)); corporateCreditCardExpenseService.getCorporateCards.and.returnValue(of([mastercardRTFCard])); + orgSettingsService.get.and.returnValue(of(orgSettingsRes)); + employeesService.getCommuteDetails.and.returnValue(of(commuteDetailsResponseData)); } it('should be able to fetch tasks with no filters', (done) => { diff --git a/src/app/core/services/tasks.service.ts b/src/app/core/services/tasks.service.ts index 27beffcc04..f8e25d85ad 100644 --- a/src/app/core/services/tasks.service.ts +++ b/src/app/core/services/tasks.service.ts @@ -17,6 +17,8 @@ import { TaskDictionary } from '../models/task-dictionary.model'; import { CorporateCreditCardExpenseService } from './corporate-credit-card-expense.service'; import { Datum } from '../models/v2/stats-response.model'; import { ExpensesService } from './platform/v1/spender/expenses.service'; +import { OrgSettingsService } from './org-settings.service'; +import { EmployeesService } from './platform/v1/spender/employees.service'; @Injectable({ providedIn: 'root', @@ -40,7 +42,9 @@ export class TasksService { private advancesRequestService: AdvanceRequestService, private currencyService: CurrencyService, private corporateCreditCardExpenseService: CorporateCreditCardExpenseService, - private expensesService: ExpensesService + private expensesService: ExpensesService, + private orgSettingsService: OrgSettingsService, + private employeesService: EmployeesService ) { this.refreshOnTaskClear(); } @@ -294,6 +298,7 @@ export class TasksService { draftExpenses: this.getDraftExpensesTasks(), teamReports: this.getTeamReportsTasks(), sentBackAdvances: this.getSentBackAdvanceTasks(), + setCommuteDetails: this.getCommuteDetailsTasks(), }).pipe( map( ({ @@ -305,6 +310,7 @@ export class TasksService { draftExpenses, teamReports, sentBackAdvances, + setCommuteDetails, }) => { this.totalTaskCount$.next( mobileNumberVerification.length + @@ -314,7 +320,8 @@ export class TasksService { unreportedExpenses.length + teamReports.length + potentialDuplicates.length + - sentBackAdvances.length + sentBackAdvances.length + + setCommuteDetails.length ); this.expensesTaskCount$.next(draftExpenses.length + unreportedExpenses.length + potentialDuplicates.length); this.reportsTaskCount$.next(sentBackReports.length + unsubmittedReports.length); @@ -331,6 +338,7 @@ export class TasksService { !filters?.sentBackAdvances ) { return mobileNumberVerification + .concat(setCommuteDetails) .concat(potentialDuplicates) .concat(sentBackReports) .concat(draftExpenses) @@ -827,4 +835,52 @@ export class TasksService { mapScalarAdvanceStatsResponse(statsResponse: Datum[]): { totalCount: number; totalAmount: number } { return this.getStatsFromResponse(statsResponse, 'count(areq_id)', 'sum(areq_amount)'); } + + getCommuteDetailsTasks(): Observable { + const isCommuteDeductionEnabled$ = this.orgSettingsService + .get() + .pipe( + map( + (orgSettings) => + orgSettings.mileage?.allowed && + orgSettings.mileage.enabled && + orgSettings.commute_deduction_settings?.allowed && + orgSettings.commute_deduction_settings.enabled + ) + ); + + const commuteDetails$ = from(this.authService.getEou()).pipe( + switchMap((eou) => this.employeesService.getCommuteDetails(eou)) + ); + + return forkJoin({ + isCommuteDeductionEnabled: isCommuteDeductionEnabled$, + commuteDetails: commuteDetails$, + }).pipe( + switchMap(({ isCommuteDeductionEnabled, commuteDetails }) => { + if (isCommuteDeductionEnabled && !commuteDetails.data[0]?.commute_details?.home_location) { + return of(this.getCommuteDetailsTask()); + } + return of([]); + }) + ); + } + + getCommuteDetailsTask(): DashboardTask[] { + const task = [ + { + hideAmount: true, + header: 'Add Commute Details', + subheader: 'Add your Home and Work locations to easily deduct commute distance from your mileage expenses', + icon: TaskIcon.LOCATION, + ctas: [ + { + content: 'Add', + event: TASKEVENT.commuteDetails, + }, + ], + }, + ]; + return task; + } } diff --git a/src/app/fyle/dashboard/tasks/tasks.component.ts b/src/app/fyle/dashboard/tasks/tasks.component.ts index 75739ae453..38e0b02eed 100644 --- a/src/app/fyle/dashboard/tasks/tasks.component.ts +++ b/src/app/fyle/dashboard/tasks/tasks.component.ts @@ -27,6 +27,8 @@ import { FilterPill } from 'src/app/shared/components/fy-filter-pills/filter-pil import { SelectedFilters } from 'src/app/shared/components/fy-filters/selected-filters.interface'; import { ExpensesService } from 'src/app/core/services/platform/v1/spender/expenses.service'; import { ExpensesQueryParams } from 'src/app/core/models/platform/v1/expenses-query-params.model'; +import { FySelectCommuteDetailsComponent } from 'src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component'; +import { OverlayResponse } from 'src/app/core/models/overlay-response.modal'; @Component({ selector: 'app-tasks', @@ -371,6 +373,9 @@ export class TasksComponent implements OnInit { case TASKEVENT.mobileNumberVerification: this.onMobileNumberVerificationTaskClick(taskCta); break; + case TASKEVENT.commuteDetails: + this.onCommuteDetailsTaskClick(); + break; default: break; } @@ -645,4 +650,30 @@ export class TasksComponent implements OnInit { isSeparateCard, }); } + + showToastMessage(message: string, type: 'success' | 'failure'): void { + const panelClass = type === 'success' ? 'msb-success' : 'msb-failure'; + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties(type, { message }), + panelClass, + }); + this.trackingService.showToastMessage({ ToastContent: message }); + } + + async onCommuteDetailsTaskClick(): Promise { + const commuteDetailsModal = await this.modalController.create({ + component: FySelectCommuteDetailsComponent, + mode: 'ios', + }); + + await commuteDetailsModal.present(); + + const { data } = (await commuteDetailsModal.onWillDismiss()) as OverlayResponse<{ action: string }>; + + // Show toast message and refresh the page once commute details are saved + if (data.action === 'save') { + this.showToastMessage('Commute details saved successfully', 'success'); + this.doRefresh(); + } + } } diff --git a/src/app/fyle/my-profile/my-profile.page.html b/src/app/fyle/my-profile/my-profile.page.html index 3bf961dc13..57fa4e4e5d 100644 --- a/src/app/fyle/my-profile/my-profile.page.html +++ b/src/app/fyle/my-profile/my-profile.page.html @@ -38,7 +38,12 @@
Commute Details
-
+
Add Location
-
+
diff --git a/src/app/fyle/my-profile/my-profile.page.ts b/src/app/fyle/my-profile/my-profile.page.ts index 18d6b3e170..221009c2af 100644 --- a/src/app/fyle/my-profile/my-profile.page.ts +++ b/src/app/fyle/my-profile/my-profile.page.ts @@ -33,12 +33,11 @@ import { EventData } from 'src/app/core/models/event-data.model'; import { PreferenceSetting } from 'src/app/core/models/preference-setting.model'; import { CopyCardDetails } from 'src/app/core/models/copy-card-details.model'; import { SpenderService } from 'src/app/core/services/platform/v1/spender/spender.service'; -import { PlatformApiResponse } from 'src/app/core/models/platform/platform-api-response.model'; import { CommuteDetails } from 'src/app/core/models/platform/v1/commute-details.model'; -import { CommuteDetailsResponse } from 'src/app/core/models/platform/commute-details-response.model'; import { FySelectCommuteDetailsComponent } from 'src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component'; import { ModalPropertiesService } from 'src/app/core/services/modal-properties.service'; import { ToastType } from 'src/app/core/enums/toast-type.enum'; +import { EmployeesService } from 'src/app/core/services/platform/v1/spender/employees.service'; @Component({ selector: 'app-my-profile', @@ -84,8 +83,6 @@ export class MyProfilePage { isMastercardRTFEnabled: boolean; - isCommuteDetailsPresent: boolean; - isMileageEnabled: boolean; isCommuteDeductionEnabled: boolean; @@ -109,7 +106,8 @@ export class MyProfilePage { private spenderService: SpenderService, private activatedRoute: ActivatedRoute, private modalController: ModalController, - private modalProperties: ModalPropertiesService + private modalProperties: ModalPropertiesService, + private employeesService: EmployeesService ) {} setupNetworkWatcher(): void { @@ -225,22 +223,12 @@ export class MyProfilePage { } setCommuteDetails(): void { - this.eou$.subscribe((eou) => { - const queryParams = { - params: { - user_id: `eq.${eou.us.id}`, - }, - }; - return this.spenderService - .get>('/employees', queryParams) - .subscribe((res) => { - this.isCommuteDetailsPresent = !!res.data?.[0]?.commute_details?.home_location; - if (this.isCommuteDetailsPresent) { - this.commuteDetails = res.data[0].commute_details; - this.mileageDistanceUnit = this.commuteDetails.distance_unit === 'MILES' ? 'Miles' : 'KM'; - } - }); - }); + from(this.authService.getEou()) + .pipe(switchMap((eou) => this.employeesService.getCommuteDetails(eou))) + .subscribe((res) => { + this.commuteDetails = res.data[0].commute_details; + this.mileageDistanceUnit = this.commuteDetails.distance_unit === 'MILES' ? 'Miles' : 'KM'; + }); } setCCCFlags(): void { @@ -433,7 +421,6 @@ export class MyProfilePage { const commuteDetailsModal = await this.modalController.create({ component: FySelectCommuteDetailsComponent, componentProps: { - distanceUnit: this.mileageDistanceUnit, existingCommuteDetails: this.commuteDetails, }, mode: 'ios', diff --git a/src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component.ts b/src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component.ts index 62f72699a8..d867999080 100644 --- a/src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component.ts +++ b/src/app/shared/components/fy-select-commute-details/fy-select-commute-details.component.ts @@ -1,10 +1,11 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ModalController } from '@ionic/angular'; -import { switchMap } from 'rxjs'; +import { forkJoin, map, switchMap } from 'rxjs'; import { Location } from 'src/app/core/models/location.model'; import { CommuteDetails } from 'src/app/core/models/platform/v1/commute-details.model'; import { LocationService } from 'src/app/core/services/location.service'; +import { OrgSettingsService } from 'src/app/core/services/org-settings.service'; import { EmployeesService } from 'src/app/core/services/platform/v1/spender/employees.service'; @Component({ @@ -13,9 +14,7 @@ import { EmployeesService } from 'src/app/core/services/platform/v1/spender/empl styleUrls: ['./fy-select-commute-details.component.scss'], }) export class FySelectCommuteDetailsComponent implements OnInit { - @Input() distanceUnit: string; - - @Input() existingCommuteDetails: CommuteDetails; + @Input() existingCommuteDetails?: CommuteDetails; commuteDetails: FormGroup; @@ -23,7 +22,8 @@ export class FySelectCommuteDetailsComponent implements OnInit { private formBuilder: FormBuilder, private modalController: ModalController, private locationService: LocationService, - private employeesService: EmployeesService + private employeesService: EmployeesService, + private orgSettingsService: OrgSettingsService ) {} ngOnInit(): void { @@ -33,7 +33,7 @@ export class FySelectCommuteDetailsComponent implements OnInit { }); // In case if spender tries to edit the commute details, prefill form with existing details - if (this.existingCommuteDetails) { + if (this.existingCommuteDetails?.home_location) { const homeLocation = { ...this.existingCommuteDetails.home_location, display: this.existingCommuteDetails.home_location.formatted_address, @@ -48,9 +48,9 @@ export class FySelectCommuteDetailsComponent implements OnInit { } } - getCalculatedDistance(distanceResponse: number): number { + getCalculatedDistance(distanceResponse: number, distanceUnit: string): number { const distanceInKM = distanceResponse / 1000; - const finalDistance = this.distanceUnit === 'Miles' ? distanceInKM * 0.6213 : distanceInKM; + const finalDistance = distanceUnit === 'MILES' ? distanceInKM * 0.6213 : distanceInKM; return finalDistance; } @@ -64,16 +64,23 @@ export class FySelectCommuteDetailsComponent implements OnInit { if (this.commuteDetails.valid) { const commuteDetailsFormValue = this.commuteDetails.value as { homeLocation: Location; workLocation: Location }; - this.locationService - .getDistance(commuteDetailsFormValue.homeLocation, commuteDetailsFormValue.workLocation) + const getMileageUnit$ = this.orgSettingsService.get().pipe(map((orgSettings) => orgSettings.mileage?.unit)); + + forkJoin({ + getMileageUnit: getMileageUnit$, + distanceResponse: this.locationService.getDistance( + commuteDetailsFormValue.homeLocation, + commuteDetailsFormValue.workLocation + ), + }) .pipe( - switchMap((distanceResponse) => { - const distance = this.getCalculatedDistance(distanceResponse); + switchMap(({ getMileageUnit, distanceResponse }) => { + const distance = this.getCalculatedDistance(distanceResponse, getMileageUnit); const commuteDetails = { home_location: this.formatLocation(commuteDetailsFormValue.homeLocation), work_location: this.formatLocation(commuteDetailsFormValue.workLocation), distance, - distance_unit: this.distanceUnit.toUpperCase(), + distance_unit: getMileageUnit.toUpperCase(), }; return this.employeesService.postCommuteDetails(commuteDetails);