diff --git a/src/app/core/services/platform/v1/spender/expenses.service.spec.ts b/src/app/core/services/platform/v1/spender/expenses.service.spec.ts index 0d3b907ffd..80505c1e61 100644 --- a/src/app/core/services/platform/v1/spender/expenses.service.spec.ts +++ b/src/app/core/services/platform/v1/spender/expenses.service.spec.ts @@ -6,6 +6,7 @@ import { expenseData, readyToReportExpensesData2 } from 'src/app/core/mock-data/ import { PAGINATION_SIZE } from 'src/app/constants'; import { expensesResponse } from 'src/app/core/mock-data/platform/v1/expenses-response.data'; import { getExpensesQueryParams } from 'src/app/core/mock-data/platform/v1/expenses-query-params.data'; +import { expensesCacheBuster$ } from '../../../transaction.service'; describe('ExpensesService', () => { let service: ExpensesService; @@ -77,6 +78,7 @@ describe('ExpensesService', () => { describe('getAllExpenses(): ', () => { it('should get all expenses for multiple pages', (done) => { + expensesCacheBuster$.next(null); spyOn(service, 'getExpensesCount').and.returnValue(of(4)); spyOn(service, 'getExpenses').and.returnValue(of(readyToReportExpensesData2)); @@ -96,6 +98,7 @@ describe('ExpensesService', () => { }); it('should get all expenses in a single page', (done) => { + expensesCacheBuster$.next(null); spyOn(service, 'getExpensesCount').and.returnValue(of(2)); spyOn(service, 'getExpenses').and.returnValue(of(readyToReportExpensesData2)); diff --git a/src/app/core/services/platform/v1/spender/expenses.service.ts b/src/app/core/services/platform/v1/spender/expenses.service.ts index 9fb9eb247e..c8d856545f 100644 --- a/src/app/core/services/platform/v1/spender/expenses.service.ts +++ b/src/app/core/services/platform/v1/spender/expenses.service.ts @@ -36,6 +36,9 @@ export class ExpensesService { .pipe(map((expenses) => expenses.data)); } + @Cacheable({ + cacheBusterObserver: expensesCacheBuster$, + }) getAllExpenses(params: ExpensesQueryParams): Observable { return this.getExpensesCount(params.queryParams).pipe( switchMap((count) => { diff --git a/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.html b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.html new file mode 100644 index 0000000000..ce1dda9bd5 --- /dev/null +++ b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.html @@ -0,0 +1,85 @@ + + + + +
Add Expenses
+
+
+ {{ selectedElements?.length }} {{ selectedElements?.length > 1 ? 'Expenses' : 'Expense' }} - + {{ selectedTotalAmount || 0 | humanizeCurrency : homeCurrency }} +
+
+
+ + + + + + + + + + +
+
+ + +
+
+ + Select all +
+ +
+ + +
+
+ +
+
+ No expense in this report +
+ Looks like there are no complete expenses! +
+ +
+ Click on the + + to add a new expense to this report +
+
+
+
+
+ + + +
+ +
+
+
diff --git a/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.scss b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.scss new file mode 100644 index 0000000000..aa08f632b9 --- /dev/null +++ b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.scss @@ -0,0 +1,68 @@ +@import '.././../../../theme/colors.scss'; + +.add-expenses-to-report { + &--toolbar { + background-color: $pure-white; + border-bottom: 1px solid $grey-lighter; + } + + &--title-container { + color: $black; + font-weight: 500; + margin-left: -10%; + } + + &--title { + font-size: 20px; + line-height: 26px; + } + + &--close { + margin-left: 16px; + } + + &--select-all-checkbox { + font-size: 14px; + padding: 16px 16px; + background-color: $pure-white; + } + + &--zero-state { + font-size: 14px; + display: flex; + flex-direction: column; + align-items: center; + color: $blue-black; + line-height: 1.5; + font-weight: 500; + margin: 20% 20px 10px; + + &--header { + margin-top: 12px; + margin-bottom: 10px; + font-size: 15px; + color: $black; + font-weight: 500; + display: flex; + flex-direction: column; + align-items: center; + } + &--sub-header { + font-size: 14px; + text-align: center; + color: $dark-grey; + display: flex; + align-items: center; + justify-content: center; + gap: -1px; + &--icon { + height: 14px; + color: $dark-grey; + } + } + } + + &--footer { + box-shadow: 0px -2px 40px rgba(215, 215, 215, 0.4); + } +} diff --git a/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.spec.ts b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.spec.ts new file mode 100644 index 0000000000..35236480af --- /dev/null +++ b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.spec.ts @@ -0,0 +1,214 @@ +import { CurrencyPipe } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { of } from 'rxjs'; +import { click, getElementBySelector, getTextContent } from 'src/app/core/dom-helpers'; +import { CurrencyService } from 'src/app/core/services/currency.service'; +import { FyCurrencyPipe } from 'src/app/shared/pipes/fy-currency.pipe'; +import { HumanizeCurrencyPipe } from 'src/app/shared/pipes/humanize-currency.pipe'; +import { AddExpensesToReportV2Component } from './add-expenses-to-report-v2.component'; +import { expenseData } from 'src/app/core/mock-data/platform/v1/expense.data'; + +describe('AddExpensesToReportV2Component', () => { + let component: AddExpensesToReportV2Component; + let fixture: ComponentFixture; + let modalController: jasmine.SpyObj; + let currencyService: jasmine.SpyObj; + let router: jasmine.SpyObj; + + const expense1 = expenseData; + const expense2 = { ...expenseData, id: 'txcSFe6efB62' }; + const expense3 = { ...expenseData, id: 'txcSFe6efB63' }; + + beforeEach(waitForAsync(() => { + const modalControllerSpy = jasmine.createSpyObj('ModalController', ['dismiss']); + const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + declarations: [AddExpensesToReportV2Component, HumanizeCurrencyPipe], + imports: [IonicModule.forRoot()], + providers: [ + FyCurrencyPipe, + CurrencyPipe, + { + provide: ModalController, + useValue: modalControllerSpy, + }, + { + provide: CurrencyService, + useValue: currencyServiceSpy, + }, + { + provide: Router, + useValue: routerSpy, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], + }).compileComponents(); + fixture = TestBed.createComponent(AddExpensesToReportV2Component); + component = fixture.componentInstance; + + modalController = TestBed.inject(ModalController) as jasmine.SpyObj; + currencyService = TestBed.inject(CurrencyService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + + currencyService.getHomeCurrency.and.returnValue(of('USD')); + component.selectedExpenseIds = ['txCYDX0peUw5', 'txfCdl3TEZ7K']; + component.selectedTotalAmount = 500; + component.selectedTotalExpenses = 2; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('close(): should dismiss modal', () => { + const closeButton = getElementBySelector(fixture, '.fy-icon-close') as HTMLElement; + click(closeButton); + + expect(modalController.dismiss).toHaveBeenCalledTimes(1); + }); + + it('addExpensestoReport(): should dismiss modal with new expense data', () => { + const addExpToReportButton = getElementBySelector(fixture, '.fy-footer-cta--primary') as HTMLElement; + click(addExpToReportButton); + + expect(modalController.dismiss).toHaveBeenCalledOnceWith({ + selectedExpenseIds: component.selectedExpenseIds, + }); + }); + + describe('updateSelectedExpenses():', () => { + it('should update selected expenses', () => { + component.selectedElements = [ + { ...expense1, is_reimbursable: true }, + { ...expense2, is_reimbursable: true }, + ]; + fixture.detectChanges(); + + component.updateSelectedExpenses(); + expect(component.selectedExpenseIds).toEqual([expense1.id, expense2.id]); + expect(component.selectedTotalExpenses).toEqual(2); + expect(component.selectedTotalAmount).toEqual(expense1.amount + expense2.amount); + }); + + it('should update selected expenses if expense is non-reimbursable', () => { + component.selectedElements = [ + { ...expense1, is_reimbursable: true }, + { ...expense2, is_reimbursable: false }, + ]; + fixture.detectChanges(); + + component.updateSelectedExpenses(); + expect(component.selectedExpenseIds).toEqual([expense1.id, expense2.id]); + expect(component.selectedTotalExpenses).toEqual(2); + expect(component.selectedTotalAmount).toEqual(expense1.amount); + }); + }); + + describe('toggleExpense():', () => { + it('should toggle expenses and filter expense if already selected', () => { + spyOn(component, 'updateSelectedExpenses'); + component.unreportedExpenses = [expense1, expense2]; + component.selectedElements = [expense1, expense2]; + fixture.detectChanges(); + + component.toggleExpense(expense2); + expect(component.selectedElements).toEqual([expense1]); + expect(component.updateSelectedExpenses).toHaveBeenCalledTimes(1); + expect(component.isSelectedAll).toBeFalse(); + }); + + it('should toggle expenses and add expense if not selected', () => { + spyOn(component, 'updateSelectedExpenses'); + component.unreportedExpenses = [expense1, expense2, expense3]; + component.selectedElements = [expense1, expense2]; + fixture.detectChanges(); + + component.toggleExpense(expense3); + expect(component.selectedElements).toEqual([expense1, expense2, expense3]); + expect(component.updateSelectedExpenses).toHaveBeenCalledTimes(1); + expect(component.isSelectedAll).toBeTrue(); + }); + }); + + describe('toggleSelectAll():', () => { + it('should select all expenses if value is true', () => { + spyOn(component, 'updateSelectedExpenses'); + component.unreportedExpenses = [expense1, expense2]; + fixture.detectChanges(); + + component.toggleSelectAll(true); + expect(component.selectedElements).toEqual([expense1, expense2]); + expect(component.updateSelectedExpenses).toHaveBeenCalledTimes(1); + }); + + it('should unselect and reset all selected expenses if value is false', () => { + component.toggleSelectAll(false); + + expect(component.selectedElements).toEqual([]); + expect(component.selectedTotalAmount).toEqual(0); + expect(component.selectedTotalExpenses).toEqual(0); + }); + }); + + it('ionViewWillEnter():', () => { + spyOn(component, 'updateSelectedExpenses'); + component.unreportedExpenses = [expense1, expense2]; + fixture.detectChanges(); + + component.ionViewWillEnter(); + expect(component.updateSelectedExpenses).toHaveBeenCalledTimes(1); + expect(currencyService.getHomeCurrency).toHaveBeenCalledTimes(2); + component.homeCurrency$.subscribe((res) => { + expect(res).toEqual('USD'); + }); + expect(component.isSelectedAll).toBeTrue(); + expect(component.selectedElements).toEqual([expense1, expense2]); + }); + + it('addNewExpense(): should navigate to add expense page', () => { + component.reportId = 'rpFE5X1Pqi9P'; + fixture.detectChanges(); + + const addNewExpenseButton = getElementBySelector(fixture, '.report-list--add-icon') as HTMLElement; + click(addNewExpenseButton); + + expect(router.navigate).toHaveBeenCalledOnceWith([ + '/', + 'enterprise', + 'add_edit_expense', + { rp_id: component.reportId, remove_from_report: false, navigate_back: true }, + ]); + expect(modalController.dismiss).toHaveBeenCalledTimes(1); + }); + + it('should show header if no expenses are not selected', () => { + component.selectedElements = []; + fixture.detectChanges(); + + expect(getTextContent(getElementBySelector(fixture, '.report-list--title'))).toEqual('Add Expenses'); + }); + + it('should show number of expenses and total amount', () => { + component.selectedElements = [expense1, expense2]; + fixture.detectChanges(); + + expect(getTextContent(getElementBySelector(fixture, '.add-expenses-to-report--title'))).toEqual( + '2 Expenses - $500.00' + ); + }); + + it('should zero state message if no unreported expense exist', () => { + component.unreportedExpenses = []; + fixture.detectChanges(); + + expect(getTextContent(getElementBySelector(fixture, '.add-expenses-to-report--zero-state--header'))).toEqual( + 'Looks like there are no complete expenses!' + ); + }); +}); diff --git a/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.ts b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.ts new file mode 100644 index 0000000000..aa35d67f84 --- /dev/null +++ b/src/app/fyle/my-view-report/add-expenses-to-report-v2/add-expenses-to-report-v2.component.ts @@ -0,0 +1,108 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { Observable } from 'rxjs'; +import { CurrencyService } from 'src/app/core/services/currency.service'; +import { Router } from '@angular/router'; +import { Expense } from 'src/app/core/models/platform/v1/expense.model'; + +@Component({ + selector: 'app-add-expenses-to-report', + templateUrl: './add-expenses-to-report-v2.component.html', + styleUrls: ['./add-expenses-to-report-v2.component.scss'], +}) +export class AddExpensesToReportV2Component implements OnInit { + @Input() unreportedExpenses: Expense[]; + + @Input() reportId: string; + + homeCurrency$: Observable; + + selectedTotalAmount = 0; + + selectedTotalExpenses = 0; + + selectedExpenseIds: string[]; + + selectedElements: Expense[]; + + isSelectedAll: boolean; + + homeCurrency: string; + + constructor( + private modalController: ModalController, + private currencyService: CurrencyService, + private router: Router + ) {} + + close() { + this.modalController.dismiss(); + } + + addExpensestoReport() { + this.modalController.dismiss({ + selectedExpenseIds: this.selectedExpenseIds, + }); + } + + updateSelectedExpenses() { + this.selectedExpenseIds = this.selectedElements.map((expense) => expense.id); + this.selectedTotalAmount = this.selectedElements.reduce((acc, expense) => { + if (expense.is_reimbursable) { + return acc + expense.amount; + } else { + return acc; + } + }, 0); + this.selectedTotalExpenses = this.selectedExpenseIds.length; + } + + toggleExpense(expense) { + const isSelectedElementsIncludesExpense = this.selectedElements.some((element) => element.id === expense.id); + if (isSelectedElementsIncludesExpense) { + this.selectedElements = this.selectedElements.filter((element) => element.id !== expense.id); + } else { + this.selectedElements.push(expense); + } + this.updateSelectedExpenses(); + this.isSelectedAll = this.selectedElements.length === this.unreportedExpenses.length; + } + + toggleSelectAll(value: boolean) { + if (value) { + this.selectedElements = this.unreportedExpenses; + this.updateSelectedExpenses(); + } else { + this.selectedElements = []; + this.selectedTotalAmount = 0; + this.selectedTotalExpenses = 0; + } + } + + ionViewWillEnter() { + this.isSelectedAll = true; + this.homeCurrency$ = this.currencyService.getHomeCurrency(); + const selectedExpenses = []; + this.unreportedExpenses.forEach((expense, i) => { + selectedExpenses.push(this.unreportedExpenses[i]); + }); + this.selectedElements = selectedExpenses; + this.updateSelectedExpenses(); + } + + addNewExpense() { + this.router.navigate([ + '/', + 'enterprise', + 'add_edit_expense', + { rp_id: this.reportId, remove_from_report: false, navigate_back: true }, + ]); + this.modalController.dismiss(); + } + + ngOnInit() { + this.currencyService.getHomeCurrency().subscribe((homeCurrency) => { + this.homeCurrency = homeCurrency; + }); + } +} diff --git a/src/app/fyle/my-view-report/my-view-report-v2.module.ts b/src/app/fyle/my-view-report/my-view-report-v2.module.ts index 1f8d065ac8..33def4f36f 100644 --- a/src/app/fyle/my-view-report/my-view-report-v2.module.ts +++ b/src/app/fyle/my-view-report/my-view-report-v2.module.ts @@ -11,10 +11,10 @@ import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { EditReportNamePopoverComponent } from './edit-report-name-popover/edit-report-name-popover.component'; -import { AddExpensesToReportComponent } from './add-expenses-to-report/add-expenses-to-report.component'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MyViewReportPageV2 } from './my-view-report-v2.page'; import { MyViewReportPageV2RoutingModule } from './my-view-report-v2-routing.module'; +import { AddExpensesToReportV2Component } from './add-expenses-to-report-v2/add-expenses-to-report-v2.component'; @NgModule({ imports: [ @@ -35,7 +35,7 @@ import { MyViewReportPageV2RoutingModule } from './my-view-report-v2-routing.mod MyViewReportPageV2, ShareReportComponent, EditReportNamePopoverComponent, - AddExpensesToReportComponent, + AddExpensesToReportV2Component, ], }) export class MyViewReportV2PageModule {} diff --git a/src/app/fyle/my-view-report/my-view-report-v2.page.html b/src/app/fyle/my-view-report/my-view-report-v2.page.html index 7fd0db0ea6..45eadc4327 100644 --- a/src/app/fyle/my-view-report/my-view-report-v2.page.html +++ b/src/app/fyle/my-view-report/my-view-report-v2.page.html @@ -181,7 +181,7 @@ No expense in this report
Looks like this report is empty.