diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 607694fc..7949b807 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -5,6 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; import { MatLegacySnackBarModule as MatSnackBarModule, MAT_LEGACY_SNACK_BAR_DEFAULT_OPTIONS as MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/legacy-snack-bar'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import * as Sentry from '@sentry/angular'; @@ -24,7 +25,8 @@ import { GlobalErrorHandler } from './app.errorhandling'; HttpClientModule, FormsModule, ReactiveFormsModule, - MatSnackBarModule + MatSnackBarModule, + MatDialogModule, ], providers: [ { diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index debe9dbb..cd950b33 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -2,14 +2,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FlexLayoutModule } from '@angular/flex-layout'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; - import { LoginComponent } from './login/login.component'; import { LogoutComponent } from './logout/logout.component'; import { FyleCallbackComponent } from './fyle-callback/fyle-callback.component'; import { AuthComponent } from './auth.component'; import { AuthRoutingModule } from './auth-routing.module'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; import { SharedLoginComponent } from './shared-login/shared-login.component'; +import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; @NgModule({ @@ -25,7 +24,8 @@ import { SharedLoginComponent } from './shared-login/shared-login.component'; AuthRoutingModule, FlexLayoutModule, MatButtonModule, - MatProgressSpinnerModule + MatProgressSpinnerModule, + MatButtonModule ] }) export class AuthModule { } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 339a9110..b2ef55d1 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,12 +1,16 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; - +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; +import { EmailMultiSelectComponent } from './email-multi-select/email-multi-select.component'; @NgModule({ - declarations: [], + declarations: [ + EmailMultiSelectComponent + ], imports: [ - CommonModule + CommonModule, + MatDialogModule ] }) export class CoreModule { } diff --git a/src/app/core/email-multi-select/email-multi-select.component.html b/src/app/core/email-multi-select/email-multi-select.component.html new file mode 100644 index 00000000..5c827ccb --- /dev/null +++ b/src/app/core/email-multi-select/email-multi-select.component.html @@ -0,0 +1,57 @@ +
+ + + + +
+ +
+ +
+
+
+ +
+
+
+ + +
+ + +
+ +
+
+ +
+
+
+ +
diff --git a/src/app/core/email-multi-select/email-multi-select.component.scss b/src/app/core/email-multi-select/email-multi-select.component.scss new file mode 100644 index 00000000..e8ec6cb2 --- /dev/null +++ b/src/app/core/email-multi-select/email-multi-select.component.scss @@ -0,0 +1,111 @@ +.email-multi-select { + &--email-number { + margin-top: 5px; + } + + &--delele-all-icon { + margin-right: 5px; + color: #5a5d72; + } + + &--delele-all-icon-clone-settings { + margin-right: 25px; + color: #5a5d72; + } + + &--delele-all-icon img { + height: 10px; + width: 10px; + } + + &--delele-all-icon-clone-settings img { + height: 10px; + width: 10px; + } + + &--selected-email { + background-color: #ffffff; + border: 1px solid #dfdfe2; + border-radius: 12px; + padding: 10px; + height: 15px; + font-size: 12px; + margin-top: 2px; + } + + &--vertical { + border-left: 1px solid #dfdfe2; + height: 20px; + margin-left: 10px; + } + + &--display-email { + display: block; + max-width: 160px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &--bottom-name { + font-size: 12px; + line-height: 15px; + color: #A9ACBC; + } + + &--head-name { + font-size: 14px; + line-height: 18px; + color: #414562; + flex: none; + order: 1; + flex-grow: 0; + } + + &--no-result { + padding: 8px 16px; + margin: 10px; + } +} + +.configuration { + &--field-section { + padding: 0px 32px; + } +} + +p { + margin: 0; +} + +.mat-icon-close { + color: #a9acbc; + padding-left: 5px; + padding-top: 5px; +} + +.mat-icon-close img { + height: 10px; + width: 10px; +} + +.mat-primary .mat-option.mat-selected:not(.mat-option-disabled) { + color: #e91e63; +} + +.multiline-mat-option.mat-option { + white-space: normal; + line-height: normal; + height: auto !important; + font-size: none !important; +} + +.mat-option { + padding-top: 8px; + padding-bottom: 8px; + margin: 10px; +} + +.example-additional-selection { + margin-left: 10px; +} diff --git a/src/app/core/email-multi-select/email-multi-select.component.spec.ts b/src/app/core/email-multi-select/email-multi-select.component.spec.ts new file mode 100644 index 00000000..b012823c --- /dev/null +++ b/src/app/core/email-multi-select/email-multi-select.component.spec.ts @@ -0,0 +1,61 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmailMultiSelectComponent } from './email-multi-select.component'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { SearchPipe } from 'src/app/shared/pipes/search.pipe'; +import { FormBuilder } from '@angular/forms'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; + +describe('EmailMultiSelectComponent', () => { + let component: EmailMultiSelectComponent; + let fixture: ComponentFixture; + const routerSpy = { navigate: jasmine.createSpy('navigate'), url: '/path' }; + let router: Router; + let formBuilder: FormBuilder; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatDialogModule, NoopAnimationsModule], + declarations: [ EmailMultiSelectComponent, SearchPipe ], + providers: [ + FormBuilder, + { provide: Router, useValue: routerSpy } + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EmailMultiSelectComponent); + component = fixture.componentInstance; + formBuilder = TestBed.inject(FormBuilder); + const form = formBuilder.group({ + searchOption: [], + emails: [['fyle@fyle.in', 'integrations@fyle.in']], + employeeMapping: [['EMPLOYEE']] + }); + component.form = form; + const adminEmails: any[] = [{name: 'fyle', email: 'fyle@fyle.in'}, {name: 'dhaara', email: 'fyle1@fyle.in'}]; + component.options = adminEmails; + component.formControllerName = 'employeeMapping'; + component.isFieldMandatory = true; + component.placeholder = 'Select representation'; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('delete function check', () => { + const event = new Event("click", undefined); + expect(component.delete(event, 'fyle@fyle.in')).toBeUndefined(); + fixture.detectChanges(); + expect(component.form.controls.emails.value).toEqual(['integrations@fyle.in']); + expect(component.delete(event, 'fyle@fyle.in', true)).toBeUndefined(); + fixture.detectChanges(); + expect(component.form.controls.emails.value).toBeNull(); + }); +}); diff --git a/src/app/core/email-multi-select/email-multi-select.component.ts b/src/app/core/email-multi-select/email-multi-select.component.ts new file mode 100644 index 00000000..a3a685ea --- /dev/null +++ b/src/app/core/email-multi-select/email-multi-select.component.ts @@ -0,0 +1,53 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { AdvancedSettingFormOption } from 'src/app/core/models/configuration/advanced-setting.model'; +import { ExportSettingFormOption } from 'src/app/core/models/configuration/export-setting.model'; +import { SimpleSearchPage, SimpleSearchType } from 'src/app/core/models/enum/enum.model'; +import { HelperService } from 'src/app/core/services/core/helper.service'; + +@Component({ + selector: 'app-email-multi-select', + templateUrl: './email-multi-select.component.html', + styleUrls: ['./email-multi-select.component.scss'] +}) +export class EmailMultiSelectComponent implements OnInit { + + @Input() form: FormGroup; + + @Input() options: ExportSettingFormOption[] | AdvancedSettingFormOption[] | any[]; + + @Input() placeholder: string; + + @Input() formControllerName: string; + + @Input() isFieldMandatory: boolean; + + @Input() mandatoryErrorListName: string; + + @Input() customErrorMessage: string; + + @Input() isCloneSettings: boolean; + + SimpleSearchPage = SimpleSearchPage; + + SimpleSearchType = SimpleSearchType; + + constructor( + public helperService: HelperService + ) { } + + delete(event: Event, email: string, deleteAll: boolean = false) { + event.preventDefault(); + event.stopPropagation(); + if (deleteAll) { + this.form.controls.emails.patchValue(null); + } else { + const emails = this.form.value.emails.filter((value: string) => value !== email); + this.form.controls.emails.patchValue(emails); + } + } + + ngOnInit(): void { + } + +} diff --git a/src/app/core/models/configuration/clone-setting.model.spec.ts b/src/app/core/models/configuration/clone-setting.model.spec.ts new file mode 100644 index 00000000..0274b050 --- /dev/null +++ b/src/app/core/models/configuration/clone-setting.model.spec.ts @@ -0,0 +1,159 @@ +import { TestBed } from '@angular/core/testing'; +import { UntypedFormControl, UntypedFormGroup} from '@angular/forms'; +import { AutoMapEmployee, CCCExpenseState, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseState, MappingDestinationField, MappingSourceField, NameInJournalEntry, ReimbursableExpensesObject } from '../enum/enum.model'; +import { CloneSettingModel, CloneSettingPost } from './clone-setting.model'; +import { ImportSettingModel } from './import-setting.model'; +import { MappingSetting } from '../db/mapping-setting.model'; +describe('CloneSettingModel', () => { + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UntypedFormGroup], + declarations: [ CloneSettingModel ] + }) + .compileComponents(); + }); + + it('Should return CloneSettingModel[]', () => { + const expence_Field = [{ + source_field: 'PROJECT', + destination_field: 'CLASS', + import_to_fyle: true, + is_custom: true, + source_placeholder: 'Fyle' + }]; + + const cloneSettingForm= new UntypedFormGroup({ + employeeMapping: new UntypedFormControl('EMPLOYEE'), + autoMapEmployee: new UntypedFormControl('EMPLOYEE_CODE'), + expenseState: new UntypedFormControl('PAID'), + cccExpenseState: new UntypedFormControl('PAID'), + reimbursableExpense: new UntypedFormControl(true), + reimbursableExportType: new UntypedFormControl('BILL'), + reimbursableExportGroup: new UntypedFormControl('sample'), + reimbursableExportDate: new UntypedFormControl(null), + creditCardExpense: new UntypedFormControl(true), + creditCardExportType: new UntypedFormControl('BILL'), + creditCardExportGroup: new UntypedFormControl('sipper'), + creditCardExportDate: new UntypedFormControl(null), + bankAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultCCCAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + accountsPayable: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultCreditCardVendor: new UntypedFormControl({id: '1', name: 'Fyle'}), + qboExpenseAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultDebitCardAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + searchOption: new UntypedFormControl([]), + chartOfAccount: new UntypedFormControl(true), + chartOfAccountTypes: new UntypedFormControl([{enabled: true, name: 'expence'}]), + expenseFields: new UntypedFormControl(expence_Field), + importItems: new UntypedFormControl(true), + taxCode: new UntypedFormControl(true), + defaultTaxCode: new UntypedFormControl({id: '1', name: 'Fyle'}), + importVendorsAsMerchants: new UntypedFormControl(true), + paymentSync: new UntypedFormControl(true), + billPaymentAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + changeAccountingPeriod: new UntypedFormControl(true), + singleCreditLineJE: new UntypedFormControl(true), + autoCreateVendors: new UntypedFormControl(true), + autoCreateMerchantsAsVendors: new UntypedFormControl(true), + exportSchedule: new UntypedFormControl(true), + exportScheduleFrequency: new UntypedFormControl(10), + memoStructure: new UntypedFormControl(['Fyle']), + emails: new UntypedFormControl([]), + addedEmail: new UntypedFormControl([]), + skipExport: new UntypedFormControl(true) + }); + + const cloneSettingPayload: CloneSettingPost= { + employee_mappings: { + workspace_general_settings: { + employee_field_mapping: EmployeeFieldMapping.EMPLOYEE, + auto_map_employees: AutoMapEmployee.EMPLOYEE_CODE + } + }, + import_settings: { + workspace_general_settings: { + import_categories: true, + import_items: true, + charts_of_accounts: ImportSettingModel.formatChartOfAccounts([{enabled: true, name: 'expence'}]), + import_tax_codes: true, + import_vendors_as_merchants: true + }, + general_mappings: { + default_tax_code: {id: '1', name: 'Fyle'} + }, + mapping_settings: [{ + source_field: MappingSourceField.PROJECT, + destination_field: MappingDestinationField.CLASS, + import_to_fyle: true, + is_custom: false, + source_placeholder: 'Fyle' + }, + { + source_field: MappingSourceField.COST_CENTER, + destination_field: MappingDestinationField.CUSTOMER, + import_to_fyle: false, + is_custom: false, + source_placeholder: null + }] + }, + export_settings: { + expense_group_settings: { + expense_state: ExpenseState.PAID, + ccc_expense_state: CCCExpenseState.PAID, + reimbursable_expense_group_fields: ['sample'], + reimbursable_export_date_type: null, + corporate_credit_card_expense_group_fields: ['sipper'], + ccc_export_date_type: null + }, + workspace_general_settings: { + reimbursable_expenses_object: ReimbursableExpensesObject.BILL, + corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.BILL, + name_in_journal_entry: NameInJournalEntry.EMPLOYEE + }, + general_mappings: { + bank_account: {id: '1', name: 'Fyle'}, + default_ccc_account: {id: '1', name: 'Fyle'}, + accounts_payable: {id: '1', name: 'Fyle'}, + default_ccc_vendor: {id: '1', name: 'Fyle'}, + qbo_expense_account: {id: '1', name: 'Fyle'}, + default_debit_card_account: {id: '1', name: 'Fyle'} + } + }, + advanced_configurations: { + workspace_general_settings: { + sync_fyle_to_qbo_payments: false, + sync_qbo_to_fyle_payments: false, + auto_create_destination_entity: true, + auto_create_merchants_as_vendors: true, + je_single_credit_line: true, + change_accounting_period: true, + memo_structure: ['Fyle'] + }, + general_mappings: { + bill_payment_account: {id: '1', name: 'Fyle'} + }, + workspace_schedules: { + enabled: true, + interval_hours: 10, + emails_selected: [], + additional_email_options: [] + } + } + }; + + const existingMappingSettings: MappingSetting[] = [{ + id: 21, + created_at: new Date(), + updated_at: new Date(), + workspace: 1, + source_field: MappingSourceField.COST_CENTER, + destination_field: MappingDestinationField.CUSTOMER, + import_to_fyle: false, + is_custom: false, + source_placeholder: null + }]; + + expect(CloneSettingModel.constructPayload(cloneSettingForm, existingMappingSettings)).toEqual(cloneSettingPayload); + }); +}); diff --git a/src/app/core/models/configuration/clone-setting.model.ts b/src/app/core/models/configuration/clone-setting.model.ts new file mode 100644 index 00000000..5055b62f --- /dev/null +++ b/src/app/core/models/configuration/clone-setting.model.ts @@ -0,0 +1,45 @@ +import { FormGroup } from "@angular/forms"; +import { AdvancedSettingGet, AdvancedSettingModel, AdvancedSettingPost } from "./advanced-setting.model"; +import { ExportSettingGet, ExportSettingModel, ExportSettingPost } from "./export-setting.model"; +import { ImportSettingGet, ImportSettingModel, ImportSettingPost } from "./import-setting.model"; +import { MappingSetting } from "../db/mapping-setting.model"; +import { EmployeeSettingGet, EmployeeSettingModel, EmployeeSettingPost } from "./employee-setting.model"; + +export type CloneSetting = { + workspace_id: number, + export_settings: ExportSettingGet, + import_settings: ImportSettingGet, + advanced_configurations: AdvancedSettingGet, + employee_mappings: EmployeeSettingGet +} + +export type CloneSettingPost = { + export_settings: ExportSettingPost, + import_settings: ImportSettingPost, + advanced_configurations: AdvancedSettingPost, + employee_mappings: EmployeeSettingPost +} + +export type CloneSettingExist = { + is_available: boolean, + workspace_name: string +} + +export class CloneSettingModel { + static constructPayload(cloneSettingsForm: FormGroup, customMappingSettings: MappingSetting[]): CloneSettingPost { + + const exportSettingPayload = ExportSettingModel.constructPayload(cloneSettingsForm); + const importSettingPayload = ImportSettingModel.constructPayload(cloneSettingsForm, customMappingSettings); + const advancedSettingPayload = AdvancedSettingModel.constructPayload(cloneSettingsForm); + const employeeMappingPayload = EmployeeSettingModel.constructPayload(cloneSettingsForm); + + const cloneSettingPayload: CloneSettingPost = { + export_settings: exportSettingPayload, + import_settings: importSettingPayload, + advanced_configurations: advancedSettingPayload, + employee_mappings: employeeMappingPayload + }; + + return cloneSettingPayload; + } +} diff --git a/src/app/core/models/configuration/export-setting.model.ts b/src/app/core/models/configuration/export-setting.model.ts index ab4bdd1d..b30a00de 100644 --- a/src/app/core/models/configuration/export-setting.model.ts +++ b/src/app/core/models/configuration/export-setting.model.ts @@ -1,15 +1,19 @@ import { UntypedFormGroup } from "@angular/forms"; -import { CorporateCreditCardExpensesObject, ExpenseGroupingFieldOption, ExpenseState, CCCExpenseState, ExportDateType, ReimbursableExpensesObject, FyleField, NameInJournalEntry } from "../enum/enum.model"; +import { CorporateCreditCardExpensesObject, ExpenseGroupingFieldOption, ExpenseState, CCCExpenseState, ExportDateType, ReimbursableExpensesObject, NameInJournalEntry } from "../enum/enum.model"; import { ExpenseGroupSettingGet, ExpenseGroupSettingPost } from "../db/expense-group-setting.model"; import { DefaultDestinationAttribute, GeneralMapping } from "../db/general-mapping.model"; import { SelectFormOption } from "../misc/select-form-option.model"; -export type ExportSettingWorkspaceGeneralSetting = { +export type ExportSettingWorkspaceGeneralSettingPost = { reimbursable_expenses_object: ReimbursableExpensesObject | null, corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject | null name_in_journal_entry: NameInJournalEntry; } +export interface ExportSettingWorkspaceGeneralSetting extends ExportSettingWorkspaceGeneralSettingPost { + is_simplify_report_closure_enabled: boolean +} + export type ExportSettingGeneralMapping = { bank_account: DefaultDestinationAttribute, default_ccc_account: DefaultDestinationAttribute, @@ -21,7 +25,7 @@ export type ExportSettingGeneralMapping = { export type ExportSettingPost = { expense_group_settings: ExpenseGroupSettingPost, - workspace_general_settings: ExportSettingWorkspaceGeneralSetting, + workspace_general_settings: ExportSettingWorkspaceGeneralSettingPost, general_mappings: ExportSettingGeneralMapping } @@ -36,6 +40,10 @@ export interface ExportSettingFormOption extends SelectFormOption { value: ExpenseState | CCCExpenseState | ReimbursableExpensesObject | CorporateCreditCardExpensesObject | ExpenseGroupingFieldOption | ExportDateType; } +export interface NameInJournalEntryOptions extends SelectFormOption { + value: NameInJournalEntry +} + export class ExportSettingModel { static constructPayload(exportSettingsForm: UntypedFormGroup): ExportSettingPost { const emptyDestinationAttribute = {id: null, name: null}; diff --git a/src/app/core/models/configuration/import-setting.model.ts b/src/app/core/models/configuration/import-setting.model.ts index 832e47b8..e5d4896c 100644 --- a/src/app/core/models/configuration/import-setting.model.ts +++ b/src/app/core/models/configuration/import-setting.model.ts @@ -53,8 +53,9 @@ export interface ImportSettingFormOption extends SelectFormOption { export class ImportSettingModel { static constructPayload(importSettingsForm: UntypedFormGroup, customMappingSettings: MappingSetting[]): ImportSettingPost { + const emptyDestinationAttribute = {id: null, name: null}; - const employeeSettingPayload: ImportSettingPost = { + const importSettingPayload: ImportSettingPost = { workspace_general_settings: { import_categories: importSettingsForm.get('chartOfAccount')?.value, import_items: importSettingsForm.get('importItems')?.value, @@ -67,7 +68,7 @@ export class ImportSettingModel { }, mapping_settings: ImportSettingModel.formatMappingSettings(importSettingsForm.get('expenseFields')?.value, customMappingSettings) }; - return employeeSettingPayload; + return importSettingPayload; } static formatChartOfAccounts(chartOfAccounts: {enabled: boolean, name: string}[]): string[] { diff --git a/src/app/core/models/enum/enum.model.ts b/src/app/core/models/enum/enum.model.ts index 8ed51c82..07ebc46f 100644 --- a/src/app/core/models/enum/enum.model.ts +++ b/src/app/core/models/enum/enum.model.ts @@ -36,6 +36,11 @@ export enum ReimbursableExpensesObject { EXPENSE = 'EXPENSE' } +export enum ExportSource { + REIMBURSABLE = 'reimbursable', + CREDIT_CARD = 'credit card' +} + export enum CorporateCreditCardExpensesObject { CREDIT_CARD_PURCHASE = 'CREDIT CARD PURCHASE', BILL = 'BILL', @@ -182,7 +187,9 @@ export enum ClickEvent { UNMAPPED_MAPPINGS_FILTER = 'Unmapped Mappings Filter', MAPPED_MAPPINGS_FILTER = 'Mapped Mappings Filter', DISCONNECT_QBO = 'Disconnect QBO', - SYNC_DIMENSION = 'Sync Dimension' + SYNC_DIMENSION = 'Sync Dimension', + CLONE_SETTINGS_BACK = 'Clone Settings Back', + CLONE_SETTINGS_RESET = 'Clone Settings Reset' } export enum ProgressPhase { @@ -197,7 +204,8 @@ export enum OnboardingStep { EXPORT_SETTINGS = 'Export Settings', IMPORT_SETTINGS = 'Import Settings', ADVANCED_SETTINGS = 'Advanced Settings', - ONBOARDING_DONE = 'Onboarding Done' + ONBOARDING_DONE = 'Onboarding Done', + CLONE_SETTINGS = 'Clone Settings' } export enum UpdateEvent { diff --git a/src/app/core/models/misc/confirmation-dialog.model.ts b/src/app/core/models/misc/confirmation-dialog.model.ts index e8911337..7b0f0ebb 100644 --- a/src/app/core/models/misc/confirmation-dialog.model.ts +++ b/src/app/core/models/misc/confirmation-dialog.model.ts @@ -2,5 +2,6 @@ export type ConfirmationDialog = { title: string, primaryCtaText: string, contents: string, - hideSecondaryCTA?: boolean + hideSecondaryCTA?: boolean, + hideWarningIcon?: boolean }; diff --git a/src/app/core/models/misc/expense-field.model.ts b/src/app/core/models/misc/expense-field.model.ts index 818a6f8d..216fa3be 100644 --- a/src/app/core/models/misc/expense-field.model.ts +++ b/src/app/core/models/misc/expense-field.model.ts @@ -2,3 +2,12 @@ export type ExpenseField = { attribute_type: string; display_name: string; }; + +export type ExpenseFieldFormArray = { + source_field: string; + destination_field: string; + import_to_fyle: boolean; + disable_import_to_fyle: boolean; + source_placeholder: string, + addSourceField?: boolean +}; diff --git a/src/app/core/services/configuration/advanced-setting.service.spec.ts b/src/app/core/services/configuration/advanced-setting.service.spec.ts index 7d418906..9ea3531d 100644 --- a/src/app/core/services/configuration/advanced-setting.service.spec.ts +++ b/src/app/core/services/configuration/advanced-setting.service.spec.ts @@ -1,11 +1,16 @@ import { getTestBed, TestBed } from '@angular/core/testing'; import { AdvancedSettingService } from './advanced-setting.service'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { AdvancedSettingGet, AdvancedSettingPost } from '../../models/configuration/advanced-setting.model'; import { environment } from 'src/environments/environment'; import { WorkspaceScheduleEmailOptions } from '../../models/db/workspace-schedule.model'; import { ExpenseFilterResponse, SkipExport } from '../../models/misc/skip-export.model'; import { JoinOption, Operator } from '../../models/enum/enum.model'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; +import { FormBuilder } from '@angular/forms'; +import { paymentSyncOptions } from 'src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture'; +import { of } from 'rxjs'; describe('AdvancedSettingService', () => { let service: AdvancedSettingService; @@ -13,13 +18,21 @@ describe('AdvancedSettingService', () => { let httpMock: HttpTestingController; const API_BASE_URL = environment.api_url; const workspace_id = environment.tests.workspaceId; + let formbuilder: FormBuilder; + let dialogSpy: jasmine.Spy; + const dialogRefSpyObj = jasmine.createSpyObj({ afterClosed: of({hours: 1, + schedule_enabled: true, + emails_selected: ["fyle@fyle.in"], + email_added: {name: "fyle", email: 'fyle@fyle.in'}}), close: null }); + dialogRefSpyObj.componentInstance = { body: '' }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, MatDialogModule, NoopAnimationsModule], providers: [AdvancedSettingService] }); injector = getTestBed(); + formbuilder = TestBed.inject(FormBuilder); service = injector.inject(AdvancedSettingService); httpMock = injector.inject(HttpTestingController); }); @@ -200,4 +213,23 @@ describe('AdvancedSettingService', () => { }); req.flush(response); }); + + it('getPaymentSyncOptions function check', () => { + const value = service.getPaymentSyncOptions(); + expect(value).toEqual(paymentSyncOptions); + }); + + it('getFrequencyIntervals function check', () => { + service.getFrequencyIntervals(); + }); + + it('openAddemailDialog function check', () => { + const form = formbuilder.group({ + exportScheduleFrequency: 12, + emails: ['test@test.com'], + exportSchedule: true, + addedEmail: [] + }); + expect((service as any).openAddemailDialog(form, [])).toBeUndefined(); + }); }); diff --git a/src/app/core/services/configuration/advanced-setting.service.ts b/src/app/core/services/configuration/advanced-setting.service.ts index fe0d58cb..04ba17b4 100644 --- a/src/app/core/services/configuration/advanced-setting.service.ts +++ b/src/app/core/services/configuration/advanced-setting.service.ts @@ -1,12 +1,16 @@ import { HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, Output, EventEmitter } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { Cacheable, CacheBuster } from 'ts-cacheable'; -import { AdvancedSettingGet, AdvancedSettingPost, AdvancedSettingWorkspaceSchedulePost } from '../../models/configuration/advanced-setting.model'; +import { AdvancedSettingFormOption, AdvancedSettingGet, AdvancedSettingPost, AdvancedSettingWorkspaceSchedulePost } from '../../models/configuration/advanced-setting.model'; import { WorkspaceSchedule, WorkspaceScheduleEmailOptions } from '../../models/db/workspace-schedule.model'; import { ExpenseFilterResponse, SkipExport } from '../../models/misc/skip-export.model'; import { ApiService } from '../core/api.service'; import { WorkspaceService } from '../workspace/workspace.service'; +import { PaymentSyncDirection } from '../../models/enum/enum.model'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { FormGroup } from '@angular/forms'; +import { AddEmailDialogComponent } from 'src/app/shared/components/configuration/advanced-settings/add-email-dialog/add-email-dialog.component'; const advancedSettingsCache$ = new Subject(); const skipExportCache = new Subject(); @@ -16,9 +20,12 @@ const skipExportCache = new Subject(); }) export class AdvancedSettingService { + @Output() patchAdminEmailsEmitter: EventEmitter = new EventEmitter(); + constructor( private apiService: ApiService, - private workspaceService: WorkspaceService + private workspaceService: WorkspaceService, + private dialog: MatDialog ) { } @Cacheable({ @@ -59,4 +66,53 @@ export class AdvancedSettingService { getWorkspaceAdmins(): Observable<[WorkspaceScheduleEmailOptions]> { return this.apiService.get(`/workspaces/${this.workspaceService.getWorkspaceId()}/admins/`, {}); } + + getPaymentSyncOptions(): AdvancedSettingFormOption[] { + return [ + { + label: 'None', + value: null + }, + { + label: 'Export Fyle ACH Payments to Quickbooks Online', + value: PaymentSyncDirection.FYLE_TO_QBO + }, + { + label: 'Import Quickbooks Payments into Fyle', + value: PaymentSyncDirection.QBO_TO_FYLE + } + ]; + } + + getFrequencyIntervals(): AdvancedSettingFormOption[] { + return [...Array(24).keys()].map(day => { + return { + label: (day + 1) === 1 ? (day + 1) + ' Hour' : (day + 1) + ' Hours', + value: day + 1 + }; + }); + } + + openAddemailDialog(form: FormGroup, adminEmails: WorkspaceScheduleEmailOptions[]): void { + const dialogRef = this.dialog.open(AddEmailDialogComponent, { + width: '467px', + data: { + workspaceId: this.workspaceService.getWorkspaceId(), + hours: form.value.exportScheduleFrequency, + schedulEnabled: form.value.exportSchedule, + selectedEmails: form.value.emails + } + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result) { + form.controls.exportScheduleFrequency.patchValue(result.hours); + form.controls.emails.patchValue(result.emails_selected); + form.controls.addedEmail.patchValue(result.email_added); + + const additionalEmails = adminEmails.concat(result.email_added); + this.patchAdminEmailsEmitter.emit(additionalEmails); + } + }); + } } diff --git a/src/app/core/services/configuration/clone-setting.service.spec.ts b/src/app/core/services/configuration/clone-setting.service.spec.ts new file mode 100644 index 00000000..0084e9c9 --- /dev/null +++ b/src/app/core/services/configuration/clone-setting.service.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, getTestBed } from '@angular/core/testing'; + +import { CloneSettingService } from './clone-setting.service'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { mockCloneSettingsGet } from 'src/app/integration/onboarding/clone-settings/clone-settings.fixture'; +import { environment } from 'src/environments/environment'; +import { WorkspaceService } from '../workspace/workspace.service'; + +describe('CloneSettingService', () => { + let service: CloneSettingService; + let injector: TestBed; + let httpMock: HttpTestingController; + const API_BASE_URL = environment.api_url; + const workspace_id = environment.tests.workspaceId; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ] + }); + injector = getTestBed(); + service = TestBed.inject(CloneSettingService); + httpMock = injector.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should get Clone Settings', () => { + service.getCloneSettings().subscribe(value => { + expect(value).toEqual(mockCloneSettingsGet); + }); + const req = httpMock.expectOne({ + method: 'GET', + url: `${API_BASE_URL}/v2/workspaces/${workspace_id}/clone_settings/` + }); + req.flush(mockCloneSettingsGet); + }); + + it('should post Clone Settings', () => { + service.postCloneSettings(mockCloneSettingsGet).subscribe(value => { + expect(value).toEqual(mockCloneSettingsGet); + }); + const req = httpMock.expectOne({ + method: 'PUT', + url: `${API_BASE_URL}/v2/workspaces/${workspace_id}/clone_settings/` + }); + req.flush(mockCloneSettingsGet); + }); +}); diff --git a/src/app/core/services/configuration/clone-setting.service.ts b/src/app/core/services/configuration/clone-setting.service.ts new file mode 100644 index 00000000..df8f4d8e --- /dev/null +++ b/src/app/core/services/configuration/clone-setting.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CloneSetting, CloneSettingExist, CloneSettingPost } from '../../models/configuration/clone-setting.model'; +import { ApiService } from '../core/api.service'; +import { WorkspaceService } from '../workspace/workspace.service'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Injectable({ + providedIn: 'root' +}) + +export class CloneSettingService { + + workspaceId = this.workspaceService.getWorkspaceId(); + + constructor( + private apiService: ApiService, + private workspaceService: WorkspaceService + ) { } + + checkCloneSettingsExists(): Observable { + return this.apiService.get(`/user/clone_settings/exists/`, {}); + } + + getCloneSettings(): Observable { + return this.apiService.get(`/v2/workspaces/${this.workspaceId}/clone_settings/`, {}); + } + + postCloneSettings(cloneSettingsPayload: CloneSettingPost): Observable { + return this.apiService.put(`/v2/workspaces/${this.workspaceId}/clone_settings/`, cloneSettingsPayload); + } +} diff --git a/src/app/core/services/configuration/employee-setting.service.ts b/src/app/core/services/configuration/employee-setting.service.ts index 1f2c7585..432893b7 100644 --- a/src/app/core/services/configuration/employee-setting.service.ts +++ b/src/app/core/services/configuration/employee-setting.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { Cacheable, CacheBuster } from 'ts-cacheable'; -import { EmployeeSettingGet, EmployeeSettingPost } from '../../models/configuration/employee-setting.model'; +import { EmployeeSettingFormOption, EmployeeSettingGet, EmployeeSettingPost } from '../../models/configuration/employee-setting.model'; import { ApiService } from '../core/api.service'; import { WorkspaceService } from '../workspace/workspace.service'; +import { AutoMapEmployee, EmployeeFieldMapping } from '../../models/enum/enum.model'; const employeeSettingsCache$ = new Subject(); @@ -32,4 +33,38 @@ export class EmployeeSettingService { postEmployeeSettings(employeeSettingsPayload: EmployeeSettingPost): Observable { return this.apiService.put(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/map_employees/`, employeeSettingsPayload); } + + getEmployeeFieldMappingOptions(): EmployeeSettingFormOption[] { + return [ + { + label: 'Employees', + value: EmployeeFieldMapping.EMPLOYEE + }, + { + label: 'Vendor', + value: EmployeeFieldMapping.VENDOR + } + ]; + } + + getAutoMapEmployeeOptions(): EmployeeSettingFormOption[] { + return [ + { + value: null, + label: 'None' + }, + { + value: AutoMapEmployee.NAME, + label: 'Fyle Name to QuickBooks Online Display name' + }, + { + value: AutoMapEmployee.EMAIL, + label: 'Fyle Email to QuickBooks Online Email' + }, + { + value: AutoMapEmployee.EMPLOYEE_CODE, + label: 'Fyle Employee Code to QuickBooks Online Display name' + } + ]; + } } diff --git a/src/app/core/services/configuration/export-setting.service.spec.ts b/src/app/core/services/configuration/export-setting.service.spec.ts index 30b019b1..752dd799 100644 --- a/src/app/core/services/configuration/export-setting.service.spec.ts +++ b/src/app/core/services/configuration/export-setting.service.spec.ts @@ -1,9 +1,11 @@ import { getTestBed, TestBed } from '@angular/core/testing'; import { ExportSettingService } from './export-setting.service'; import { ExportSettingGet, ExportSettingPost } from '../../models/configuration/export-setting.model'; -import { ExpenseState, CCCExpenseState, ReimbursableExpensesObject, CorporateCreditCardExpensesObject, ExportDateType, NameInJournalEntry } from '../../models/enum/enum.model'; +import { ExpenseState, CCCExpenseState, ReimbursableExpensesObject, CorporateCreditCardExpensesObject, ExportDateType, ExpenseGroupingFieldOption, NameInJournalEntry } from '../../models/enum/enum.model'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; import { environment } from 'src/environments/environment'; +import { AbstractControl, FormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { exportResponse, mockCCCExpenseStateOptions, mockReimbursableExpenseGroupingFieldOptions, mockReimbursableExportTypeOptions } from 'src/app/shared/components/configuration/export-settings/export-settings.fixture'; describe('ExportSettingService', () => { let service: ExportSettingService; @@ -11,6 +13,8 @@ describe('ExportSettingService', () => { let httpMock: HttpTestingController; const API_BASE_URL = environment.api_url; const workspace_id = environment.tests.workspaceId; + let formbuilder: FormBuilder; + beforeEach(() => { TestBed.configureTestingModule({ @@ -18,6 +22,7 @@ describe('ExportSettingService', () => { providers: [ExportSettingService] }); injector = getTestBed(); + formbuilder = TestBed.inject(FormBuilder); service = injector.inject(ExportSettingService); httpMock = injector.inject(HttpTestingController); }); @@ -40,6 +45,7 @@ describe('ExportSettingService', () => { workspace_general_settings: { reimbursable_expenses_object: ReimbursableExpensesObject.BILL, corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.BILL, + is_simplify_report_closure_enabled: true, name_in_journal_entry: NameInJournalEntry.EMPLOYEE }, general_mappings: { @@ -99,6 +105,7 @@ describe('ExportSettingService', () => { workspace_general_settings: { reimbursable_expenses_object: ReimbursableExpensesObject.BILL, corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.BILL, + is_simplify_report_closure_enabled: true, name_in_journal_entry: NameInJournalEntry.MERCHANT }, general_mappings: { @@ -122,4 +129,109 @@ describe('ExportSettingService', () => { }); + it('exportSelectionValidator function check', () => { + const control = { value: ExpenseState.PAID, parent: formbuilder.group({ + reimbursableExpense: ReimbursableExpensesObject.BILL + }) }; + expect((service as any).exportSelectionValidator()(control as AbstractControl)).toEqual({forbiddenOption: { value: 'PAID' }}); + const control1 = { value: ExpenseState.PAYMENT_PROCESSING, parent: formbuilder.group({ + creditCardExpense: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE + }) }; + expect((service as any).exportSelectionValidator()(control1 as AbstractControl)).toEqual({forbiddenOption: { value: 'PAYMENT_PROCESSING' }}); + }); + + it('createReimbursableExpenseWatcher function check', () => { + const form = formbuilder.group({ + reimbursableExpense: false, + expenseState: ExpenseState.PAID, + reimbursableExportType: ReimbursableExpensesObject.BILL, + reimbursableExportDate: ExportDateType.APPROVED_AT, + reimbursableExportGroup: ExpenseGroupingFieldOption.EXPENSE_ID + }); + + expect((service as any).createReimbursableExpenseWatcher(form, exportResponse)).toBeUndefined(); + + form.controls.reimbursableExpense.patchValue(true); + expect((service as any).createReimbursableExpenseWatcher(form, exportResponse)).toBeUndefined(); + + form.controls.reimbursableExpense.patchValue(false); + expect((service as any).createReimbursableExpenseWatcher(form, exportResponse)).toBeUndefined(); + }); + + it('createCreditCardExpenseWatcher function check', () => { + const form = formbuilder.group({ + creditCardExpense: false, + cccExpenseState: ExpenseState.PAID, + creditCardExportType: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE, + creditCardExportGroup: ExpenseGroupingFieldOption.EXPENSE_ID, + creditCardExportDate: ExportDateType.POSTED_AT + }); + + expect((service as any).createCreditCardExpenseWatcher(form, exportResponse)).toBeUndefined(); + + form.controls.creditCardExpense.patchValue(true); + expect((service as any).createCreditCardExpenseWatcher(form, exportResponse)).toBeUndefined(); + + form.controls.creditCardExpense.patchValue(false); + expect((service as any).createCreditCardExpenseWatcher(form, exportResponse)).toBeUndefined(); + }); + + it('setGeneralMappingsValidator function check', () => { + const form= new UntypedFormGroup({ + employeeMapping: new UntypedFormControl('EMPLOYEE'), + autoMapEmployee: new UntypedFormControl('EMPLOYEE_CODE'), + expenseState: new UntypedFormControl('PAID'), + cccExpenseState: new UntypedFormControl('PAID'), + reimbursableExpense: new UntypedFormControl(true), + reimbursableExportType: new UntypedFormControl('BILL'), + reimbursableExportGroup: new UntypedFormControl('sample'), + reimbursableExportDate: new UntypedFormControl(null), + creditCardExpense: new UntypedFormControl(true), + creditCardExportType: new UntypedFormControl('BILL'), + creditCardExportGroup: new UntypedFormControl('sipper'), + creditCardExportDate: new UntypedFormControl(null), + bankAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultCCCAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + accountsPayable: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultCreditCardVendor: new UntypedFormControl({id: '1', name: 'Fyle'}), + qboExpenseAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + defaultDebitCardAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + searchOption: new UntypedFormControl([]), + chartOfAccount: new UntypedFormControl(true), + chartOfAccountTypes: new UntypedFormControl([{enabled: true, name: 'expence'}]), + importItems: new UntypedFormControl(true), + taxCode: new UntypedFormControl(true), + defaultTaxCode: new UntypedFormControl({id: '1', name: 'Fyle'}), + importVendorsAsMerchants: new UntypedFormControl(true), + paymentSync: new UntypedFormControl(true), + billPaymentAccount: new UntypedFormControl({id: '1', name: 'Fyle'}), + changeAccountingPeriod: new UntypedFormControl(true), + singleCreditLineJE: new UntypedFormControl(true), + autoCreateVendors: new UntypedFormControl(true), + autoCreateMerchantsAsVendors: new UntypedFormControl(true), + exportSchedule: new UntypedFormControl(true), + exportScheduleFrequency: new UntypedFormControl(10), + memoStructure: new UntypedFormControl(['Fyle']), + emails: new UntypedFormControl([]), + addedEmail: new UntypedFormControl([]), + skipExport: new UntypedFormControl(true) + }); + form.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE); + expect((service as any).setGeneralMappingsValidator(form)).toBeUndefined(); + expect((service as any).showCCCAccountsPayableField(form)); + + }); + + it('function check', () => { + expect((service as any).getExportGroup([ExpenseGroupingFieldOption.EXPENSE_ID])).toEqual('expense_id'); + expect((service as any).getExportGroup(null)).toEqual(''); + + expect((service as any).getReimbursableExpenseGroupingFieldOptions()); + expect((service as any).getReimbursableExportTypeOptions()); + expect((service as any).getcreditCardExportTypes()); + expect((service as any).getReimbursableExpenseGroupingDateOptions()); + expect((service as any).getReimbursableExpenseStateOptions()); + expect((service as any).getCCCExpenseStateOptions()); + expect((service as any).nameInJournalOptions()); + }); }); diff --git a/src/app/core/services/configuration/export-setting.service.ts b/src/app/core/services/configuration/export-setting.service.ts index 4508aed8..04cdf805 100644 --- a/src/app/core/services/configuration/export-setting.service.ts +++ b/src/app/core/services/configuration/export-setting.service.ts @@ -1,9 +1,14 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ExportSettingGet, ExportSettingPost } from '../../models/configuration/export-setting.model'; +import { ExportSettingGet, ExportSettingPost, ExportSettingFormOption, NameInJournalEntryOptions } from '../../models/configuration/export-setting.model'; import { ApiService } from '../core/api.service'; import { WorkspaceService } from '../workspace/workspace.service'; +import { AutoMapEmployee, CCCExpenseState, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExpenseState, ExportDateType, NameInJournalEntry, ReimbursableExpensesObject } from '../../models/enum/enum.model'; +import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { EmployeeSettingFormOption } from '../../models/configuration/employee-setting.model'; + + @Injectable({ providedIn: 'root' }) @@ -21,4 +26,302 @@ export class ExportSettingService { postExportSettings(exportSettingsPayload: ExportSettingPost): Observable { return this.apiService.put(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/export_settings/`, exportSettingsPayload); } + + createReimbursableExpenseWatcher(form: FormGroup, exportSettings: ExportSettingGet): void { + form.controls.reimbursableExpense.valueChanges.subscribe((isReimbursableExpenseSelected) => { + if (isReimbursableExpenseSelected) { + form.controls.expenseState.setValidators(Validators.required); + form.controls.expenseState.setValue(exportSettings.expense_group_settings?.expense_state ? exportSettings.expense_group_settings?.expense_state : ExpenseState.PAYMENT_PROCESSING); + form.controls.reimbursableExportType.setValidators(Validators.required); + form.controls.reimbursableExportGroup.setValidators(Validators.required); + form.controls.reimbursableExportDate.setValidators(Validators.required); + } else { + form.controls.expenseState.clearValidators(); + form.controls.reimbursableExportType.clearValidators(); + form.controls.reimbursableExportGroup.clearValidators(); + form.controls.reimbursableExportDate.clearValidators(); + form.controls.expenseState.setValue(null); + form.controls.reimbursableExportType.setValue(null); + form.controls.reimbursableExportGroup.setValue(null); + form.controls.reimbursableExportDate.setValue(null); + } + }); + } + + createCreditCardExpenseWatcher(form: FormGroup, exportSettings: ExportSettingGet): void { + form.controls.creditCardExpense.valueChanges.subscribe((isCreditCardExpenseSelected) => { + if (isCreditCardExpenseSelected) { + form.controls.cccExpenseState.setValidators(Validators.required); + form.controls.cccExpenseState.setValue(exportSettings.expense_group_settings?.ccc_expense_state ? exportSettings.expense_group_settings?.ccc_expense_state : exportSettings.workspace_general_settings.is_simplify_report_closure_enabled ? CCCExpenseState.APPROVED: CCCExpenseState.PAYMENT_PROCESSING); + form.controls.creditCardExportType.setValidators(Validators.required); + form.controls.creditCardExportGroup.setValidators(Validators.required); + form.controls.creditCardExportDate.setValidators(Validators.required); + } else { + form.controls.cccExpenseState.clearValidators(); + form.controls.creditCardExportType.clearValidators(); + form.controls.creditCardExportGroup.clearValidators(); + form.controls.creditCardExportDate.clearValidators(); + form.controls.cccExpenseState.setValue(null); + form.controls.creditCardExportType.setValue(null); + form.controls.creditCardExportGroup.setValue(null); + form.controls.creditCardExportDate.setValue(null); + } + }); + } + + getExportGroup(exportGroups: string[] | null): string { + if (exportGroups) { + const exportGroup = exportGroups.find((exportGroup) => { + return exportGroup === ExpenseGroupingFieldOption.EXPENSE_ID || exportGroup === ExpenseGroupingFieldOption.CLAIM_NUMBER || exportGroup === ExpenseGroupingFieldOption.SETTLEMENT_ID; + }); + return exportGroup ? exportGroup : ExpenseGroupingFieldOption.CLAIM_NUMBER; + } + + return ''; + } + + nameInJournalOptions(): NameInJournalEntryOptions[] { + return [ + { + label: 'Merchant Name', + value: NameInJournalEntry.MERCHANT + }, + { + label: 'Employee Name', + value: NameInJournalEntry.EMPLOYEE + } + ]; + } + + getReimbursableExpenseGroupingFieldOptions() { + return [ + { + label: 'Report', + value: ExpenseGroupingFieldOption.CLAIM_NUMBER + }, + { + label: 'Payment', + value: ExpenseGroupingFieldOption.SETTLEMENT_ID + }, + { + label: 'Expense', + value: ExpenseGroupingFieldOption.EXPENSE_ID + } + ]; + } + + getReimbursableExportTypeOptions(employeeFieldMapping: EmployeeFieldMapping): ExportSettingFormOption[] { + return { + EMPLOYEE: [ + { + label: 'Check', + value: ReimbursableExpensesObject.CHECK + }, + { + label: 'Expense', + value: ReimbursableExpensesObject.EXPENSE + }, + { + label: 'Journal Entry', + value: ReimbursableExpensesObject.JOURNAL_ENTRY + } + ], + VENDOR: [ + { + label: 'Bill', + value: ReimbursableExpensesObject.BILL + }, + { + label: 'Expense', + value: ReimbursableExpensesObject.EXPENSE + }, + { + label: 'Journal Entry', + value: ReimbursableExpensesObject.JOURNAL_ENTRY + } + ] + }[employeeFieldMapping]; + } + + getcreditCardExportTypes(): ExportSettingFormOption[] { + return [ + { + label: 'Bill', + value: CorporateCreditCardExpensesObject.BILL + }, + { + label: 'Credit Card Purchase', + value: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE + }, + { + label: 'Journal Entry', + value: CorporateCreditCardExpensesObject.JOURNAL_ENTRY + }, + { + label: 'Debit Card Expense', + value: CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE + } + ]; + } + + getReimbursableExpenseGroupingDateOptions(): ExportSettingFormOption[] { + return [ + { + label: 'Current Date', + value: ExportDateType.CURRENT_DATE + }, + { + label: 'Verification Date', + value: ExportDateType.VERIFIED_AT + }, + { + label: 'Spend Date', + value: ExportDateType.SPENT_AT + }, + { + label: 'Approval Date', + value: ExportDateType.APPROVED_AT + }, + { + label: 'Last Spend Date', + value: ExportDateType.LAST_SPENT_AT + } + ]; + } + + getReimbursableExpenseStateOptions(isSimplifyReportClosureEnabled: boolean): ExportSettingFormOption[] { + return [ + { + label: isSimplifyReportClosureEnabled ? 'Processing' : 'Payment Processing', + value: ExpenseState.PAYMENT_PROCESSING + }, + { + label: isSimplifyReportClosureEnabled ? 'Closed' : 'Paid', + value: ExpenseState.PAID + } + ]; + } + + getCCCExpenseStateOptions(isSimplifyReportClosureEnabled: boolean): ExportSettingFormOption[] { + return [ + { + label: isSimplifyReportClosureEnabled ? 'Approved' : 'Payment Processing', + value: isSimplifyReportClosureEnabled ? CCCExpenseState.APPROVED: CCCExpenseState.PAYMENT_PROCESSING + }, + { + label: isSimplifyReportClosureEnabled ? 'Closed' : 'Paid', + value: CCCExpenseState.PAID + } + ]; + } + + exportSelectionValidator(exportSettingsForm: FormGroup, isCloneSetting: boolean = false): ValidatorFn { + return (control: AbstractControl): {[key: string]: object} | null => { + let forbidden = true; + if (exportSettingsForm) { + if (typeof control.value === 'boolean') { + if (control.value) { + forbidden = false; + } else { + if (control.parent?.get('reimbursableExpense')?.value || control.parent?.get('creditCardExpense')?.value) { + forbidden = false; + } + } + } else if ((control.value === ExpenseState.PAID || control.value === ExpenseState.PAYMENT_PROCESSING || control.value === CCCExpenseState.APPROVED) + && (control.parent?.get('reimbursableExpense')?.value || control.parent?.get('creditCardExpense')?.value)) { + forbidden = false; + } else if (isCloneSetting && (control.parent?.get('reimbursableExpense')?.value || control.parent?.get('creditCardExpense')?.value)) { + forbidden = false; + } + + if (!forbidden) { + control.parent?.get('expenseState')?.setErrors(null); + control.parent?.get('cccExpenseState')?.setErrors(null); + control.parent?.get('reimbursableExpense')?.setErrors(null); + control.parent?.get('creditCardExpense')?.setErrors(null); + return null; + } + } + + return { + forbiddenOption: { + value: control.value + } + }; + }; + } + + showExpenseAccountField(form: FormGroup): boolean { + return form.controls.reimbursableExportType.value === ReimbursableExpensesObject.EXPENSE; + } + + showBankAccountField(form: FormGroup): boolean { + return form.value.employeeMapping === EmployeeFieldMapping.EMPLOYEE && form.controls.reimbursableExportType.value && form.controls.reimbursableExportType.value !== ReimbursableExpensesObject.EXPENSE; + } + + showReimbursableAccountsPayableField(form: FormGroup): boolean { + return (form.controls.reimbursableExportType.value === ReimbursableExpensesObject.BILL) || (form.controls.reimbursableExportType.value === ReimbursableExpensesObject.JOURNAL_ENTRY && form.value.employeeMapping === EmployeeFieldMapping.VENDOR); + } + + showCreditCardAccountField(form: FormGroup): boolean { + return form.controls.creditCardExportType.value && form.controls.creditCardExportType.value !== CorporateCreditCardExpensesObject.BILL && form.controls.creditCardExportType.value !== CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE; + } + + showDebitCardAccountField(form: FormGroup): boolean { + return form.controls.creditCardExportType.value && form.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE; + } + + showDefaultCreditCardVendorField(form: FormGroup): boolean { + return form.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.BILL; + } + + showCCCAccountsPayableField(form: FormGroup): boolean { + return form.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.BILL; + } + + setGeneralMappingsValidator(form: FormGroup): void { + if (this.showBankAccountField(form)) { + form.controls.bankAccount.setValidators(Validators.required); + } else { + form.controls.bankAccount.clearValidators(); + form.controls.bankAccount.updateValueAndValidity(); + } + + if (this.showCreditCardAccountField(form)) { + form.controls.defaultCCCAccount.setValidators(Validators.required); + } else { + form.controls.defaultCCCAccount.clearValidators(); + form.controls.defaultCCCAccount.updateValueAndValidity(); + } + + if (this.showDebitCardAccountField(form)) { + form.controls.defaultDebitCardAccount.setValidators(Validators.required); + } else { + form.controls.defaultDebitCardAccount.clearValidators(); + form.controls.defaultDebitCardAccount.updateValueAndValidity(); + + } + + if (this.showReimbursableAccountsPayableField(form) || this.showCCCAccountsPayableField(form)) { + form.controls.accountsPayable.setValidators(Validators.required); + } else { + form.controls.accountsPayable.clearValidators(); + form.controls.accountsPayable.updateValueAndValidity(); + } + + if (this.showDefaultCreditCardVendorField(form)) { + form.controls.defaultCreditCardVendor.setValidators(Validators.required); + } else { + form.controls.defaultCreditCardVendor.clearValidators(); + form.controls.defaultCreditCardVendor.updateValueAndValidity(); + } + + if (this.showExpenseAccountField(form)) { + form.controls.qboExpenseAccount.setValidators(Validators.required); + } else { + form.controls.qboExpenseAccount.clearValidators(); + form.controls.qboExpenseAccount.updateValueAndValidity(); + } + } } + diff --git a/src/app/core/services/configuration/import-setting.service.spec.ts b/src/app/core/services/configuration/import-setting.service.spec.ts index e3b04e6b..39501594 100644 --- a/src/app/core/services/configuration/import-setting.service.spec.ts +++ b/src/app/core/services/configuration/import-setting.service.spec.ts @@ -4,6 +4,11 @@ import { ImportSettingPost, ImportSettingModel } from '../../models/configuratio import { MappingSourceField, MappingDestinationField } from '../../models/enum/enum.model'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; import { environment } from 'src/environments/environment'; +import { MatLegacyDialog as MatDialog, MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; + +import { FormBuilder } from '@angular/forms'; +import { of } from 'rxjs'; +import { mockPatchExpenseFieldsFormArray } from 'src/app/shared/components/configuration/import-settings/import-settings.fixture'; describe('ImportSettingService', () => { let service: ImportSettingService; @@ -11,17 +16,82 @@ describe('ImportSettingService', () => { let httpMock: HttpTestingController; const API_BASE_URL = environment.api_url; const workspace_id = environment.tests.workspaceId; + let formbuilder: FormBuilder; + let dialogSpy: jasmine.Spy; + const dialogRefSpyObj = jasmine.createSpyObj({ afterClosed: of({source_field: MappingDestinationField.TAX_CODE, + destination_field: MappingDestinationField.CLASS, + import_to_fyle: true, + name: MappingDestinationField.TAX_CODE, + disable_import_to_fyle: true, + source_placeholder: 'close'}), close: null }); + dialogRefSpyObj.componentInstance = { body: '' }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, MatDialogModule], providers: [ImportSettingService] }); + dialogSpy = spyOn(TestBed.get(MatDialog), 'open').and.returnValue(dialogRefSpyObj); injector = getTestBed(); + formbuilder = TestBed.inject(FormBuilder); service = injector.inject(ImportSettingService); httpMock = injector.inject(HttpTestingController); + + }); + + it('getQboExpenseFields function check', () => { + const qboAttributes = ['CUSTOMER']; + const mappingSettings = [ + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "is_custom": false, + "source_placeholder": null + } + ]; + const fyleFields = ['COST_CENTER', 'PROJECT']; + expect((service as any).getQboExpenseFields(qboAttributes, mappingSettings, true, fyleFields)); }); + it('getExpenseFieldsFormArray function check', () => { + const mappingSettings = [ + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "is_custom": false, + "source_placeholder": null + } + ]; + expect((service as any).getExpenseFieldsFormArray(mappingSettings, false)); + }); + + it('getPatchExpenseFieldValues function check', () => { + const mappingSettings = + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "disable_import_to_fyle": true, + "source_placeholder": '', + "addSourceField": false + }; + expect((service as any).getPatchExpenseFieldValues("CUSTOMER", "COST_CENTER")).toEqual(mappingSettings); + }); + + it('importToggleWatcher function check', () => { + const form = formbuilder.group({ + source_field: [MappingSourceField.PROJECT], + destination_field: [MappingDestinationField.CUSTOMER], + disable_import_to_fyle: [false], + import_to_fyle: [false, (service as any).importToggleWatcher()], + source_placeholder: [''] + }); + + form.controls.import_to_fyle.patchValue(true); + expect((service as any).importToggleWatcher()); + }); it('should be created', () => { expect(service).toBeTruthy(); @@ -91,4 +161,18 @@ describe('ImportSettingService', () => { }); req.flush(response); }); + + it('createExpenceField function check', () => { + const mappingSettings = [ + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "is_custom": false, + "source_placeholder": null + } + ]; + expect((service as any).createExpenseField(MappingDestinationField.CLASS, mappingSettings)).toBeUndefined(); + expect(dialogSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/core/services/configuration/import-setting.service.ts b/src/app/core/services/configuration/import-setting.service.ts index 63397c16..c4cb7766 100644 --- a/src/app/core/services/configuration/import-setting.service.ts +++ b/src/app/core/services/configuration/import-setting.service.ts @@ -1,23 +1,115 @@ -import { Injectable } from '@angular/core'; -import { ImportSettingPost } from '../../models/configuration/import-setting.model'; +import { EventEmitter, Injectable, Output } from '@angular/core'; +import { ExpenseFieldsFormOption, ImportSettingPost } from '../../models/configuration/import-setting.model'; import { ApiService } from '../core/api.service'; import { WorkspaceService } from '../workspace/workspace.service'; +import { ExpenseField, ExpenseFieldFormArray } from '../../models/misc/expense-field.model'; +import { MappingSetting } from '../../models/db/mapping-setting.model'; +import { ExpenseFieldCreationDialogComponent } from 'src/app/shared/components/configuration/import-settings/expense-field-creation-dialog/expense-field-creation-dialog.component'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { RxwebValidators } from '@rxweb/reactive-form-validators'; @Injectable({ providedIn: 'root' }) export class ImportSettingService { + workspaceId = this.workspaceService.getWorkspaceId() + + @Output() patchExpenseFieldEmitter: EventEmitter = new EventEmitter(); constructor( private apiService: ApiService, - private workspaceService: WorkspaceService + private workspaceService: WorkspaceService, + private formBuilder: FormBuilder, + private dialog: MatDialog ) { } getImportSettings() { - return this.apiService.get(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/import_settings/`, {}); + return this.apiService.get(`/v2/workspaces/${this.workspaceId}/import_settings/`, {}); } postImportSettings(exportSettingsPayload: ImportSettingPost){ - return this.apiService.put(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/import_settings/`, exportSettingsPayload); + return this.apiService.put(`/v2/workspaces/${this.workspaceId}/import_settings/`, exportSettingsPayload); + } + + getQboExpenseFields(qboAttributes: ExpenseField[], mappingSettings: MappingSetting[], isCloneSettings: boolean = false, fyleFields: string[] = []): ExpenseFieldsFormOption[] { + return qboAttributes.map(attribute => { + const mappingSetting = mappingSettings.filter((mappingSetting: MappingSetting) => { + if (mappingSetting.destination_field.toUpperCase() === attribute.attribute_type) { + if (isCloneSettings) { + return fyleFields.includes(mappingSetting.source_field.toUpperCase()) ? mappingSetting : false; + } + + return mappingSetting; + } + return false; + }); + return { + source_field: mappingSetting.length > 0 ? mappingSetting[0].source_field : '', + destination_field: attribute.display_name, + import_to_fyle: mappingSetting.length > 0 ? mappingSetting[0].import_to_fyle : false, + disable_import_to_fyle: false, + source_placeholder: '' + }; + }); + } + + private importToggleWatcher(): ValidatorFn { + return (control: AbstractControl): {[key: string]: object} | null => { + if (control.value) { + // Mark Fyle field as mandatory if toggle is enabled + control.parent?.get('source_field')?.setValidators(Validators.required); + control.parent?.get('source_field')?.setValidators(RxwebValidators.unique()); + } else { + // Reset Fyle field if toggle is disabled + control.parent?.get('source_field')?.clearValidators(); + control.parent?.get('source_field')?.setValue(null); + } + + return null; + }; + } + + getExpenseFieldsFormArray(qboExpenseField: ExpenseFieldsFormOption[], isWatcherRequired: boolean): FormGroup[] { + return qboExpenseField.map((field) => { + return this.formBuilder.group({ + source_field: [field.source_field, Validators.required], + destination_field: [field.destination_field.toUpperCase()], + disable_import_to_fyle: [field.disable_import_to_fyle], + import_to_fyle: [field.import_to_fyle, isWatcherRequired ? this.importToggleWatcher() : ''], + source_placeholder: [''] + }); + }); + } + + private getPatchExpenseFieldValues(destinationType: string, sourceField: string = '', source_placeholder: string = '', addSourceField: boolean = false): ExpenseFieldFormArray { + return { + source_field: sourceField, + destination_field: destinationType, + import_to_fyle: sourceField ? true : false, + disable_import_to_fyle: sourceField ? true : false, + source_placeholder: source_placeholder, + addSourceField: addSourceField + }; + } + + createExpenseField(destinationType: string, mappingSettings: MappingSetting[]): void { + const existingFields = mappingSettings.map(setting => setting.source_field.split('_').join(' ')); + const dialogRef = this.dialog.open(ExpenseFieldCreationDialogComponent, { + width: '551px', + data: existingFields + }); + + const expenseFieldValue = this.getPatchExpenseFieldValues(destinationType); + this.patchExpenseFieldEmitter.emit(expenseFieldValue); + + dialogRef.afterClosed().subscribe((expenseField) => { + if (expenseField) { + const sourceType = expenseField.name.split(' ').join('_').toUpperCase(); + + const expenseFieldValue = this.getPatchExpenseFieldValues(destinationType, sourceType, expenseField.source_placeholder, true); + this.patchExpenseFieldEmitter.emit(expenseFieldValue); + } + }); } } diff --git a/src/app/core/services/core/helper.service.spec.ts b/src/app/core/services/core/helper.service.spec.ts index 29e84253..4b3d32fa 100644 --- a/src/app/core/services/core/helper.service.spec.ts +++ b/src/app/core/services/core/helper.service.spec.ts @@ -1,14 +1,23 @@ import { TestBed } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { FormControl, FormGroup } from '@angular/forms'; import { DefaultDestinationAttribute } from '../../models/db/general-mapping.model'; import { HelperService } from './helper.service'; +import { Router } from '@angular/router'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; describe('HelperService', () => { let service: HelperService; + const routerSpy = { navigate: jasmine.createSpy('navigate'), url: '/path' }; + let router: Router; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: routerSpy } + ], + imports: [MatDialogModule] + }); service = TestBed.inject(HelperService); }); @@ -17,8 +26,8 @@ describe('HelperService', () => { }); it('clearSearchText service check', () => { - const form=new UntypedFormGroup({ - searchOption: new UntypedFormControl('fyle') + const form=new FormGroup({ + searchOption: new FormControl('fyle') }); service.clearSearchText(form); expect(form.controls.searchOption.value).toBeNull(); diff --git a/src/app/core/services/core/helper.service.ts b/src/app/core/services/core/helper.service.ts index b976cf03..df36682d 100644 --- a/src/app/core/services/core/helper.service.ts +++ b/src/app/core/services/core/helper.service.ts @@ -4,6 +4,10 @@ import { UntypedFormGroup } from '@angular/forms'; import { SnakeCaseToSpaceCase } from 'src/app/shared/pipes/snake-case-to-space-case.pipe'; import { DefaultDestinationAttribute } from '../../models/db/general-mapping.model'; import { WindowService } from './window.service'; +import { ConfirmationDialog } from '../../models/misc/confirmation-dialog.model'; +import { ConfirmationDialogComponent } from 'src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component'; +import { Router } from '@angular/router'; +import { MatLegacyDialog as MatDialog} from '@angular/material/legacy-dialog'; @Injectable({ providedIn: 'root' @@ -12,7 +16,11 @@ export class HelperService { private windowReference: Window; - constructor(private windowService: WindowService) { + constructor( + private windowService: WindowService, + private dialog: MatDialog, + private router: Router + ) { this.windowReference = this.windowService.nativeWindow; } @@ -37,4 +45,17 @@ export class HelperService { getSpaceCasedTitleCase(word: string): string { return new SnakeCaseToSpaceCase().transform((new TitleCasePipe().transform(word))); } + + openDialogAndSetupRedirection(data: ConfirmationDialog, url: string): void { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + width: '551px', + data: data + }); + + dialogRef.afterClosed().subscribe((ctaClicked: boolean) => { + if (ctaClicked) { + this.router.navigate([url]); + } + }); + } } diff --git a/src/app/core/services/integration/tracking.service.ts b/src/app/core/services/integration/tracking.service.ts index a118a138..04c683de 100644 --- a/src/app/core/services/integration/tracking.service.ts +++ b/src/app/core/services/integration/tracking.service.ts @@ -136,4 +136,8 @@ export class TrackingService { onMappingsAlphabeticalFilter(properties: MappingAlphabeticalFilterAdditionalProperty): void { this.eventTrack('Mappings Alphabetical Filter', properties); } + + onCloneSettingsSave(properties: Partial): void { + this.eventTrack('Clone Settings Saved', properties); + } } diff --git a/src/app/core/services/misc/mapping.service.ts b/src/app/core/services/misc/mapping.service.ts index b4f28fa6..04cc3c12 100644 --- a/src/app/core/services/misc/mapping.service.ts +++ b/src/app/core/services/misc/mapping.service.ts @@ -215,5 +215,4 @@ export class MappingService { } return undefined; } - } diff --git a/src/app/integration/integration.module.ts b/src/app/integration/integration.module.ts index d760d555..9127d339 100644 --- a/src/app/integration/integration.module.ts +++ b/src/app/integration/integration.module.ts @@ -5,7 +5,6 @@ import { IntegrationRoutingModule } from './integration-routing.module'; import { SharedModule } from '../shared/shared.module'; - @NgModule({ declarations: [ IntegrationComponent diff --git a/src/app/integration/main/mapping/employee-mapping/employee-mapping.component.spec.ts b/src/app/integration/main/mapping/employee-mapping/employee-mapping.component.spec.ts index 8c40d6d4..8d6105ba 100644 --- a/src/app/integration/main/mapping/employee-mapping/employee-mapping.component.spec.ts +++ b/src/app/integration/main/mapping/employee-mapping/employee-mapping.component.spec.ts @@ -15,6 +15,7 @@ import { mappingList } from 'src/app/shared/components/mapping/mapping-table/map import { environment } from 'src/environments/environment'; import { EmployeeMappingComponent } from './employee-mapping.component'; import { employeeMappingResponse, getEmployeeMappingResponse, getEmployeeMappingResponse1, mappinglist, MappingStatsResponse, qboData, qboData2, workspaceGeneralSettingResponse } from './employee-mapping.fixture'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; describe('EmployeeMappingComponent', () => { let component: EmployeeMappingComponent; @@ -44,7 +45,7 @@ describe('EmployeeMappingComponent', () => { getPageSize: () => 10 }; await TestBed.configureTestingModule({ - imports: [ FormsModule, ReactiveFormsModule, HttpClientModule, MatSnackBarModule, HttpClientTestingModule, NoopAnimationsModule ], + imports: [ FormsModule, ReactiveFormsModule, HttpClientModule, MatSnackBarModule, MatDialogModule, HttpClientTestingModule, NoopAnimationsModule ], declarations: [ EmployeeMappingComponent ], providers: [ { provide: WorkspaceService, useValue: service1 }, diff --git a/src/app/integration/onboarding/clone-settings/clone-settings.component.html b/src/app/integration/onboarding/clone-settings/clone-settings.component.html new file mode 100644 index 00000000..3b2ee8c0 --- /dev/null +++ b/src/app/integration/onboarding/clone-settings/clone-settings.component.html @@ -0,0 +1,962 @@ + + +
+
+ + +
+ +
+
+
+ +
+
+

Employee Mappings

+
+ In this section, you can configure how to map Fyle Employees to QuickBooks Online Vendor / + Employee +
+
+
+ +
+
+
+ +

Employee Representation

+ +
+ +
+ +
+
+
+ +
+
+
+ +

Employee Mapping

+ +
+ +
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+

Export Settings

+
In this section, you can configure how + and when expenses from Fyle need to be exported to QuickBooks Online
+
+
+
+
+
+

Reimbursable Expense

+ +
+ +
+ + +
+
+
+ +
+
+
+
+ +

State of Export

+ +
+ + +
+
+ +
+
+
+ +

Mode of Export

+ +
+
+ + +
+
+
+ +
+
+
+ +

{{ "To which Bank Account should the " + + getExportType(cloneSettingsForm.value.reimbursableExportType) + " be posted to?"}} +

+ +
+ + +
+
+
+ +

{{"To which Expense Account should the " + + getExportType(cloneSettingsForm.value.reimbursableExportType) + " be posted to?"}} +

+ +
+ + +
+
+
+ +

To which Accounts Payable account should the + Bill be posted to?

+ +
+ + +
+
+ +
+
+
+ +

{{"Set the " + + getExportType(cloneSettingsForm.value.reimbursableExportType) + " date as"}}

+ +
+ + + +
+
+ +
+
+
+ +

+ {{generateGroupingLabel(exportSource.REIMBURSABLE)}}

+ +
+ + +
+
+
+
+
+
+

Corporate Card Expense

+ +
+ +
+ +
+
+
+
+
+
+
+ +

State of Export

+ +
+ + +
+
+ +
+
+
+ +

Mode of Export

+ +
+
+ + +
+
+
+ +
+
+
+ +

Set Default Credit Card Account as

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+
+
+ +

Select Accounts Payable Account

+ +
+ + + +
+
+
+ +
+
+
+ +

Name in Journal Entry (CCC)

+ +
+ + +
+
+ +
+
+
+ +

+ {{generateGroupingLabel(exportSource.CREDIT_CARD)}}

+ +
+ + + +
+
+ +
+
+
+ +

{{"Set the " + + getExportType(cloneSettingsForm.value.creditCardExportType) + " date as"}}

+ +
+ + + +
+
+
+
+ + +
+
+
+ +
+
+

Import Settings

+
In this section, you can configure how + the dimensions you import from QuickBooks Online should be mapped in Fyle
+
+
+ +
+
+
+ QuickBooks Online Field +
+
+ Fyle Field +
+
+
+ +
+
+
+
+

+ Chart of Accounts +

+
+ + + +
+

+ Category +

+
+
+ View Accounts + +
+ +
+ + {{ chartOfAccountType.value.name }} + +
+
+
+
+ +
+
+ +
+
+
+

+ Products / Service +

+
+ + +
+

+ Category +

+
+
+
+ +
+
+ +
+
+
+

+ Vendors +

+
+ +
+

+ Merchant +

+
+
+
+ +
+
+ +
+
+
+
+
+

+ {{ expenseField.value.destination_field | titlecase }} +

+
+ + + + + + + {{ fyleExpenseField.split('_').join(' ') | titlecase }} + + + +
+ + +
+
+
+ +

or

+

+ Create a new field in Fyle +

+
+
+ +
+ +
+
+
+ +
+
+
+

+ Tax Code +

+
+ + + +
+

+ Tax Group +

+
+ + + + +
+ +

+ {{ option.value }}

+

{{ option.value }}

+ +
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+

Add Fields

+ +
+ + + + + + + +
+
+ + +
+
+
+ +
+
+

Advanced Settings

+
+ This section contains settings to automate and customize your expense exports +
+
+
+ +
+
+
+
+ +

Set description field in QuickBooks Online +

+ +
+ +
+ + +
+ + {{ option | titlecase | snakeCaseToSpaceCase }} + +
+
+
+
+
+
+
+
+ Preview in QuickBooks Online +
+
+
+ {{ memoPreviewText }} +
+
+
+
+ +
+
+
+ +

Auto - Sync payment between Fyle and + QuickBooks Online +

+ +
+ +
+ +
+
+
+ +
+
+
+

+ To which Payment account should the payment entries be posted? + +

+
+ +
+ +
+
+
+ +
+
+
+ +

Schedule automatic export

+ +
+ +
+ +
+
+
+ +
+
+
+

+ Set the frequency of export + +

+
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+ +
+ or + + Add new email address +
+
+
+
+ +
+
+
+ +

Auto Create Employee as Vendor

+ +
+ +
+ +
+
+
+ +
+
+
+ +

Auto-Create Merchants as Vendor

+ +
+ +
+ +
+
+
+ +
+
+
+ +

Create a single itemized offset credit entry + for Journal

+ +
+ +
+ + +
+
+
+ +
+
+
+ +

Post entries in the next open accounting + period

+ +
+ +
+ +
+
+
+
+
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/src/app/integration/onboarding/clone-settings/clone-settings.component.scss b/src/app/integration/onboarding/clone-settings/clone-settings.component.scss new file mode 100644 index 00000000..44ef2bf2 --- /dev/null +++ b/src/app/integration/onboarding/clone-settings/clone-settings.component.scss @@ -0,0 +1,222 @@ +.clone-settings { + &--dropdown-section { + padding-top: 30px; + } + + &--section { + display: flex; + justify-content: center; + } + + &--block { + margin-top: -140px; + margin-bottom: 58px; + width: 1098px; + background: #FFFFFF; + border: 1px solid #F5F5F5; + box-sizing: border-box; + box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); + border-radius: 8px; + } + + &--export-dropdown { + margin-top: 50px; + } + + &--header-section { + padding-top: 24px; + background: #FAFCFF; + border: 1px solid #F5F5F5; + } + + &--header-icon { + padding: 0px 10px 0px 32px; + } + + &--header-caption { + padding-top: 10px; + padding-bottom: 24px; + } + + &--configuration-section { + padding: 24px 32px; + } + + &--export-section { + padding: 0px 32px; + margin: -10px 0px -10px 0px; + } + + &--info-icon { + padding: 4px 0px 0px 10px; + } + + &--sub-options-icon { + padding-right: 12px; + } + + &--field-section { + width: 500px; + } + + &--sub-options-text { + color: #2C304E; + font-size: 14px; + } + + &--export-type { + padding-left: 10px; + } + + &--export-type-text { + font-weight: 600; + } + + &--field-header-section { + padding: 8px 0px 0px 32px; + } + + &--field-header { + height: 48px; + font-size: 14px; + } + + &--qbo-header { + padding-left: 13px; + padding-right: 202px; + } + + &--coa-import-section { + padding: 20px 0px 12px 32px; + } + + &--qbo-field { + height: 34px; + width: 304px; + background: #F5F5F5; + border: 1px solid #DFDFE2; + border-radius: 4px; + font-size: 14px; + color: #2C304E; + } + + &--qbo-field-text { + padding-left: 14px; + } + + &--back-btn { + margin-right: 12px; + } + + &--error-message { + padding-top: 10px; + margin-left: -22px; + } + + &--reset-btn { + width: 186px; + } + + &--footer-inner-section { + height: 84px; + padding: 12px 32px; + } + + &--coa-list { + padding-left: 12px; + font-size: 14px; + } + + &--cta-text { + color: #FF3366; + } + + &--coa-list-icon { + padding-left: 15px; + } + + &--delete-icon { + padding-right: 34px; + } + + &--tax-group-field { + margin-right: 12px; + } + + &--header-advanced-setting { + padding-top: 24px; + } + + &--configuration-payment-section { + padding: 0px 32px 24px; + } + + &--configuration-payment-field { + padding-left: 28px; + } + + &--add-email-text { + padding-right: 210px; + padding-top: 18px; + } + + &--add-btn { + margin-bottom: 8px; + height: 16px; + width: 16px; + } + + &--span-or { + margin: 1px 15px 8px 5px ; + font-weight: 500; + font-size: 14px; + color: #2c304e; + } + + &--additional-email-text { + margin-top: -40px; + } + + &--add-field-section { + width: 124px; + } + + &--add-field { + padding-right: 12px; + font-size: 14px; + } + + &--memo-preview-section { + padding-top: 24px; + } + + &--memo-preview-text { + font-weight: 500; + } + + &--memo-preview-area { + padding: 12px 12px 0px 0px; + } + + &--memo-preview { + padding: 8px 0px 8px 8px; + background: #F5F5F5; + border-radius: 4px; + display: flex; + align-items: center; + } + + &--memo-preview-select { + ::ng-deep .mat-select-panel-wrap { + // TODO: check this + margin-left: 7% !important; + flex-basis: 96% !important; + } + + ::ng-deep .cdk-overlay-pane { + // TODO: check this + margin-left: 1.5% !important; + } + } +} + diff --git a/src/app/integration/onboarding/clone-settings/clone-settings.component.spec.ts b/src/app/integration/onboarding/clone-settings/clone-settings.component.spec.ts new file mode 100644 index 00000000..9072a7c3 --- /dev/null +++ b/src/app/integration/onboarding/clone-settings/clone-settings.component.spec.ts @@ -0,0 +1,428 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { CloneSettingsComponent } from './clone-settings.component'; +import { HttpClientModule } from '@angular/common/http'; +import { FormBuilder, UntypedFormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { mockCloneSettingExist, mockCloneSettingsGet, mockGroupedDestinationAttribtues } from './clone-settings.fixture'; +import { CloneSettingService } from 'src/app/core/services/configuration/clone-setting.service'; +import { MappingService } from 'src/app/core/services/misc/mapping.service'; +import { chartOfAccountTypesList, expenseFieldresponse, mockExpenseFieldsFormArray, mockPatchExpenseFieldsFormArray, qboField } from 'src/app/shared/components/configuration/import-settings/import-settings.fixture'; +import { getMappingSettingResponse } from 'src/app/shared/components/mapping/generic-mapping/generic-mapping.fixture'; +import { AdvancedSettingService } from 'src/app/core/services/configuration/advanced-setting.service'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatLegacyDialogModule as MatDialogModule, MatLegacyDialog } from '@angular/material/legacy-dialog'; +import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; +import { CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExportDateType, ExportSource, MappingDestinationField, MappingSourceField, ReimbursableExpensesObject } from 'src/app/core/models/enum/enum.model'; +import { ExportSettingService } from 'src/app/core/services/configuration/export-setting.service'; +import { ImportSettingService } from 'src/app/core/services/configuration/import-setting.service'; +import { mockNameInJournalEntry, mockReimbursableExpenseGroupingDateOptions, mockReimbursableExpenseGroupingFieldOptions, mockReimbursableExpenseStateOptions } from 'src/app/shared/components/configuration/export-settings/export-settings.fixture'; +import { adminEmails, destinationAttribute, memo, paymentSyncOptions, previewResponse } from 'src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture'; + + +describe('CloneSettingsComponent', () => { + let component: CloneSettingsComponent; + let fixture: ComponentFixture; + let router: Router; + let formbuilder: UntypedFormBuilder; + let exportSettingService: ExportSettingService; + let importSettingService: ImportSettingService; + let cloneSettingService: CloneSettingService; + const routerSpy = { navigate: jasmine.createSpy('navigate'), url: '/path' }; + let dialogSpy: jasmine.Spy; + const dialogRefSpyObj = jasmine.createSpyObj({ afterClosed: of({}), close: '' }); + dialogRefSpyObj.componentInstance = { body: '' }; + let service1: any; + let service2: any; + let service3: any; + let service4: any; + + beforeEach(async () => { + service1 = { + checkCloneSettingsExists: () => of(mockCloneSettingExist), + postCloneSettings: () => of(mockCloneSettingsGet), + getCloneSettings: () => of(mockCloneSettingsGet) + }; + service2 = { + getGroupedQBODestinationAttributes: () => of(mockGroupedDestinationAttribtues), + getFyleExpenseFields: () => of(expenseFieldresponse), + getMappingSettings: () => of(getMappingSettingResponse), + getQBODestinationAttributes: () => null, + getExpenseFieldsFormArray: () => mockExpenseFieldsFormArray + }; + service3 = { + getPaymentSyncOptions: () => of(paymentSyncOptions), + getFrequencyIntervals: () => null, + getWorkspaceAdmins: () => null, + openAddemailDialog: () => null, + patchAdminEmailsEmitter: of(mockPatchExpenseFieldsFormArray) + }; + service4 = { + exportSelectionValidator: () => undefined, + createCreditCardExpenseWatcher: () => undefined, + createReimbursableExpenseWatcher: () => undefined, + getReimbursableExpenseGroupingFieldOptions: () => mockReimbursableExpenseGroupingFieldOptions, + getReimbursableExpenseGroupingDateOptions: () => mockReimbursableExpenseGroupingDateOptions, + getcreditCardExportTypes: () => undefined, + getReimbursableExportTypeOptions: () => undefined, + getCCCExpenseStateOptions: () => undefined, + getExportGroup: () => undefined, + getReimbursableExpenseStateOptions: () => mockReimbursableExpenseStateOptions, + setGeneralMappingsValidator: () => undefined, + nameInJournalOptions: () => mockNameInJournalEntry + }; + await TestBed.configureTestingModule({ + declarations: [ CloneSettingsComponent ], + imports: [ + HttpClientModule, MatDialogModule, MatSnackBarModule, MatMenuModule, NoopAnimationsModule + ], + providers: [ + FormBuilder, + { provide: Router, useValue: routerSpy }, + { provide: ExportSettingService, useValue: service4}, + { provide: CloneSettingService, useValue: service1 }, + { provide: MappingService, useValue: service2 }, + { provide: AdvancedSettingService, useValue: service3 }, + { provide: ExportSettingService, useValue: service4} + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CloneSettingsComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + formbuilder = TestBed.inject(UntypedFormBuilder); + dialogSpy = spyOn(TestBed.get(MatLegacyDialog), 'open').and.returnValue(dialogRefSpyObj); + exportSettingService = TestBed.inject(ExportSettingService); + importSettingService = TestBed.inject(ImportSettingService); + component.cloneSettings = mockCloneSettingsGet; + component.qboExpenseFields = qboField; + component.chartOfAccountTypesList = chartOfAccountTypesList; + + + component.cloneSettingsForm = formbuilder.group({ + employeeMapping: [component.cloneSettings.employee_mappings.workspace_general_settings?.employee_field_mapping, Validators.required], + autoMapEmployee: [component.cloneSettings.employee_mappings.workspace_general_settings?.auto_map_employees, Validators.nullValidator], + + // Export Settings + reimbursableExpense: [component.cloneSettings.export_settings.workspace_general_settings?.reimbursable_expenses_object ? true : false], + reimbursableExportDate: [component.cloneSettings.export_settings.expense_group_settings?.reimbursable_export_date_type], + expenseState: [component.cloneSettings.export_settings.expense_group_settings?.expense_state], + reimbursableExportGroup: [exportSettingService.getExportGroup(component.cloneSettings.export_settings.expense_group_settings?.reimbursable_expense_group_fields)], + reimbursableExportType: [component.cloneSettings.export_settings.workspace_general_settings?.reimbursable_expenses_object], + + creditCardExpense: [component.cloneSettings.export_settings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false], + creditCardExportDate: [component.cloneSettings.export_settings.expense_group_settings?.ccc_export_date_type], + cccExpenseState: [component.cloneSettings.export_settings.expense_group_settings?.ccc_expense_state], + creditCardExportGroup: [exportSettingService.getExportGroup(component.cloneSettings.export_settings.expense_group_settings?.corporate_credit_card_expense_group_fields)], + creditCardExportType: [component.cloneSettings.export_settings.workspace_general_settings?.corporate_credit_card_expenses_object], + + bankAccount: [component.cloneSettings.export_settings.general_mappings?.bank_account?.id ? component.cloneSettings.export_settings.general_mappings.bank_account : null], + qboExpenseAccount: [component.cloneSettings.export_settings.general_mappings?.qbo_expense_account?.id ? component.cloneSettings.export_settings.general_mappings.qbo_expense_account : null], + defaultCCCAccount: [component.cloneSettings.export_settings.general_mappings?.default_ccc_account?.id ? component.cloneSettings.export_settings.general_mappings.default_ccc_account : null], + accountsPayable: [component.cloneSettings.export_settings.general_mappings?.accounts_payable?.id ? component.cloneSettings.export_settings.general_mappings.accounts_payable : null], + defaultCreditCardVendor: [component.cloneSettings.export_settings.general_mappings?.default_ccc_vendor?.id ? component.cloneSettings.export_settings.general_mappings.default_ccc_vendor : null], + defaultDebitCardAccount: [component.cloneSettings.export_settings.general_mappings?.default_debit_card_account?.id ? component.cloneSettings.export_settings.general_mappings.default_debit_card_account : null], + searchOption: [], + + // Import Settings + chartOfAccount: [component.cloneSettings.import_settings.workspace_general_settings.import_categories], + importItems: [component.cloneSettings.import_settings.workspace_general_settings.import_items], + chartOfAccountTypes: formbuilder.array([]), + expenseFields: formbuilder.array([]), + taxCode: [component.cloneSettings.import_settings.workspace_general_settings.import_tax_codes], + defaultTaxCode: [component.cloneSettings.import_settings.general_mappings?.default_tax_code?.id ? component.cloneSettings.import_settings.general_mappings.default_tax_code : null], + importVendorsAsMerchants: [component.cloneSettings.import_settings.workspace_general_settings.import_vendors_as_merchants], + memoStructure: [component.cloneSettings.advanced_configurations.workspace_general_settings.memo_structure] + }); + + cloneSettingService = TestBed.inject(CloneSettingService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('getExportType function check', () => { + const response = ReimbursableExpensesObject.JOURNAL_ENTRY; + const output = response.toLowerCase().charAt(0).toUpperCase() + response.toLowerCase().slice(1); + expect(component.getExportType(ReimbursableExpensesObject.JOURNAL_ENTRY)).toEqual(output); + }); + + it('createCreditCardExportGroupWatcher function check', () => { + component.cloneSettingsForm.controls.creditCardExportGroup.patchValue(!component.cloneSettingsForm.controls.creditCardExportGroup.value); + expect((component as any).createCreditCardExportGroupWatcher()).toBeUndefined(); + component.cloneSettingsForm.controls.creditCardExpense.patchValue(!component.cloneSettingsForm.controls.creditCardExportGroup.value); + component.cccExpenseGroupingDateOptions = [{ + 'label': 'Posted Date', + 'value': ExportDateType.POSTED_AT + }, + { + 'label': 'Spend Date', + 'value': ExportDateType.SPENT_AT + }]; + + component.cloneSettingsForm.controls.creditCardExportGroup.patchValue(ExpenseGroupingFieldOption.EXPENSE_ID); + expect((component as any).createCreditCardExportGroupWatcher()).toBeUndefined(); + component.cloneSettingsForm.controls.creditCardExportGroup.patchValue(ExpenseGroupingFieldOption.CLAIM_NUMBER); + expect((component as any).createCreditCardExportGroupWatcher()).toBeUndefined(); + }); + + it('createReimbursableExportGroupWatcher function check', () => { + component.cloneSettingsForm.controls.reimbursableExportGroup.patchValue(ExpenseGroupingFieldOption.EXPENSE_ID); + expect((component as any).createReimbursableExportGroupWatcher()).toBeUndefined(); + component.cloneSettingsForm.controls.reimbursableExportGroup.patchValue(ExpenseGroupingFieldOption.SETTLEMENT_ID); + expect((component as any).createReimbursableExportGroupWatcher()).toBeUndefined(); + }); + + + it('showBankAccountField function check', () => { + component.employeeFieldMapping = EmployeeFieldMapping.EMPLOYEE; + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.BILL); + fixture.detectChanges(); + expect(component.showBankAccountField()).toBeTrue(); + }); + + it('showReimbursableAccountsPayableField function check', () => { + component.cloneSettingsForm.controls.employeeMapping.patchValue(EmployeeFieldMapping.VENDOR); + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.JOURNAL_ENTRY); + fixture.detectChanges(); + expect(component.showReimbursableAccountsPayableField()).toBeTrue(); + }); + + it('showCCCAccountsPayableField function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.BILL); + fixture.detectChanges(); + expect(component.showCCCAccountsPayableField()).toBeTrue(); + }); + + it('showDefaultCreditCardVendorField function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.BILL); + fixture.detectChanges(); + expect(component.showDefaultCreditCardVendorField()).toBeTrue(); + }); + + it('showExpenseAccountField function check', () => { + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.EXPENSE); + fixture.detectChanges(); + expect(component.showExpenseAccountField()).toBeTrue(); + }); + + it('showCreditCardAccountField function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.JOURNAL_ENTRY); + fixture.detectChanges(); + expect(component.showCreditCardAccountField()).toBeTrue(); + }); + + it('showDebitCardAccountField function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE); + fixture.detectChanges(); + expect(component.showDebitCardAccountField()).toBeTrue(); + }); + + it('showSingleCreditLineJEField function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.JOURNAL_ENTRY); + fixture.detectChanges(); + expect(component.showSingleCreditLineJEField()).toBeTrue(); + }); + + it('resetConfiguraions function check', () => { + expect((component as any).resetConfiguraions()).toBeUndefined(); + fixture.detectChanges(); + expect(dialogSpy).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/workspaces/onboarding/employee_settings']); + }); + + it('function check', () => { + expect((component as any).setupExportWatchers()).toBeUndefined(); + expect((component as any).setupExpenseFieldWatcher()).toBeUndefined(); + }); + + it('Save Function check', () => { + component.isSaveInProgress = false; + component.mappingSettings = []; + component.qboExpenseFields = [ + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "disable_import_to_fyle": false, + "source_placeholder": "" + } + ]; + component.chartOfAccountTypesList = [ + 'Expense', 'Other Expense', 'Fixed Asset', 'Cost of Goods Sold', 'Current Liability', 'Equity', + 'Other Current Asset', 'Other Current Liability', 'Long Term Liability', 'Current Asset', 'Income', 'Other Income' + ]; + spyOn(cloneSettingService, 'postCloneSettings').and.callThrough(); + expect(component.save()).toBeUndefined(); + fixture.detectChanges(); + expect(cloneSettingService.postCloneSettings).toHaveBeenCalled(); + }); + + it('navigateToPreviousStep Function check', () => { + component.navigateToPreviousStep(); + expect(routerSpy.navigate).toHaveBeenCalledWith(([`/workspaces/onboarding/qbo_connector`])); + }); + + it('enableTaxImport Function check', () => { + component.enableTaxImport(); + expect(component.cloneSettingsForm.controls.taxCode.value).toBeTrue(); + }); + + it('enableTaxImport Function check', () => { + component.enableTaxImport(); + expect(component.cloneSettingsForm.controls.taxCode.value).toBeTrue(); + }); + + it('enableVendorAsMerchantImport Function check', () => { + component.enableVendorAsMerchantImport(); + expect(component.cloneSettingsForm.controls.importVendorsAsMerchants.value).toBeTrue(); + }); + + it('disablVendorAsMerchantImport Function check', () => { + component.disablVendorAsMerchantImport(); + expect(component.cloneSettingsForm.controls.importVendorsAsMerchants.value).toBeFalse(); + }); + + it('disableImportCoa Function check', () => { + component.disableImportCoa(); + expect(component.cloneSettingsForm.controls.chartOfAccount.value).toBeFalse(); + }); + + it('restrictExpenseGroupSetting function check', () => { + expect((component as any).restrictExpenseGroupSetting('CREDIT CARD PURCHASE')).toBeUndefined(); + }); + + it('enableAccountImport function check', () => { + component.enableAccountImport(); + expect(component.cloneSettingsForm.controls.chartOfAccount.value).toBeTrue(); + }); + + it('setImportFields function check', () => { + component.enableAccountImport(); + expect(component.cloneSettingsForm.controls.chartOfAccount.value).toBeTrue(); + }); + + it('getQboExpenseFields function check', () => { + const qboAttributes = ['CUSTOMER']; + const mappingSettings = [ + { + "source_field": "COST_CENTER", + "destination_field": "CUSTOMER", + "import_to_fyle": true, + "is_custom": false, + "source_placeholder": null + } + ]; + const fyleFields = ['COST_CENTER', 'PROJECT']; + + expect((component as any).getQboExpenseFields(qboAttributes, mappingSettings, true, fyleFields)); + }); + + it('setImportFields function check', () => { + component.mappingSettings = []; + + const fyleFields = ['COST_CENTER', 'PROJECT']; + spyOn(importSettingService, 'getQboExpenseFields').and.returnValue([]); + expect((component as any).setImportFields(fyleFields)); + }); + + it('setupEmployeeMappingWatcher function check', () => { + component.cloneSettingsForm.controls.employeeMapping.patchValue(EmployeeFieldMapping.VENDOR); + expect((component as any).setupEmployeeMappingWatcher()).toBeUndefined(); + fixture.detectChanges(); + component.cloneSettingsForm.controls.employeeMapping.patchValue(EmployeeFieldMapping.EMPLOYEE); + expect((component as any).setupEmployeeMappingWatcher()).toBeUndefined(); + }); + + it('createReimbursableExportTypeWatcher function check', () => { + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.EXPENSE); + expect((component as any).createReimbursableExportTypeWatcher()).toBeUndefined(); + fixture.detectChanges(); + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.BILL); + expect((component as any).createReimbursableExportTypeWatcher()).toBeUndefined(); + }); + + it('createCreditCardExportTypeWatcher function check', () => { + component.cloneSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE); + expect((component as any).createCreditCardExportTypeWatcher()).toBeUndefined(); + fixture.detectChanges(); + component.cloneSettingsForm.controls.employeeMapping.patchValue(CorporateCreditCardExpensesObject.JOURNAL_ENTRY); + expect((component as any).createCreditCardExportTypeWatcher()).toBeUndefined(); + }); + + it('openAddemailDialog function check', () => { + expect(component.openAddemailDialog()).toBeUndefined(); + }); + + it('showAutoCreateVendorsField function check', () => { + expect(component.showAutoCreateVendorsField()).toBeFalse(); + }); + + it('showAutoCreateMerchantsAsVendorsField function check', () => { + expect(component.showAutoCreateMerchantsAsVendorsField()).toBeFalse(); + }); + + it('showPaymentSyncField function check', () => { + expect(component.showPaymentSyncField()).toBeTrue(); + }); + + it('showImportProducts function check', () => { + expect(component.showImportProducts()).toBeTrue(); + }); + + it('showImportVendors function check', () => { + expect(component.showImportVendors()).toBeTrue(); + }); + + it('formatememopreview function check', () => { + component.memoStructure = memo; + fixture.detectChanges(); + (component as any).formatMemoPreview(); + expect(component.memoPreviewText.length).toEqual(previewResponse.length); + }); + + it('generateGroupingLabel function check', () => { + component.cloneSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.EXPENSE); + fixture.detectChanges(); + expect(component.generateGroupingLabel(ExportSource.REIMBURSABLE)).toEqual('How should the expenses be grouped?'); + }); + + it('createMemoStructureWatcher function check', () => { + expect((component as any).createMemoStructureWatcher()).toBeUndefined(); + component.cloneSettingsForm.controls.memoStructure.patchValue(['Integration']); + }); + + it('setCustomValidatorsAndWatchers function check', () => { + expect((component as any).setCustomValidatorsAndWatchers()).toBeUndefined(); + expect((component as any).setupForm()).toBeUndefined(); + }); + + it('addExpenseField function check', () => { + const formOptions = { + source_field: 'SOURCE_FIELD', + destination_field: 'DESTINATION_FIELD', + import_to_fyle: true, + disable_import_to_fyle: false, + source_placeholder: 'placeholder' + }; + + const additionalQboExpenseFields = { + source_field: 'SOURCE_FIELD_DUPE', + destination_field: 'DESTINATION_FIELD_DUPE', + import_to_fyle: true, + disable_import_to_fyle: false, + source_placeholder: 'placeholder' + }; + + component.additionalQboExpenseFields = [additionalQboExpenseFields]; + expect((component as any).addExpenseField(formOptions)).toBeUndefined(); + }); +}); + + diff --git a/src/app/integration/onboarding/clone-settings/clone-settings.component.ts b/src/app/integration/onboarding/clone-settings/clone-settings.component.ts new file mode 100644 index 00000000..bc200565 --- /dev/null +++ b/src/app/integration/onboarding/clone-settings/clone-settings.component.ts @@ -0,0 +1,674 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, AbstractControl, UntypedFormGroup, FormArray } from '@angular/forms'; +import { forkJoin } from 'rxjs'; +import { CloneSetting, CloneSettingModel } from 'src/app/core/models/configuration/clone-setting.model'; +import { ExportSettingFormOption, NameInJournalEntryOptions } from 'src/app/core/models/configuration/export-setting.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { CloneSettingService } from 'src/app/core/services/configuration/clone-setting.service'; +import { ExportSettingService } from 'src/app/core/services/configuration/export-setting.service'; +import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; + +import { HelperService } from 'src/app/core/services/core/helper.service'; +import { EmployeeFieldMapping, ReimbursableExpensesObject, ClickEvent, OnboardingStep, ProgressPhase, ExpenseGroupingFieldOption, CorporateCreditCardExpensesObject, ExportDateType, MappingDestinationField, SimpleSearchType, SimpleSearchPage, PaymentSyncDirection, QBOField, AutoMapEmployee, ExportSource } from 'src/app/core/models/enum/enum.model'; +import { MappingService } from 'src/app/core/services/misc/mapping.service'; +import { TrackingService } from 'src/app/core/services/integration/tracking.service'; +import { ConfirmationDialog } from 'src/app/core/models/misc/confirmation-dialog.model'; +import { Router } from '@angular/router'; +import { MappingSetting } from 'src/app/core/models/db/mapping-setting.model'; +import { EmployeeSettingFormOption } from 'src/app/core/models/configuration/employee-setting.model'; +import { ExpenseFieldsFormOption } from 'src/app/core/models/configuration/import-setting.model'; +import { RxwebValidators } from '@rxweb/reactive-form-validators'; +import { ImportSettingService } from 'src/app/core/services/configuration/import-setting.service'; +import { ExpenseField } from 'src/app/core/models/misc/expense-field.model'; +import { AdvancedSettingService } from 'src/app/core/services/configuration/advanced-setting.service'; +import { AdvancedSettingFormOption } from 'src/app/core/models/configuration/advanced-setting.model'; +import { WorkspaceScheduleEmailOptions } from 'src/app/core/models/db/workspace-schedule.model'; +import { EmployeeSettingService } from 'src/app/core/services/configuration/employee-setting.service'; + + +@Component({ + selector: 'app-clone-settings', + templateUrl: './clone-settings.component.html', + styleUrls: ['./clone-settings.component.scss'] +}) +export class CloneSettingsComponent implements OnInit { + + isLoading: boolean = true; + + isSaveInProgress: boolean = false; + + fyleExpenseFields: string[]; + + cloneSettingsForm: FormGroup; + + autoMapEmployeeTypes: EmployeeSettingFormOption[] = this.employeeSettingService.getAutoMapEmployeeOptions(); + + reimbursableExportOptions: ExportSettingFormOption[]; + + reimbursableExpenseGroupingDateOptions: ExportSettingFormOption[] = this.exportSettingService.getReimbursableExpenseGroupingDateOptions(); + + employeeFieldMappingOptions: EmployeeSettingFormOption[] = this.employeeSettingService.getEmployeeFieldMappingOptions(); + + expenseGroupingFieldOptions: ExportSettingFormOption[] = this.exportSettingService.getReimbursableExpenseGroupingFieldOptions(); + + paymentSyncOptions: AdvancedSettingFormOption[] = this.advancedSettingService.getPaymentSyncOptions(); + + frequencyIntervals: AdvancedSettingFormOption[] = this.advancedSettingService.getFrequencyIntervals(); + + nameInJournalOptions: NameInJournalEntryOptions[] = this.exportSettingService.nameInJournalOptions(); + + adminEmails: WorkspaceScheduleEmailOptions[]; + + bankAccounts: DestinationAttribute[]; + + cccAccounts: DestinationAttribute[]; + + accountsPayables: DestinationAttribute[]; + + vendors: DestinationAttribute[]; + + employeeFieldMapping: EmployeeFieldMapping; + + expenseAccounts: DestinationAttribute[]; + + cloneSettings: CloneSetting; + + reimbursableExpenseStateOptions: ExportSettingFormOption[]; + + cccExpenseStateOptions: ExportSettingFormOption[]; + + cccExpenseExportOptions: ExportSettingFormOption[]; + + mappingSettings: MappingSetting[]; + + cccExpenseGroupingDateOptions: ExportSettingFormOption[]; + + ProgressPhase = ProgressPhase; + + qboExpenseFields: ExpenseFieldsFormOption[]; + + additionalQboExpenseFields: ExpenseFieldsFormOption[]; + + SimpleSearchPage = SimpleSearchPage; + + SimpleSearchType = SimpleSearchType; + + taxCodes: DestinationAttribute[]; + + autoCreateMerchantsAsVendors: boolean; + + exportSource = ExportSource; + + chartOfAccountTypesList: string[] = [ + 'Expense', 'Other Expense', 'Fixed Asset', 'Cost of Goods Sold', 'Current Liability', 'Equity', + 'Other Current Asset', 'Other Current Liability', 'Long Term Liability', 'Current Asset', 'Income', 'Other Income' + ]; + + defaultMemoFields: string[] = ['employee_email', 'merchant', 'purpose', 'category', 'spent_on', 'report_number', 'expense_link']; + + hoveredIndex: { + categoryImport: number, + itemsImport: number, + vendorsImport: number, + expenseFieldImport: number, + taxImport: number + } = { + categoryImport: -1, + itemsImport: -1, + vendorsImport: -1, + expenseFieldImport: -1, + taxImport: -1 + }; + + memoStructure: string[] = []; + + memoPreviewText: string = ''; + + showNameInJournalOption: boolean = false; + + constructor( + private advancedSettingService: AdvancedSettingService, + private exportSettingService: ExportSettingService, + private importSettingService: ImportSettingService, + private employeeSettingService: EmployeeSettingService, + public helperService: HelperService, + private formBuilder: FormBuilder, + private cloneSettingService: CloneSettingService, + private mappingService: MappingService, + private trackingService: TrackingService, + private snackBar: MatSnackBar, + private router: Router + ) { } + + resetConfiguraions(): void { + const data: ConfirmationDialog = { + title: 'Are you sure?', + contents: `By resetting the configuration, you will be configuring each setting individually from the beginning.

+ Would you like to continue?`, + primaryCtaText: 'Yes' + }; + this.trackingService.onClickEvent(ClickEvent.CLONE_SETTINGS_RESET, {page: OnboardingStep.CLONE_SETTINGS}); + + this.helperService.openDialogAndSetupRedirection(data, '/workspaces/onboarding/employee_settings'); + } + + navigateToPreviousStep(): void { + this.trackingService.onClickEvent(ClickEvent.CLONE_SETTINGS_BACK, {page: OnboardingStep.CLONE_SETTINGS}); + this.router.navigate([`/workspaces/onboarding/qbo_connector`]); + } + + private formatMemoPreview(): void { + const time = Date.now(); + const today = new Date(time); + + const previewValues: { [key: string]: string } = { + employee_email: 'john.doe@acme.com', + category: 'Meals and Entertainment', + purpose: 'Client Meeting', + merchant: 'Pizza Hut', + report_number: 'C/2021/12/R/1', + spent_on: today.toLocaleDateString(), + expense_link: 'https://app.fylehq.com/app/main/#/enterprise/view_expense/' + }; + + this.memoPreviewText = ''; + this.memoStructure.forEach((field, index) => { + if (field in previewValues) { + this.memoPreviewText += previewValues[field]; + if (index + 1 !== this.memoStructure.length) { + this.memoPreviewText = this.memoPreviewText + ' - '; + } + } + }); + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.defaultMemoFields, event.previousIndex, event.currentIndex); + const selectedMemoFields = this.defaultMemoFields.filter(memoOption => this.cloneSettingsForm.value.memoStructure.indexOf(memoOption) !== -1); + const memoStructure = selectedMemoFields ? selectedMemoFields : this.defaultMemoFields; + this.memoStructure = memoStructure; + this.formatMemoPreview(); + } + + save(): void { + if (this.cloneSettingsForm.valid) { + this.isSaveInProgress = true; + const customMappingSettings = this.mappingSettings.filter(setting => !setting.import_to_fyle); + const cloneSettingPayload = CloneSettingModel.constructPayload(this.cloneSettingsForm, customMappingSettings); + + this.cloneSettingService.postCloneSettings(cloneSettingPayload).subscribe((response) => { + this.isSaveInProgress = false; + this.snackBar.open('Cloned settings successfully'); + this.router.navigate([`/workspaces/onboarding/done`]); + }, () => { + this.isSaveInProgress = false; + this.snackBar.open('Failed to clone settings'); + }); + } + } + + getExportType(exportType: ReimbursableExpensesObject | CorporateCreditCardExpensesObject): string { + const lowerCaseWord = exportType.toLowerCase(); + return lowerCaseWord.charAt(0).toUpperCase() + lowerCaseWord.slice(1); + } + + generateGroupingLabel(exportSource: ExportSource): string { + let exportType: ReimbursableExpensesObject | CorporateCreditCardExpensesObject; + if (exportSource === ExportSource.REIMBURSABLE) { + exportType = this.cloneSettingsForm.value.reimbursableExportType; + } else { + exportType = this.cloneSettingsForm.value.creditCardExportType; + } + + if (exportType === ReimbursableExpensesObject.EXPENSE) { + return 'How should the expenses be grouped?'; + } + return `How should the expense in ${this.getExportType(exportType)} be grouped?`; + } + + private setCreditCardExpenseGroupingDateOptions(creditCardExportGroup: ExpenseGroupingFieldOption): void { + if (creditCardExportGroup === ExpenseGroupingFieldOption.EXPENSE_ID) { + this.cccExpenseGroupingDateOptions = this.reimbursableExpenseGroupingDateOptions.concat([{ + label: 'Posted Date', + value: ExportDateType.POSTED_AT + }]); + } else { + this.cccExpenseGroupingDateOptions = this.reimbursableExpenseGroupingDateOptions.concat(); + } + } + + showImportVendors(): boolean { + return !this.cloneSettingsForm.value.autoCreateMerchantsAsVendors; + } + + showImportProducts(): boolean { + return this.cloneSettingsForm.controls.reimbursableExportType.value !== 'JOURNAL_ENTRY' && this.cloneSettingsForm.controls.creditCardExportType.value !== 'JOURNAL ENTRY'; + } + + showExpenseAccountField(): boolean { + return this.cloneSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.EXPENSE; + } + + showBankAccountField(): boolean { + return this.cloneSettingsForm.value.employeeMapping === EmployeeFieldMapping.EMPLOYEE && this.cloneSettingsForm.controls.reimbursableExportType.value && this.cloneSettingsForm.controls.reimbursableExportType.value !== ReimbursableExpensesObject.EXPENSE; + } + + showReimbursableAccountsPayableField(): boolean { + return (this.cloneSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.BILL) || (this.cloneSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.JOURNAL_ENTRY && this.cloneSettingsForm.value.employeeMapping === EmployeeFieldMapping.VENDOR); + } + + showCreditCardAccountField(): boolean { + return this.cloneSettingsForm.controls.creditCardExportType.value && this.cloneSettingsForm.controls.creditCardExportType.value !== CorporateCreditCardExpensesObject.BILL && this.cloneSettingsForm.controls.creditCardExportType.value !== CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE; + } + + showDebitCardAccountField(): boolean { + return this.cloneSettingsForm.controls.creditCardExportType.value && this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE; + } + + showDefaultCreditCardVendorField(): boolean { + return this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.BILL; + } + + showCCCAccountsPayableField(): boolean { + return this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.BILL; + } + + showSingleCreditLineJEField(): boolean { + return this.cloneSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.JOURNAL_ENTRY || this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.JOURNAL_ENTRY; + } + + showAutoCreateVendorsField(): boolean { + return this.cloneSettingsForm.controls.employeeMapping.value === EmployeeFieldMapping.VENDOR && this.cloneSettingsForm.controls.autoMapEmployee.value !== null && this.cloneSettingsForm.controls.autoMapEmployee.value !== AutoMapEmployee.EMPLOYEE_CODE; + } + + showAutoCreateMerchantsAsVendorsField(): boolean { + return !this.cloneSettingsForm.controls.importVendorsAsMerchants.value && (this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE || this.cloneSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE); + } + + showPaymentSyncField(): boolean { + return this.cloneSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.BILL; + } + + private restrictExpenseGroupSetting(creditCardExportType: string | null) : void { + if (creditCardExportType === CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE || creditCardExportType === CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE) { + this.cloneSettingsForm.controls.creditCardExportGroup.setValue(ExpenseGroupingFieldOption.EXPENSE_ID); + this.cloneSettingsForm.controls.creditCardExportGroup.disable(); + + this.cccExpenseGroupingDateOptions = [{ + label: 'Posted Date', + value: ExportDateType.POSTED_AT + }, + { + label: 'Spend Date', + value: ExportDateType.SPENT_AT + }]; + } else { + this.cloneSettingsForm.controls.creditCardExportGroup.enable(); + this.setCreditCardExpenseGroupingDateOptions(this.cloneSettingsForm.controls.creditCardExportGroup.value); + } + } + + private createCreditCardExportTypeWatcher(): void { + this.restrictExpenseGroupSetting(this.cloneSettingsForm.controls.creditCardExpense.value); + this.cloneSettingsForm.controls.creditCardExportType.valueChanges.subscribe((creditCardExportType: string) => { + this.exportSettingService.setGeneralMappingsValidator(this.cloneSettingsForm); + this.restrictExpenseGroupSetting(creditCardExportType); + this.showNameInJournalOption = creditCardExportType === CorporateCreditCardExpensesObject.JOURNAL_ENTRY ? true : false; + }); + } + + private createReimbursableExportTypeWatcher(): void { + this.cloneSettingsForm.controls.reimbursableExportType.valueChanges.subscribe(() => { + this.exportSettingService.setGeneralMappingsValidator(this.cloneSettingsForm); + }); + } + + private createReimbursableExportGroupWatcher(): void { + this.cloneSettingsForm.controls.reimbursableExportGroup.valueChanges.subscribe((reimbursableExportGroup: ExpenseGroupingFieldOption) => { + if (reimbursableExportGroup === ExpenseGroupingFieldOption.EXPENSE_ID) { + this.reimbursableExpenseGroupingDateOptions.pop(); + } else { + if (this.reimbursableExpenseGroupingDateOptions.length !== 5) { + this.reimbursableExpenseGroupingDateOptions.push({ + label: 'Last Spend Date', + value: ExportDateType.LAST_SPENT_AT + }); + } + } + }); + } + + private createCreditCardExportGroupWatcher(): void { + this.cloneSettingsForm.controls.creditCardExportGroup.valueChanges.subscribe((creditCardExportGroup: ExpenseGroupingFieldOption) => { + if (creditCardExportGroup && creditCardExportGroup === ExpenseGroupingFieldOption.EXPENSE_ID) { + this.cccExpenseGroupingDateOptions = this.cccExpenseGroupingDateOptions.filter((option) => { + return option.value !== ExportDateType.LAST_SPENT_AT; + }); + this.setCreditCardExpenseGroupingDateOptions(creditCardExportGroup); + } else { + const lastSpentAt = this.cccExpenseGroupingDateOptions.filter((option) => { + return option.value === ExportDateType.LAST_SPENT_AT; + }); + if (!lastSpentAt.length) { + this.cccExpenseGroupingDateOptions.push({ + label: 'Last Spend Date', + value: ExportDateType.LAST_SPENT_AT + }); + } + this.setCreditCardExpenseGroupingDateOptions(creditCardExportGroup); + } + }); + } + + private setupExportWatchers(): void { + this.cloneSettingsForm?.controls.reimbursableExpense?.setValidators((this.exportSettingService.exportSelectionValidator(this.cloneSettingsForm, true))); + this.cloneSettingsForm?.controls.creditCardExpense?.setValidators(this.exportSettingService.exportSelectionValidator(this.cloneSettingsForm, true)); + } + + private setupExpenseFieldWatcher(): void { + this.importSettingService.patchExpenseFieldEmitter.subscribe((expenseField) => { + if (expenseField.addSourceField) { + this.fyleExpenseFields.push(expenseField.source_field); + } + this.expenseFields.controls.filter(field => field.value.destination_field === expenseField.destination_field)[0].patchValue(expenseField); + }); + } + + private setupAdditionalEmailsWatcher(): void { + this.advancedSettingService.patchAdminEmailsEmitter.subscribe((additionalEmails) => { + this.adminEmails = additionalEmails; + }); + } + + private setupEmployeeMappingWatcher(): void { + this.cloneSettingsForm.controls.employeeMapping.valueChanges.subscribe((employeeMapping: EmployeeFieldMapping) => { + this.reimbursableExportOptions = this.exportSettingService.getReimbursableExportTypeOptions(employeeMapping); + }); + } + + disableImportCoa(): void { + this.cloneSettingsForm.controls.chartOfAccount.setValue(false); + } + + disableImportItems(): void { + this.cloneSettingsForm.controls.importItems.setValue(false); + } + + enableItemsImport(): void { + this.cloneSettingsForm.controls.importItems.setValue(true); + } + + enableVendorAsMerchantImport(): void { + this.cloneSettingsForm.controls.importVendorsAsMerchants.setValue(true); + } + + disablVendorAsMerchantImport(): void { + this.cloneSettingsForm.controls.importVendorsAsMerchants.setValue(false); + } + + disableImportTax(): void { + this.cloneSettingsForm.controls.taxCode.setValue(false); + this.cloneSettingsForm.controls.defaultTaxCode.clearValidators(); + this.cloneSettingsForm.controls.defaultTaxCode.setValue(null); + } + + enableTaxImport(): void { + this.cloneSettingsForm.controls.taxCode.setValue(true); + this.cloneSettingsForm.controls.defaultTaxCode.setValidators(Validators.required); + } + + enableAccountImport(): void { + this.cloneSettingsForm.controls.chartOfAccount.setValue(true); + } + + private createMemoStructureWatcher(): void { + this.formatMemoPreview(); + this.cloneSettingsForm.controls.memoStructure.valueChanges.subscribe((memoChanges) => { + this.memoStructure = memoChanges; + this.formatMemoPreview(); + }); + } + + private setCustomValidatorsAndWatchers(): void { + + this.createMemoStructureWatcher(); + + this.exportSettingService.createReimbursableExpenseWatcher(this.cloneSettingsForm, this.cloneSettings.export_settings); + this.exportSettingService.createCreditCardExpenseWatcher(this.cloneSettingsForm, this.cloneSettings.export_settings); + this.exportSettingService.setGeneralMappingsValidator(this.cloneSettingsForm); + + // Export select fields + this.createReimbursableExportTypeWatcher(); + this.createCreditCardExportTypeWatcher(); + + // Grouping fields + this.createReimbursableExportGroupWatcher(); + this.createCreditCardExportGroupWatcher(); + + this.setupExportWatchers(); + this.setupEmployeeMappingWatcher(); + + this.setCreditCardExpenseGroupingDateOptions(this.cloneSettingsForm.controls.creditCardExportGroup.value); + this.setupExpenseFieldWatcher(); + + this.setupAdditionalEmailsWatcher(); + } + + createChartOfAccountField(type: string): UntypedFormGroup { + return this.formBuilder.group({ + enabled: [this.cloneSettings.import_settings.workspace_general_settings.charts_of_accounts.includes(type) || type === 'Expense' ? true : false], + name: [type] + }); + } + + openAddemailDialog(): void { + this.advancedSettingService.openAddemailDialog(this.cloneSettingsForm, this.adminEmails); + } + + get chartOfAccountTypes() { + return this.cloneSettingsForm.get('chartOfAccountTypes') as FormArray; + } + + get expenseFields() { + return this.cloneSettingsForm.get('expenseFields') as FormArray; + } + + createExpenseField(destinationType: string): void { + this.importSettingService.createExpenseField(destinationType, this.mappingSettings); + } + + addExpenseField(field: ExpenseFieldsFormOption): void { + this.expenseFields.push(this.formBuilder.group({ + source_field: [field.source_field, Validators.compose([RxwebValidators.unique(), Validators.required])], + destination_field: [field.destination_field.toUpperCase()], + disable_import_to_fyle: [field.disable_import_to_fyle], + import_to_fyle: [field.import_to_fyle], + source_placeholder: [''] + })); + this.expenseFields.markAllAsTouched(); + this.additionalQboExpenseFields = this.additionalQboExpenseFields.filter((expenseField) => expenseField.destination_field !== field.destination_field); + } + + deleteExpenseField(index: number, expenseField: AbstractControl): void { + this.expenseFields.removeAt(index); + const additionalField = { + source_field: '', + destination_field: expenseField.value.destination_field, + disable_import_to_fyle: false, + import_to_fyle: false, + source_placeholder: '' + }; + this.additionalQboExpenseFields.push(additionalField); + } + + + private setupForm(): void { + const chartOfAccountTypeFormArray = this.chartOfAccountTypesList.map((type) => this.createChartOfAccountField(type)); + + const expenseFieldsFormArray = this.importSettingService.getExpenseFieldsFormArray(this.qboExpenseFields, false); + + let paymentSync = ''; + if (this.cloneSettings.advanced_configurations.workspace_general_settings.sync_fyle_to_qbo_payments) { + paymentSync = PaymentSyncDirection.FYLE_TO_QBO; + } else if (this.cloneSettings.advanced_configurations.workspace_general_settings.sync_qbo_to_fyle_payments) { + paymentSync = PaymentSyncDirection.QBO_TO_FYLE; + } + + this.memoStructure = this.cloneSettings.advanced_configurations.workspace_general_settings.memo_structure; + + this.cloneSettingsForm = this.formBuilder.group({ + // Employee Mapping + employeeMapping: [this.cloneSettings.employee_mappings.workspace_general_settings?.employee_field_mapping, Validators.required], + autoMapEmployee: [this.cloneSettings.employee_mappings.workspace_general_settings?.auto_map_employees], + + // Export Settings + reimbursableExpense: [this.cloneSettings.export_settings.workspace_general_settings?.reimbursable_expenses_object ? true : false], + reimbursableExportDate: [this.cloneSettings.export_settings.expense_group_settings?.reimbursable_export_date_type], + expenseState: [this.cloneSettings.export_settings.expense_group_settings?.expense_state], + reimbursableExportGroup: [this.exportSettingService.getExportGroup(this.cloneSettings.export_settings.expense_group_settings?.reimbursable_expense_group_fields)], + reimbursableExportType: [this.cloneSettings.export_settings.workspace_general_settings?.reimbursable_expenses_object], + + creditCardExpense: [this.cloneSettings.export_settings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false], + creditCardExportDate: [this.cloneSettings.export_settings.expense_group_settings?.ccc_export_date_type], + cccExpenseState: [this.cloneSettings.export_settings.expense_group_settings?.ccc_expense_state], + creditCardExportGroup: [this.exportSettingService.getExportGroup(this.cloneSettings.export_settings.expense_group_settings?.corporate_credit_card_expense_group_fields)], + creditCardExportType: [this.cloneSettings.export_settings.workspace_general_settings?.corporate_credit_card_expenses_object], + nameInJournalEntry: [this.cloneSettings.export_settings.workspace_general_settings.name_in_journal_entry], + + bankAccount: [this.cloneSettings.export_settings.general_mappings?.bank_account?.id ? this.cloneSettings.export_settings.general_mappings.bank_account : null], + qboExpenseAccount: [this.cloneSettings.export_settings.general_mappings?.qbo_expense_account?.id ? this.cloneSettings.export_settings.general_mappings.qbo_expense_account : null], + defaultCCCAccount: [this.cloneSettings.export_settings.general_mappings?.default_ccc_account?.id ? this.cloneSettings.export_settings.general_mappings.default_ccc_account : null], + accountsPayable: [this.cloneSettings.export_settings.general_mappings?.accounts_payable?.id ? this.cloneSettings.export_settings.general_mappings.accounts_payable : null], + defaultCreditCardVendor: [this.cloneSettings.export_settings.general_mappings?.default_ccc_vendor?.id ? this.cloneSettings.export_settings.general_mappings.default_ccc_vendor : null], + defaultDebitCardAccount: [this.cloneSettings.export_settings.general_mappings?.default_debit_card_account?.id ? this.cloneSettings.export_settings.general_mappings.default_debit_card_account : null], + searchOption: [], + + // Import Settings + chartOfAccount: [this.cloneSettings.import_settings.workspace_general_settings.import_categories], + importItems: [this.cloneSettings.import_settings.workspace_general_settings.import_items], + chartOfAccountTypes: this.formBuilder.array(chartOfAccountTypeFormArray), + expenseFields: this.formBuilder.array(expenseFieldsFormArray), + taxCode: [this.cloneSettings.import_settings.workspace_general_settings.import_tax_codes], + defaultTaxCode: [this.cloneSettings.import_settings.general_mappings?.default_tax_code?.id ? this.cloneSettings.import_settings.general_mappings.default_tax_code : null], + importVendorsAsMerchants: [this.cloneSettings.import_settings.workspace_general_settings.import_vendors_as_merchants], + + // Advanced Settings + paymentSync: [paymentSync], + billPaymentAccount: [this.cloneSettings.advanced_configurations.general_mappings?.bill_payment_account], + exportSchedule: [this.cloneSettings.advanced_configurations.workspace_schedules?.enabled ? this.cloneSettings.advanced_configurations.workspace_schedules.interval_hours : false], + exportScheduleFrequency: [this.cloneSettings.advanced_configurations.workspace_schedules?.enabled ? this.cloneSettings.advanced_configurations.workspace_schedules.interval_hours : null], + emails: [this.cloneSettings.advanced_configurations.workspace_schedules?.emails_selected ? this.cloneSettings.advanced_configurations.workspace_schedules?.emails_selected : []], + addedEmail: [], + changeAccountingPeriod: [this.cloneSettings.advanced_configurations.workspace_general_settings.change_accounting_period], + autoCreateVendors: [this.cloneSettings.advanced_configurations.workspace_general_settings.auto_create_destination_entity], + autoCreateMerchantsAsVendors: [this.cloneSettings.advanced_configurations.workspace_general_settings.auto_create_merchants_as_vendors ? this.cloneSettings.advanced_configurations.workspace_general_settings.auto_create_merchants_as_vendors : false], + singleCreditLineJE: [this.cloneSettings.advanced_configurations.workspace_general_settings.je_single_credit_line], + memoStructure: [this.cloneSettings.advanced_configurations.workspace_general_settings.memo_structure] + + }); + + this.setCustomValidatorsAndWatchers(); + + this.cloneSettingsForm.markAllAsTouched(); + + this.isLoading = false; + } + + private getQboExpenseFields(qboAttributes: string[], mappingSettings: MappingSetting[], isCloneSettings: boolean = false, fyleFields: string[] = []): ExpenseFieldsFormOption[] { + return qboAttributes.map(attribute => { + const mappingSetting = mappingSettings.filter((mappingSetting: MappingSetting) => { + if (mappingSetting.destination_field.toUpperCase() === attribute) { + if (isCloneSettings) { + return fyleFields.includes(mappingSetting.source_field.toUpperCase()) ? mappingSetting : false; + } + return mappingSetting; + } + return false; + }); + + return { + source_field: mappingSetting.length > 0 ? mappingSetting[0].source_field : '', + destination_field: attribute, + import_to_fyle: mappingSetting.length > 0 ? mappingSetting[0].import_to_fyle : false, + disable_import_to_fyle: false, + source_placeholder: '' + }; + }); + } + + private setImportFields(fyleFields: ExpenseField[]): void { + this.fyleExpenseFields = fyleFields.map(field => field.attribute_type); + // Remove custom mapped Fyle options + const customMappedFyleFields = this.mappingSettings.filter(setting => !setting.import_to_fyle).map(setting => setting.source_field); + const customMappedQuickbooksFields = this.mappingSettings.filter(setting => !setting.import_to_fyle).map(setting => setting.destination_field); + const importedQboFields = this.cloneSettings.import_settings.mapping_settings.filter(setting => setting.import_to_fyle).map(setting => setting.destination_field); + + if (customMappedFyleFields.length) { + this.fyleExpenseFields = this.fyleExpenseFields.filter(field => !customMappedFyleFields.includes(field)); + } + + const qboFields = [ + {attribute_type: MappingDestinationField.CLASS, display_name: 'Class'}, + {attribute_type: MappingDestinationField.DEPARTMENT, display_name: 'Department'}, + {attribute_type: MappingDestinationField.CUSTOMER, display_name: 'Customer'} + ]; + + // Remove custom mapped Quickbooks fields + const qboAttributes = qboFields.filter( + field => !customMappedQuickbooksFields.includes(field.attribute_type) + ); + + this.qboExpenseFields = this.getQboExpenseFields(importedQboFields, this.cloneSettings.import_settings.mapping_settings, true, this.fyleExpenseFields); + const allExpenseFields = this.importSettingService.getQboExpenseFields(qboAttributes, this.cloneSettings.import_settings.mapping_settings, true, this.fyleExpenseFields); + + this.additionalQboExpenseFields = allExpenseFields.filter((field) => { + return !this.qboExpenseFields.some((qboField) => qboField.destination_field.toUpperCase() === field.destination_field.toUpperCase()); + }); + } + + + private setupPage(): void { + const destinationAttributes = ['BANK_ACCOUNT', 'CREDIT_CARD_ACCOUNT', 'ACCOUNTS_PAYABLE', 'VENDOR']; + + forkJoin([ + this.cloneSettingService.getCloneSettings(), + this.mappingService.getGroupedQBODestinationAttributes(destinationAttributes), + this.mappingService.getMappingSettings(), + this.mappingService.getFyleExpenseFields(), + this.advancedSettingService.getWorkspaceAdmins(), + this.mappingService.getQBODestinationAttributes(QBOField.TAX_CODE) + ]).subscribe(responses => { + this.cloneSettings = responses[0]; + this.fyleExpenseFields = responses[3].map(field => field.attribute_type); + + this.adminEmails = this.cloneSettings.advanced_configurations.workspace_schedules?.additional_email_options ? this.cloneSettings.advanced_configurations.workspace_schedules?.additional_email_options.concat(responses[4]) : responses[4]; + this.employeeFieldMapping = this.cloneSettings.employee_mappings.workspace_general_settings.employee_field_mapping; + this.mappingSettings = responses[2].results; + + this.autoCreateMerchantsAsVendors = responses[0].advanced_configurations.workspace_general_settings.auto_create_merchants_as_vendors; + this.bankAccounts = responses[1].BANK_ACCOUNT; + this.cccAccounts = responses[1].CREDIT_CARD_ACCOUNT; + this.accountsPayables = responses[1].ACCOUNTS_PAYABLE; + this.vendors = responses[1].VENDOR; + this.expenseAccounts = this.bankAccounts.concat(this.cccAccounts); + this.showNameInJournalOption = this.cloneSettings.export_settings.workspace_general_settings?.corporate_credit_card_expenses_object === CorporateCreditCardExpensesObject.JOURNAL_ENTRY ? true : false; + + this.reimbursableExportOptions = this.exportSettingService.getReimbursableExportTypeOptions(EmployeeFieldMapping.EMPLOYEE); + this.cccExpenseExportOptions = this.exportSettingService.getcreditCardExportTypes(); + + this.reimbursableExpenseStateOptions = this.exportSettingService.getReimbursableExpenseStateOptions(this.cloneSettings.export_settings.workspace_general_settings.is_simplify_report_closure_enabled); + this.cccExpenseStateOptions = this.exportSettingService.getCCCExpenseStateOptions(this.cloneSettings.export_settings.workspace_general_settings.is_simplify_report_closure_enabled); + this.reimbursableExportOptions = this.exportSettingService.getReimbursableExportTypeOptions(this.cloneSettings.employee_mappings.workspace_general_settings.employee_field_mapping); + + this.setImportFields(responses[3]); + this.taxCodes = responses[5]; + + this.setupForm(); + }); + } + + ngOnInit(): void { + this.setupPage(); + } +} diff --git a/src/app/integration/onboarding/clone-settings/clone-settings.fixture.ts b/src/app/integration/onboarding/clone-settings/clone-settings.fixture.ts new file mode 100644 index 00000000..f22471dc --- /dev/null +++ b/src/app/integration/onboarding/clone-settings/clone-settings.fixture.ts @@ -0,0 +1,153 @@ +import { CloneSetting, CloneSettingExist, CloneSettingPost } from "src/app/core/models/configuration/clone-setting.model"; +import { GroupedDestinationAttribute } from "src/app/core/models/db/destination-attribute.model"; +import {AutoMapEmployee, CCCExpenseState, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseState, ExportDateType, MappingDestinationField, MappingSourceField, NameInJournalEntry, ReimbursableExpensesObject } from "src/app/core/models/enum/enum.model"; + +export const mockCloneSettingExist: CloneSettingExist = { + is_available: true, + workspace_name: 'Fyle for Ashwin' +}; + +export const mockCloneSettingsGet: CloneSetting = { + workspace_id: 1, + export_settings: { + expense_group_settings: { + reimbursable_expense_group_fields: null, + corporate_credit_card_expense_group_fields: null, + expense_state: ExpenseState.PAID, + reimbursable_export_date_type: null, + ccc_expense_state: CCCExpenseState.PAID, + ccc_export_date_type: null + }, + workspace_general_settings: { + reimbursable_expenses_object: ReimbursableExpensesObject.BILL, + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + corporate_credit_card_expenses_object: null, + is_simplify_report_closure_enabled: false + }, + general_mappings: { + accounts_payable: { name: 'fyle', id: "1" }, + bank_account: { name: 'fyle', id: "1" }, + qbo_expense_account: { name: 'fyle', id: "1" }, + default_ccc_account: { name: 'fyle', id: "1" }, + default_ccc_vendor: { name: 'fyle', id: "1" }, + default_debit_card_account: { name: 'fyle', id: "1" } + }, + workspace_id: 1 + }, + import_settings: { + general_mappings: { + id: 1, + created_at: new Date(), + updated_at: new Date(), + workspace: 1, + accounts_payable: { name: 'fyle', id: "1" }, + bank_account: { name: 'fyle', id: "1" }, + qbo_expense_account: { name: 'fyle', id: "1" }, + default_ccc_account: { name: 'fyle', id: "1" }, + default_ccc_vendor: { name: 'fyle', id: "1" }, + default_debit_card_account: { name: 'fyle', id: "1" }, + default_tax_code: { name: 'fyle', id: "1" }, + bill_payment_account: { name: 'fyle', id: "1" } + }, + mapping_settings: [{ + id: 1, + created_at: new Date(), + updated_at: new Date(), + workspace: 1, + source_field: MappingSourceField.TAX_GROUP, + destination_field: MappingSourceField.PROJECT, + import_to_fyle: true, + is_custom: true, + source_placeholder: null + }, + { + id: 2, + created_at: new Date(), + updated_at: new Date(), + workspace: 1, + source_field: 'CUSTOM_FIELD', + destination_field: MappingDestinationField.CLASS, + import_to_fyle: false, + is_custom: true, + source_placeholder: null + }], + workspace_general_settings: { + auto_create_destination_entity: true, + auto_create_merchants_as_vendors: true, + memo_structure: [], + change_accounting_period: true, + charts_of_accounts: ['Expense'], + created_at: new Date("2022-04-27T11:07:17.694377Z"), + id: 1, + employee_field_mapping: EmployeeFieldMapping.EMPLOYEE, + import_vendors_as_merchants: true, + import_items: true, + je_single_credit_line: false, + import_categories: false, + import_projects: false, + import_tax_codes: false, + skip_cards_mapping: false, + sync_fyle_to_qbo_payments: false, + sync_qbo_to_fyle_payments: false, + updated_at: new Date("2022-04-28T12:48:39.150177Z"), + workspace: 1, + reimbursable_expenses_object: null, + corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE, + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + auto_map_employees: AutoMapEmployee.EMAIL, + is_simplify_report_closure_enabled: true + }, + workspace_id: 1 + }, + advanced_configurations: { + workspace_general_settings: { + sync_fyle_to_qbo_payments: true, + sync_qbo_to_fyle_payments: false, + auto_create_destination_entity: true, + change_accounting_period: true, + je_single_credit_line: true, + auto_create_merchants_as_vendors: true, + memo_structure: [] + }, + general_mappings: { + bill_payment_account: {id: '1', name: 'Fyle'} + }, + workspace_schedules: { + enabled: true, + interval_hours: 10, + emails_selected: [], + additional_email_options: [] + }, + workspace_id: 1 + }, + employee_mappings: { + workspace_id: 1, + workspace_general_settings: { + auto_map_employees: AutoMapEmployee.NAME, + employee_field_mapping: EmployeeFieldMapping.EMPLOYEE + } + } + }; + +export const mockGroupedDestinationAttribtues: GroupedDestinationAttribute = { + BANK_ACCOUNT: [{ + id: 3, + attribute_type: 'BANK_ACCOUNT', + display_name: "string", + value: "Fyle", + destination_id: "1", + active: true, + created_at: new Date(), + updated_at: new Date(), + workspace: 2, + detail: { + email: 'String', + fully_qualified_name: 'string' + } + }], + TAX_CODE: [], + CREDIT_CARD_ACCOUNT: [], + ACCOUNTS_PAYABLE: [], + VENDOR: [], + ACCOUNT: [] +}; diff --git a/src/app/integration/onboarding/onboarding-routing.module.ts b/src/app/integration/onboarding/onboarding-routing.module.ts index 3b250081..fc7e7688 100644 --- a/src/app/integration/onboarding/onboarding-routing.module.ts +++ b/src/app/integration/onboarding/onboarding-routing.module.ts @@ -9,6 +9,7 @@ import { OnboardingImportSettingsComponent } from './onboarding-import-settings/ import { OnboardingLandingComponent } from './onboarding-landing/onboarding-landing.component'; import { OnboardingQboConnectorComponent } from './onboarding-qbo-connector/onboarding-qbo-connector.component'; import { OnboardingComponent } from './onboarding.component'; +import { CloneSettingsComponent } from './clone-settings/clone-settings.component'; const routes: Routes = [ @@ -20,6 +21,11 @@ const routes: Routes = [ path: '', component: OnboardingComponent, children: [ + { + path: 'clone_settings', + component: CloneSettingsComponent, + canActivate: [WorkspacesGuard] + }, { path: 'export_settings', component: OnboardingExportSettingsComponent, diff --git a/src/app/integration/onboarding/onboarding.module.ts b/src/app/integration/onboarding/onboarding.module.ts index 3d3e83e0..20cf0369 100644 --- a/src/app/integration/onboarding/onboarding.module.ts +++ b/src/app/integration/onboarding/onboarding.module.ts @@ -10,8 +10,11 @@ import { MatLegacySelectModule as MatSelectModule } from '@angular/material/lega import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle'; import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; - +import { MatMenuModule } from '@angular/material/menu'; +import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; import { SharedModule } from 'src/app/shared/shared.module'; +import { DragDropModule } from '@angular/cdk/drag-drop'; // Components import { OnboardingExportSettingsComponent } from './onboarding-export-settings/onboarding-export-settings.component'; @@ -23,6 +26,7 @@ import { OnboardingLandingComponent } from './onboarding-landing/onboarding-land import { OnboardingQboConnectorComponent } from './onboarding-qbo-connector/onboarding-qbo-connector.component'; import { OnboardingEmployeeSettingsComponent } from './onboarding-employee-settings/onboarding-employee-settings.component'; import { OnboardingRoutingModule } from './onboarding-routing.module'; +import { CloneSettingsComponent } from './clone-settings/clone-settings.component'; @NgModule({ declarations: [ @@ -33,7 +37,8 @@ import { OnboardingRoutingModule } from './onboarding-routing.module'; OnboardingComponent, OnboardingLandingComponent, OnboardingQboConnectorComponent, - OnboardingEmployeeSettingsComponent + OnboardingEmployeeSettingsComponent, + CloneSettingsComponent ], imports: [ CommonModule, @@ -46,7 +51,12 @@ import { OnboardingRoutingModule } from './onboarding-routing.module'; MatSlideToggleModule, MatCheckboxModule, MatDialogModule, - SharedModule + SharedModule, + MatMenuModule, + MatTooltipModule, + SharedModule, + MatProgressSpinnerModule, + DragDropModule ] }) export class OnboardingModule { } diff --git a/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.spec.ts b/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.spec.ts index 481c19c1..47140bc7 100644 --- a/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.spec.ts +++ b/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.spec.ts @@ -5,7 +5,7 @@ import { UntypedFormBuilder, FormsModule, ReactiveFormsModule, Validators } from import { MatLegacySnackBar as MatSnackBar, MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from 'src/app/shared/shared.module'; -import { adminEmails, advancedSettingResponse, customFields, destinationAttribute, emailResponse, errorResponse, postExpenseFilterResponse, getadvancedSettingResponse, getadvancedSettingResponse2, getExpenseFilterResponse, memo, previewResponse, response, conditionMock1, conditionMock2, conditionMock3, customOperatorMock1, customOperatorMock2, customOperatorMock3, customOperatorMock4, claimNumberOperators, spentAtOperators, reportTitleOperators, conditionMock4, conditionFieldOptions, getExpenseFilterResponse2, getExpenseFilterResponse3, getExpenseFilterResponse4 } from './advanced-settings.fixture'; +import { adminEmails, advancedSettingResponse, customFields, destinationAttribute, emailResponse, errorResponse, postExpenseFilterResponse, getadvancedSettingResponse, getadvancedSettingResponse2, getExpenseFilterResponse, memo, previewResponse, response, conditionMock1, conditionMock2, conditionMock3, customOperatorMock1, customOperatorMock2, customOperatorMock3, customOperatorMock4, claimNumberOperators, spentAtOperators, reportTitleOperators, conditionMock4, conditionFieldOptions, getExpenseFilterResponse2, getExpenseFilterResponse3, getExpenseFilterResponse4, paymentSyncOptions } from './advanced-settings.fixture'; import { Router } from '@angular/router'; import { AdvancedSettingService } from 'src/app/core/services/configuration/advanced-setting.service'; import { MappingService } from 'src/app/core/services/misc/mapping.service'; @@ -47,7 +47,9 @@ describe('AdvancedSettingsComponent', () => { getWorkspaceAdmins: () => of(adminEmails), postExpenseFilter: () => of(postExpenseFilterResponse), getExpenseFilter: () => of(getExpenseFilterResponse), - deleteExpenseFilter: () => of() + getPaymentSyncOptions: () => paymentSyncOptions, + deleteExpenseFilter: () => of(), + openAddemailDialog: () => undefined }; service2 = { diff --git a/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.ts b/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.ts index d89a546e..493b0c98 100644 --- a/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.ts +++ b/src/app/shared/components/configuration/advanced-settings/advanced-settings.component.ts @@ -50,20 +50,7 @@ export class AdvancedSettingsComponent implements OnInit, OnDestroy { memoPreviewText: string = ''; - paymentSyncOptions: AdvancedSettingFormOption[] = [ - { - label: 'None', - value: null - }, - { - label: 'Export Fyle ACH Payments to QuickBooks Online', - value: PaymentSyncDirection.FYLE_TO_QBO - }, - { - label: 'Import QuickBooks Online Payments into Fyle', - value: PaymentSyncDirection.QBO_TO_FYLE - } - ]; + paymentSyncOptions: AdvancedSettingFormOption[] = this.advancedSettingService.getPaymentSyncOptions(); frequencyIntervals: AdvancedSettingFormOption[] = [...Array(24).keys()].map(day => { return { diff --git a/src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture.ts b/src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture.ts index 680610c7..1ffbf99e 100644 --- a/src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture.ts +++ b/src/app/shared/components/configuration/advanced-settings/advanced-settings.fixture.ts @@ -1,8 +1,8 @@ -import { AdvancedSettingGet, AdvancedSettingPost } from "src/app/core/models/configuration/advanced-setting.model"; +import { AdvancedSettingFormOption, AdvancedSettingGet, AdvancedSettingPost } from "src/app/core/models/configuration/advanced-setting.model"; import { DestinationAttribute } from "src/app/core/models/db/destination-attribute.model"; import { WorkspaceSchedule, WorkspaceScheduleEmailOptions } from "src/app/core/models/db/workspace-schedule.model"; import { WorkspaceGeneralSetting } from "src/app/core/models/db/workspace-general-setting.model"; -import { AutoMapEmployee, CorporateCreditCardExpensesObject, CustomOperatorOption, EmployeeFieldMapping, NameInJournalEntry, JoinOption, Operator, ReimbursableExpensesObject } from "src/app/core/models/enum/enum.model"; +import { AutoMapEmployee, CorporateCreditCardExpensesObject, CustomOperatorOption, EmployeeFieldMapping, JoinOption, Operator, PaymentSyncDirection, ReimbursableExpensesObject, NameInJournalEntry } from "src/app/core/models/enum/enum.model"; import { ConditionField, ExpenseFilterResponse, SkipExport } from "src/app/core/models/misc/skip-export.model"; export const response:WorkspaceGeneralSetting = { @@ -56,6 +56,7 @@ export const advancedSettingResponse:AdvancedSettingGet = { }, workspace_id: 1 }; + export const destinationAttribute: DestinationAttribute[] = [{ id: 1, attribute_type: 'EMPLOYEE', @@ -107,7 +108,20 @@ export const getadvancedSettingResponse:AdvancedSettingGet = { }, workspace_id: 1 }; - +export const paymentSyncOptions = [ + { + label: 'None', + value: null + }, + { + label: 'Export Fyle ACH Payments to Quickbooks Online', + value: PaymentSyncDirection.FYLE_TO_QBO + }, + { + label: 'Import Quickbooks Payments into Fyle', + value: PaymentSyncDirection.QBO_TO_FYLE + } +]; export const getadvancedSettingResponse2:AdvancedSettingGet = { workspace_general_settings: { sync_fyle_to_qbo_payments: true, diff --git a/src/app/shared/components/configuration/configuration-step-header-section/configuration-step-header-section.component.spec.ts b/src/app/shared/components/configuration/configuration-step-header-section/configuration-step-header-section.component.spec.ts index ad494f39..8ead9b1b 100644 --- a/src/app/shared/components/configuration/configuration-step-header-section/configuration-step-header-section.component.spec.ts +++ b/src/app/shared/components/configuration/configuration-step-header-section/configuration-step-header-section.component.spec.ts @@ -10,6 +10,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { environment } from 'src/environments/environment'; import { WorkspaceService } from 'src/app/core/services/workspace/workspace.service'; import { OnboardingState } from 'src/app/core/models/enum/enum.model'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; describe('ConfigurationStepHeaderSectionComponent', () => { let component: ConfigurationStepHeaderSectionComponent; @@ -25,7 +26,7 @@ describe('ConfigurationStepHeaderSectionComponent', () => { dialogRefSpyObj.componentInstance = { body: '' }; // Attach componentInstance to the spy object... beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule, HttpClientModule, MatSnackBarModule, BrowserAnimationsModule, HttpClientTestingModule], + imports: [RouterTestingModule, HttpClientModule, MatSnackBarModule, MatDialogModule, BrowserAnimationsModule, HttpClientTestingModule], declarations: [ ConfigurationStepHeaderSectionComponent], providers: [ WorkspaceService, { provide: Router, diff --git a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.scss b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.scss index 2fdb78f5..48892f29 100644 --- a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.scss +++ b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.scss @@ -2,30 +2,6 @@ width: 430px; } -:host { - .mat-slide-toggle { - &.mat-checked { - ::ng-deep.mat-slide-toggle-bar::after { - content: 'Yes'; - font-size: 75%; - color: #FFFFFF; - position: absolute; - left: 8px; - top: -1px; - } - } - &:not(.mat-checked) { - ::ng-deep.mat-slide-toggle-bar::after { - content: 'No'; - font-size: 75%; - color: #FFFFFF; - position: absolute; - left: 25px; - top: -2px; - } - } - } -} .export-schedule-section { padding-left: 28px; padding-top: 24px; diff --git a/src/app/shared/components/configuration/employee-settings/employee-settings.component.spec.ts b/src/app/shared/components/configuration/employee-settings/employee-settings.component.spec.ts index 90e10f03..7dfd21b6 100644 --- a/src/app/shared/components/configuration/employee-settings/employee-settings.component.spec.ts +++ b/src/app/shared/components/configuration/employee-settings/employee-settings.component.spec.ts @@ -46,7 +46,9 @@ describe('EmployeeSettingsComponent', () => { service1 = { getEmployeeSettings: () => of(response), - postEmployeeSettings: () => of(response) + postEmployeeSettings: () => of(response), + getEmployeeFieldMappingOptions: () => null, + getAutoMapEmployeeOptions: () => null }; service2 = { diff --git a/src/app/shared/components/configuration/employee-settings/employee-settings.component.ts b/src/app/shared/components/configuration/employee-settings/employee-settings.component.ts index ea65ebbe..2b6573c9 100644 --- a/src/app/shared/components/configuration/employee-settings/employee-settings.component.ts +++ b/src/app/shared/components/configuration/employee-settings/employee-settings.component.ts @@ -6,7 +6,7 @@ import { Router } from '@angular/router'; import { forkJoin } from 'rxjs'; import { EmployeeSettingFormOption, EmployeeSettingGet, EmployeeSettingModel } from 'src/app/core/models/configuration/employee-setting.model'; import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; -import { AutoMapEmployee, ConfigurationCtaText, EmployeeFieldMapping, OnboardingState, OnboardingStep, ProgressPhase, ReimbursableExpensesObject, UpdateEvent } from 'src/app/core/models/enum/enum.model'; +import { ConfigurationCtaText, EmployeeFieldMapping, OnboardingState, OnboardingStep, ProgressPhase, ReimbursableExpensesObject, UpdateEvent } from 'src/app/core/models/enum/enum.model'; import { ConfirmationDialog } from 'src/app/core/models/misc/confirmation-dialog.model'; import { EmployeeSettingService } from 'src/app/core/services/configuration/employee-setting.service'; import { ExportSettingService } from 'src/app/core/services/configuration/export-setting.service'; @@ -37,35 +37,9 @@ export class EmployeeSettingsComponent implements OnInit, OnDestroy { existingEmployeeFieldMapping: EmployeeFieldMapping | undefined; - employeeMappingOptions: EmployeeSettingFormOption[] = [ - { - value: EmployeeFieldMapping.EMPLOYEE, - label: 'Employees' - }, - { - value: EmployeeFieldMapping.VENDOR, - label: 'Vendors' - } - ]; - - autoMapEmployeeOptions: EmployeeSettingFormOption[] = [ - { - value: null, - label: 'None' - }, - { - value: AutoMapEmployee.NAME, - label: 'Fyle Name to QuickBooks Online Display name' - }, - { - value: AutoMapEmployee.EMAIL, - label: 'Fyle Email to QuickBooks Online Email' - }, - { - value: AutoMapEmployee.EMPLOYEE_CODE, - label: 'Fyle Employee Code to QuickBooks Online Display name' - } - ]; + employeeMappingOptions: EmployeeSettingFormOption[] = this.employeeSettingService.getEmployeeFieldMappingOptions(); + + autoMapEmployeeOptions: EmployeeSettingFormOption[] = this.employeeSettingService.getAutoMapEmployeeOptions(); windowReference: Window; @@ -202,7 +176,7 @@ export class EmployeeSettingsComponent implements OnInit, OnDestroy { this.setLiveEntityExample(responses[1]); this.employeeSettingsForm = this.formBuilder.group({ employeeMapping: [this.existingEmployeeFieldMapping, Validators.required], - autoMapEmployee: [responses[0].workspace_general_settings?.auto_map_employees, Validators.nullValidator] + autoMapEmployee: [responses[0].workspace_general_settings?.auto_map_employees] }); this.reimbursableExportType = responses[2].workspace_general_settings?.reimbursable_expenses_object; this.isLoading = false; @@ -218,5 +192,4 @@ export class EmployeeSettingsComponent implements OnInit, OnDestroy { ngOnInit(): void { this.setupForm(); } - } diff --git a/src/app/shared/components/configuration/employee-settings/employee-settings.fixture.ts b/src/app/shared/components/configuration/employee-settings/employee-settings.fixture.ts index 5dd1d3ad..dd1e5453 100644 --- a/src/app/shared/components/configuration/employee-settings/employee-settings.fixture.ts +++ b/src/app/shared/components/configuration/employee-settings/employee-settings.fixture.ts @@ -19,7 +19,8 @@ export const response1: ExportSettingGet = { workspace_general_settings: { reimbursable_expenses_object: ReimbursableExpensesObject.BILL, corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.BILL, - name_in_journal_entry: NameInJournalEntry.EMPLOYEE + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + is_simplify_report_closure_enabled: true }, general_mappings: { bank_account: { id: '1', name: 'Fyle' }, diff --git a/src/app/shared/components/configuration/export-settings/export-settings.component.html b/src/app/shared/components/configuration/export-settings/export-settings.component.html index dab5bd89..97bcb01d 100644 --- a/src/app/shared/components/configuration/export-settings/export-settings.component.html +++ b/src/app/shared/components/configuration/export-settings/export-settings.component.html @@ -90,7 +90,7 @@ [isFieldMandatory]="true" [mandatoryErrorListName]="'expense export grouping'" [iconPath]="'assets/images/svgs/general/tabs.svg'" - [label]="generateGroupingLabel('reimbursable')" + [label]="generateGroupingLabel(exportSource.REIMBURSABLE)" [subLabel]="'Grouping reflects how the expense entries of a ' + getExportType(exportSettingsForm.value.reimbursableExportType) + ' are posted in QuickBooks Online. For example, grouping by payment exports all expenses within a Fyle payment queue as one record.'" [placeholder]="'Select expense export grouping'" [formControllerName]="'reimbursableExportGroup'" @@ -232,7 +232,7 @@ [isFieldMandatory]="true" [mandatoryErrorListName]="'expense export grouping'" [iconPath]="'assets/images/svgs/general/tabs.svg'" - [label]="generateGroupingLabel('credit card')" + [label]="generateGroupingLabel(exportSource.CREDIT_CARD)" [subLabel]="'Grouping reflects how the expense entries of a ' + getExportType(exportSettingsForm.value.creditCardExportType) + ' are posted in QuickBooks Online. For example, grouping by payment exports all expenses within a Fyle payment queue as one consolidated record.'" [placeholder]="'Select expense export grouping'" [formControllerName]="'creditCardExportGroup'" diff --git a/src/app/shared/components/configuration/export-settings/export-settings.component.spec.ts b/src/app/shared/components/configuration/export-settings/export-settings.component.spec.ts index 90277a70..4e5e6666 100644 --- a/src/app/shared/components/configuration/export-settings/export-settings.component.spec.ts +++ b/src/app/shared/components/configuration/export-settings/export-settings.component.spec.ts @@ -5,8 +5,8 @@ import { MatLegacySnackBarModule as MatSnackBarModule} from '@angular/material/l import { ExportSettingsComponent } from './export-settings.component'; import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from 'src/app/shared/shared.module'; -import { CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExpenseState, ExportDateType, OnboardingState, ReimbursableExpensesObject } from 'src/app/core/models/enum/enum.model'; -import { destinationAttribute, errorResponse, exportResponse, exportResponse1, export_settings, replacecontent1, replacecontent2, replacecontent3, workspaceResponse, workspaceResponse1 } from './export-settings.fixture'; +import { CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExpenseState, ExportDateType, ExportSource, OnboardingState, ReimbursableExpensesObject } from 'src/app/core/models/enum/enum.model'; +import { destinationAttribute, errorResponse, exportResponse, exportResponse1, export_settings, mockCCCExpenseStateOptions, mockCreditCardExportType, mockNameInJournalEntry, mockReimbursableExpenseGroupingDateOptions, mockReimbursableExpenseGroupingFieldOptions, mockReimbursableExpenseStateOptions, mockReimbursableExportTypeOptions, replacecontent1, replacecontent2, replacecontent3, workspaceResponse, workspaceResponse1 } from './export-settings.fixture'; import { MappingService } from 'src/app/core/services/misc/mapping.service'; import { WorkspaceService } from 'src/app/core/services/workspace/workspace.service'; import { ExportSettingService } from 'src/app/core/services/configuration/export-setting.service'; @@ -33,7 +33,19 @@ describe('ExportSettingsComponent', () => { beforeEach(async () => { service1 = { getExportSettings: () => of(exportResponse), - postExportSettings: () => of(exportResponse) + postExportSettings: () => of(exportResponse), + exportSelectionValidator: () => undefined, + createCreditCardExpenseWatcher: () => undefined, + createReimbursableExpenseWatcher: () => undefined, + getReimbursableExpenseGroupingFieldOptions: () => mockReimbursableExpenseGroupingFieldOptions, + getReimbursableExpenseGroupingDateOptions: () => mockReimbursableExpenseGroupingDateOptions, + getcreditCardExportTypes: () => undefined, + getReimbursableExportTypeOptions: () => undefined, + getCCCExpenseStateOptions: () => undefined, + getExportGroup: () => undefined, + getReimbursableExpenseStateOptions: () => mockReimbursableExpenseStateOptions, + setGeneralMappingsValidator: () => undefined, + nameInJournalOptions: () => mockNameInJournalEntry }; service2 = { getGroupedQBODestinationAttributes: () => of(destinationAttribute), @@ -69,13 +81,13 @@ describe('ExportSettingsComponent', () => { component.exportSettings = exportResponse; component.exportSettingsForm = formbuilder.group({ expenseState: [component.exportSettings.expense_group_settings?.expense_state, Validators.required], - reimbursableExpense: [component.exportSettings.workspace_general_settings?.reimbursable_expenses_object ? true : false, (component as any).exportSelectionValidator()], + reimbursableExpense: [component.exportSettings.workspace_general_settings?.reimbursable_expenses_object ? true : false], reimbursableExportType: [component.exportSettings.workspace_general_settings?.reimbursable_expenses_object], - reimbursableExportGroup: [(component as any).getExportGroup(component.exportSettings.expense_group_settings?.reimbursable_expense_group_fields)], + reimbursableExportGroup: [exportSettingService.getExportGroup(component.exportSettings.expense_group_settings?.reimbursable_expense_group_fields)], reimbursableExportDate: [component.exportSettings.expense_group_settings?.reimbursable_export_date_type], - creditCardExpense: [component.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false, (component as any).exportSelectionValidator()], + creditCardExpense: [component.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false], creditCardExportType: [component.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object], - creditCardExportGroup: [(component as any).getExportGroup(component.exportSettings.expense_group_settings?.corporate_credit_card_expense_group_fields)], + creditCardExportGroup: [exportSettingService.getExportGroup(component.exportSettings.expense_group_settings?.corporate_credit_card_expense_group_fields)], creditCardExportDate: [component.exportSettings.expense_group_settings?.ccc_export_date_type], bankAccount: [component.exportSettings.general_mappings?.bank_account?.id ? component.exportSettings.general_mappings.bank_account : null], defaultCCCAccount: [component.exportSettings.general_mappings?.default_ccc_account?.id ? component.exportSettings.general_mappings.default_ccc_account : null], @@ -105,9 +117,6 @@ describe('ExportSettingsComponent', () => { expect(component.getExportType(ReimbursableExpensesObject.JOURNAL_ENTRY)).toEqual(output); }); - it('getReimbursableExportTypes function check', () => { - expect(component.getReimbursableExportTypes(EmployeeFieldMapping.EMPLOYEE)).toEqual(export_settings); - }); it('navigateToPreviousStep function check', () => { expect(component.navigateToPreviousStep()).toBeUndefined(); @@ -123,25 +132,9 @@ describe('ExportSettingsComponent', () => { it('generateGroupingLabel function check', () => { component.exportSettingsForm.controls.reimbursableExportType.patchValue(ReimbursableExpensesObject.EXPENSE); fixture.detectChanges(); - expect(component.generateGroupingLabel('reimbursable')).toEqual('How should the expenses be grouped?'); + expect(component.generateGroupingLabel(ExportSource.REIMBURSABLE)).toEqual('How should the expenses be grouped?'); }); - it('createReimbursableExpenseWatcher function check', () => { - component.ngOnInit(); - component.exportSettingsForm.controls.reimbursableExpense.patchValue(true); - expect((component as any).createReimbursableExpenseWatcher()).toBeUndefined(); - fixture.detectChanges(); - component.exportSettingsForm.controls.reimbursableExpense.patchValue(false); - expect((component as any).createReimbursableExpenseWatcher()).toBeUndefined(); - }); - - it('createCreditCardExpenseWatcher function check', () => { - component.exportSettingsForm.controls.creditCardExpense.patchValue(!component.exportSettingsForm.controls.creditCardExpense.value); - expect((component as any).createCreditCardExpenseWatcher()).toBeUndefined(); - // Fixture.detectChanges(); - component.exportSettingsForm.controls.creditCardExpense.patchValue(!component.exportSettingsForm.controls.creditCardExpense.value); - expect((component as any).createCreditCardExpenseWatcher()).toBeUndefined(); - }); it('restrictExpenseGroupSetting function check', () => { expect((component as any).restrictExpenseGroupSetting('CREDIT CARD PURCHASE')).toBeUndefined(); @@ -177,12 +170,6 @@ describe('ExportSettingsComponent', () => { expect(component.showReimbursableAccountsPayableField()).toBeTrue(); }); - it('setGeneralMappingsValidator function check', () => { - component.exportSettingsForm.controls.creditCardExportType.patchValue(CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE); - fixture.detectChanges(); - expect((component as any).setGeneralMappingsValidator()).toBeUndefined(); - }); - it('createReimbursableExportGroupWatcher function check', () => { const reimbursable = component.exportSettingsForm.controls.reimbursableExportGroup.value; component.exportSettingsForm.controls.reimbursableExportGroup.patchValue('expense_id'); @@ -201,11 +188,6 @@ describe('ExportSettingsComponent', () => { expect((component as any).createCreditCardExportGroupWatcher()).toBeUndefined(); }); - it('function check', () => { - expect((component as any).getExportGroup([ExpenseGroupingFieldOption.EXPENSE_ID])).toEqual('expense_id'); - expect((component as any).getExportGroup(null)).toEqual(''); - }); - it('advancedSettingAffected function check', () => { component.exportSettings.workspace_general_settings.corporate_credit_card_expenses_object = CorporateCreditCardExpensesObject.BILL; component.exportSettings.workspace_general_settings.reimbursable_expenses_object = ReimbursableExpensesObject.CHECK; @@ -272,15 +254,4 @@ describe('ExportSettingsComponent', () => { expect(exportSettingService.postExportSettings).toHaveBeenCalled(); expect(component.isLoading).toBeFalse(); }); - - it('exportSelectionValidator function check', () => { - const control = { value: ExpenseState.PAID, parent: formbuilder.group({ - reimbursableExpense: ReimbursableExpensesObject.BILL - }) }; - expect((component as any).exportSelectionValidator()(control as AbstractControl)).toBeNull(); - const control1 = { value: ExpenseState.PAYMENT_PROCESSING, parent: formbuilder.group({ - creditCardExpense: CorporateCreditCardExpensesObject.BILL - }) }; - expect((component as any).exportSelectionValidator()(control1 as AbstractControl)).toBeNull(); - }); }); diff --git a/src/app/shared/components/configuration/export-settings/export-settings.component.ts b/src/app/shared/components/configuration/export-settings/export-settings.component.ts index 16d06d5c..da0be457 100644 --- a/src/app/shared/components/configuration/export-settings/export-settings.component.ts +++ b/src/app/shared/components/configuration/export-settings/export-settings.component.ts @@ -3,8 +3,8 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Val import { Router } from '@angular/router'; import { forkJoin } from 'rxjs'; import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; -import { ConfigurationCtaText, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExpenseState, CCCExpenseState, ExportDateType, OnboardingState, OnboardingStep, ProgressPhase, ReimbursableExpensesObject, UpdateEvent, NameInJournalEntry } from 'src/app/core/models/enum/enum.model'; -import { ExportSettingGet, ExportSettingFormOption, ExportSettingModel } from 'src/app/core/models/configuration/export-setting.model'; +import { ConfigurationCtaText, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseGroupingFieldOption, ExpenseState, CCCExpenseState, ExportDateType, OnboardingState, OnboardingStep, ProgressPhase, ReimbursableExpensesObject, UpdateEvent, NameInJournalEntry, ExportSource } from 'src/app/core/models/enum/enum.model'; +import { ExportSettingGet, ExportSettingFormOption, ExportSettingModel, NameInJournalEntryOptions } from 'src/app/core/models/configuration/export-setting.model'; import { ExportSettingService } from 'src/app/core/services/configuration/export-setting.service'; import { HelperService } from 'src/app/core/services/core/helper.service'; import { MappingService } from 'src/app/core/services/misc/mapping.service'; @@ -55,75 +55,15 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { cccExpenseStateOptions: ExportSettingFormOption[]; - nameInJournalOptions = [ - { - label: 'Merchant Name', - value: NameInJournalEntry.MERCHANT - }, - { - label: 'Employee Name', - value: NameInJournalEntry.EMPLOYEE - } - ]; - - expenseGroupingFieldOptions: ExportSettingFormOption[] = [ - { - label: 'Report', - value: ExpenseGroupingFieldOption.CLAIM_NUMBER - }, - { - label: 'Payment', - value: ExpenseGroupingFieldOption.SETTLEMENT_ID - }, - { - label: 'Expense', - value: ExpenseGroupingFieldOption.EXPENSE_ID - } - ]; - - reimbursableExpenseGroupingDateOptions: ExportSettingFormOption[] = [ - { - label: 'Current Date', - value: ExportDateType.CURRENT_DATE - }, - { - label: 'Verification Date', - value: ExportDateType.VERIFIED_AT - }, - { - label: 'Spend Date', - value: ExportDateType.SPENT_AT - }, - { - label: 'Approval Date', - value: ExportDateType.APPROVED_AT - }, - { - label: 'Last Spend Date', - value: ExportDateType.LAST_SPENT_AT - } - ]; + nameInJournalOptions: NameInJournalEntryOptions[] = this.exportSettingService.nameInJournalOptions(); + + expenseGroupingFieldOptions: ExportSettingFormOption[] = this.exportSettingService.getReimbursableExpenseGroupingFieldOptions(); + + reimbursableExpenseGroupingDateOptions: ExportSettingFormOption[] = this.exportSettingService.getReimbursableExpenseGroupingDateOptions(); cccExpenseGroupingDateOptions: ExportSettingFormOption[]; - creditCardExportTypes: ExportSettingFormOption[] = [ - { - label: 'Bill', - value: CorporateCreditCardExpensesObject.BILL - }, - { - label: 'Credit Card Purchase', - value: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE - }, - { - label: 'Journal Entry', - value: CorporateCreditCardExpensesObject.JOURNAL_ENTRY - }, - { - label: 'Debit Card Expense', - value: CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE - } - ]; + creditCardExportTypes: ExportSettingFormOption[] = this.exportSettingService.getcreditCardExportTypes(); reimbursableExportTypes: ExportSettingFormOption[]; @@ -131,6 +71,8 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { ProgressPhase = ProgressPhase; + exportSource = ExportSource; + private readonly sessionStartTime = new Date(); private timeSpentEventRecorded: boolean = false; @@ -158,9 +100,9 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { return lowerCaseWord.charAt(0).toUpperCase() + lowerCaseWord.slice(1); } - generateGroupingLabel(exportSource: 'reimbursable' | 'credit card'): string { + generateGroupingLabel(exportSource: 'credit card' | 'reimbursable'): string { let exportType: ReimbursableExpensesObject | CorporateCreditCardExpensesObject; - if (exportSource === 'reimbursable') { + if (exportSource === ExportSource.REIMBURSABLE) { exportType = this.exportSettingsForm.value.reimbursableExportType; } else { exportType = this.exportSettingsForm.value.creditCardExportType; @@ -173,84 +115,6 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { return `How should the expense in ${this.getExportType(exportType)} be grouped?`; } - getReimbursableExportTypes(employeeFieldMapping: EmployeeFieldMapping): ExportSettingFormOption[] { - return { - EMPLOYEE: [ - { - label: 'Check', - value: ReimbursableExpensesObject.CHECK - }, - { - label: 'Expense', - value: ReimbursableExpensesObject.EXPENSE - }, - { - label: 'Journal Entry', - value: ReimbursableExpensesObject.JOURNAL_ENTRY - } - ], - VENDOR: [ - { - label: 'Bill', - value: ReimbursableExpensesObject.BILL - }, - { - label: 'Expense', - value: ReimbursableExpensesObject.EXPENSE - }, - { - label: 'Journal Entry', - value: ReimbursableExpensesObject.JOURNAL_ENTRY - } - ] - }[employeeFieldMapping]; - } - - private createReimbursableExpenseWatcher(): void { - this.exportSettingsForm.controls.reimbursableExpense.valueChanges.subscribe((isReimbursableExpenseSelected) => { - if (isReimbursableExpenseSelected) { - this.exportSettingsForm.controls.expenseState.setValidators(Validators.required); - this.exportSettingsForm.controls.expenseState.setValue(this.exportSettings.expense_group_settings?.expense_state ? this.exportSettings.expense_group_settings?.expense_state : ExpenseState.PAYMENT_PROCESSING); - this.exportSettingsForm.controls.reimbursableExportType.setValidators(Validators.required); - this.exportSettingsForm.controls.reimbursableExportGroup.setValidators(Validators.required); - this.exportSettingsForm.controls.reimbursableExportDate.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.expenseState.clearValidators(); - this.exportSettingsForm.controls.reimbursableExportType.clearValidators(); - this.exportSettingsForm.controls.reimbursableExportGroup.clearValidators(); - this.exportSettingsForm.controls.reimbursableExportDate.clearValidators(); - this.exportSettingsForm.controls.expenseState.setValue(null); - this.exportSettingsForm.controls.reimbursableExportType.setValue(null); - this.exportSettingsForm.controls.reimbursableExportGroup.setValue(null); - this.exportSettingsForm.controls.reimbursableExportDate.setValue(null); - } - - this.setGeneralMappingsValidator(); - }); - } - - private createCreditCardExpenseWatcher(): void { - this.exportSettingsForm.controls.creditCardExpense.valueChanges.subscribe((isCreditCardExpenseSelected) => { - if (isCreditCardExpenseSelected) { - this.exportSettingsForm.controls.cccExpenseState.setValidators(Validators.required); - this.exportSettingsForm.controls.cccExpenseState.setValue(this.exportSettings.expense_group_settings?.ccc_expense_state ? this.exportSettings.expense_group_settings?.ccc_expense_state : this.is_simplify_report_closure_enabled ? CCCExpenseState.APPROVED: CCCExpenseState.PAYMENT_PROCESSING); - this.exportSettingsForm.controls.creditCardExportType.setValidators(Validators.required); - this.exportSettingsForm.controls.creditCardExportGroup.setValidators(Validators.required); - this.exportSettingsForm.controls.creditCardExportDate.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.cccExpenseState.clearValidators(); - this.exportSettingsForm.controls.creditCardExportType.clearValidators(); - this.exportSettingsForm.controls.creditCardExportGroup.clearValidators(); - this.exportSettingsForm.controls.creditCardExportDate.clearValidators(); - this.exportSettingsForm.controls.cccExpenseState.setValue(null); - this.exportSettingsForm.controls.creditCardExportType.setValue(null); - this.exportSettingsForm.controls.creditCardExportGroup.setValue(null); - this.exportSettingsForm.controls.creditCardExportDate.setValue(null); - } - - this.setGeneralMappingsValidator(); - }); - } private restrictExpenseGroupSetting(creditCardExportType: string | null) : void { if (creditCardExportType === CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE || creditCardExportType === CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE) { @@ -284,53 +148,19 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { private createReimbursableExportTypeWatcher(): void { this.exportSettingsForm.controls.reimbursableExportType.valueChanges.subscribe(() => { - this.setGeneralMappingsValidator(); - }); + this.exportSettingService.setGeneralMappingsValidator(this.exportSettingsForm); +}); } private createCreditCardExportTypeWatcher(): void { this.restrictExpenseGroupSetting(this.exportSettings.workspace_general_settings.corporate_credit_card_expenses_object); this.exportSettingsForm.controls.creditCardExportType.valueChanges.subscribe((creditCardExportType: string) => { - this.setGeneralMappingsValidator(); + this.exportSettingService.setGeneralMappingsValidator(this.exportSettingsForm); this.restrictExpenseGroupSetting(creditCardExportType); this.showNameInJournalOption = creditCardExportType === CorporateCreditCardExpensesObject.JOURNAL_ENTRY ? true : false; }); } - private exportSelectionValidator(): ValidatorFn { - return (control: AbstractControl): {[key: string]: object} | null => { - let forbidden = true; - if (this.exportSettingsForm) { - if (typeof control.value === 'boolean') { - if (control.value) { - forbidden = false; - } else { - if (control.parent?.get('reimbursableExpense')?.value || control.parent?.get('creditCardExpense')?.value) { - forbidden = false; - } - } - } else if ((control.value === ExpenseState.PAID || control.value === ExpenseState.PAYMENT_PROCESSING || control.value === CCCExpenseState.APPROVED) - && (control.parent?.get('reimbursableExpense')?.value || control.parent?.get('creditCardExpense')?.value)) { - forbidden = false; - } - - if (!forbidden) { - control.parent?.get('expenseState')?.setErrors(null); - control.parent?.get('cccExpenseState')?.setErrors(null); - control.parent?.get('reimbursableExpense')?.setErrors(null); - control.parent?.get('creditCardExpense')?.setErrors(null); - return null; - } - } - - return { - forbiddenOption: { - value: control.value - } - }; - }; - } - showBankAccountField(): boolean { return this.employeeFieldMapping === EmployeeFieldMapping.EMPLOYEE && this.exportSettingsForm.controls.reimbursableExportType.value && this.exportSettingsForm.controls.reimbursableExportType.value !== ReimbursableExpensesObject.EXPENSE; } @@ -363,50 +193,6 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { return (this.exportSettingsForm.controls.reimbursableExportType.value === ReimbursableExpensesObject.BILL || this.exportSettingsForm.controls.creditCardExportType.value === CorporateCreditCardExpensesObject.BILL) ? ReimbursableExpensesObject.BILL : ReimbursableExpensesObject.JOURNAL_ENTRY; } - private setGeneralMappingsValidator(): void { - if (this.showBankAccountField()) { - this.exportSettingsForm.controls.bankAccount.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.bankAccount.clearValidators(); - this.exportSettingsForm.controls.bankAccount.updateValueAndValidity(); - } - - if (this.showCreditCardAccountField()) { - this.exportSettingsForm.controls.defaultCCCAccount.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.defaultCCCAccount.clearValidators(); - this.exportSettingsForm.controls.defaultCCCAccount.updateValueAndValidity(); - } - - if (this.showDebitCardAccountField()) { - this.exportSettingsForm.controls.defaultDebitCardAccount.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.defaultDebitCardAccount.clearValidators(); - this.exportSettingsForm.controls.defaultDebitCardAccount.updateValueAndValidity(); - } - - if (this.showReimbursableAccountsPayableField() || this.showCCCAccountsPayableField()) { - this.exportSettingsForm.controls.accountsPayable.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.accountsPayable.clearValidators(); - this.exportSettingsForm.controls.accountsPayable.updateValueAndValidity(); - } - - if (this.showDefaultCreditCardVendorField()) { - this.exportSettingsForm.controls.defaultCreditCardVendor.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.defaultCreditCardVendor.clearValidators(); - this.exportSettingsForm.controls.defaultCreditCardVendor.updateValueAndValidity(); - } - - if (this.showExpenseAccountField()) { - this.exportSettingsForm.controls.qboExpenseAccount.setValidators(Validators.required); - } else { - this.exportSettingsForm.controls.qboExpenseAccount.clearValidators(); - this.exportSettingsForm.controls.qboExpenseAccount.updateValueAndValidity(); - } - } - private createReimbursableExportGroupWatcher(): void { this.exportSettingsForm.controls.reimbursableExportGroup.valueChanges.subscribe((reimbursableExportGroup: ExpenseGroupingFieldOption) => { if (reimbursableExportGroup === ExpenseGroupingFieldOption.EXPENSE_ID) { @@ -444,14 +230,21 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { }); } + private setupExportWatchers(): void { + this.exportSettingsForm?.controls.reimbursableExpense?.setValidators(this.exportSettingService.exportSelectionValidator(this.exportSettingsForm)); + this.exportSettingsForm?.controls.creditCardExpense?.setValidators(this.exportSettingService.exportSelectionValidator(this.exportSettingsForm)); + } + private setCustomValidatorsAndWatchers(): void { + this.setupExportWatchers(); + // Date grouping this.setCreditCardExpenseGroupingDateOptions(this.exportSettingsForm.controls.creditCardExportGroup.value); // Toggles - this.createReimbursableExpenseWatcher(); - this.createCreditCardExpenseWatcher(); + this.exportSettingService.createReimbursableExpenseWatcher(this.exportSettingsForm, this.exportSettings); + this.exportSettingService.createCreditCardExpenseWatcher(this.exportSettingsForm, this.exportSettings); // Export select fields this.createReimbursableExportTypeWatcher(); @@ -461,18 +254,7 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { this.createReimbursableExportGroupWatcher(); this.createCreditCardExportGroupWatcher(); - this.setGeneralMappingsValidator(); - } - - private getExportGroup(exportGroups: string[] | null): string { - if (exportGroups) { - const exportGroup = exportGroups.find((exportGroup) => { - return exportGroup === ExpenseGroupingFieldOption.EXPENSE_ID || exportGroup === ExpenseGroupingFieldOption.CLAIM_NUMBER || exportGroup === ExpenseGroupingFieldOption.SETTLEMENT_ID; - }); - return exportGroup ? exportGroup : ExpenseGroupingFieldOption.CLAIM_NUMBER; - } - - return ''; + this.exportSettingService.setGeneralMappingsValidator(this.exportSettingsForm); } private getSettingsAndSetupForm(): void { @@ -485,7 +267,6 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { ]).subscribe(response => { this.exportSettings = response[0]; this.employeeFieldMapping = response[2].employee_field_mapping; - this.reimbursableExportTypes = this.getReimbursableExportTypes(this.employeeFieldMapping); this.bankAccounts = response[1].BANK_ACCOUNT; this.cccAccounts = response[1].CREDIT_CARD_ACCOUNT; @@ -495,28 +276,9 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { this.is_simplify_report_closure_enabled = response[2].is_simplify_report_closure_enabled; this.import_items = response[2].import_items; - this.cccExpenseStateOptions = [ - { - label: this.is_simplify_report_closure_enabled ? 'Approved' : 'Payment Processing', - value: this.is_simplify_report_closure_enabled ? CCCExpenseState.APPROVED: CCCExpenseState.PAYMENT_PROCESSING - }, - { - label: this.is_simplify_report_closure_enabled ? 'Closed' : 'Paid', - value: CCCExpenseState.PAID - } - ]; - - this.expenseStateOptions = [ - { - label: this.is_simplify_report_closure_enabled ? 'Processing' : 'Payment Processing', - value: ExpenseState.PAYMENT_PROCESSING - }, - { - label: this.is_simplify_report_closure_enabled ? 'Closed' : 'Paid', - value: ExpenseState.PAID - } - ]; - + this.reimbursableExportTypes = this.exportSettingService.getReimbursableExportTypeOptions(this.employeeFieldMapping); + this.cccExpenseStateOptions = this.exportSettingService.getCCCExpenseStateOptions(this.is_simplify_report_closure_enabled); + this.expenseStateOptions = this.exportSettingService.getReimbursableExpenseStateOptions(this.is_simplify_report_closure_enabled); this.showNameInJournalOption = this.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object === CorporateCreditCardExpensesObject.JOURNAL_ENTRY ? true : false; this.setupForm(); @@ -526,14 +288,14 @@ export class ExportSettingsComponent implements OnInit, OnDestroy { private setupForm(): void { this.exportSettingsForm = this.formBuilder.group({ expenseState: [this.exportSettings.expense_group_settings?.expense_state], - reimbursableExpense: [this.exportSettings.workspace_general_settings?.reimbursable_expenses_object ? true : false, this.exportSelectionValidator()], + reimbursableExpense: [this.exportSettings.workspace_general_settings?.reimbursable_expenses_object ? true : false], reimbursableExportType: [this.exportSettings.workspace_general_settings?.reimbursable_expenses_object], - reimbursableExportGroup: [this.getExportGroup(this.exportSettings.expense_group_settings?.reimbursable_expense_group_fields)], + reimbursableExportGroup: [this.exportSettingService.getExportGroup(this.exportSettings.expense_group_settings?.reimbursable_expense_group_fields)], reimbursableExportDate: [this.exportSettings.expense_group_settings?.reimbursable_export_date_type], cccExpenseState: [this.exportSettings.expense_group_settings?.ccc_expense_state], - creditCardExpense: [this.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false, this.exportSelectionValidator()], + creditCardExpense: [this.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false], creditCardExportType: [this.exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object], - creditCardExportGroup: [this.getExportGroup(this.exportSettings.expense_group_settings?.corporate_credit_card_expense_group_fields)], + creditCardExportGroup: [this.exportSettingService.getExportGroup(this.exportSettings.expense_group_settings?.corporate_credit_card_expense_group_fields)], creditCardExportDate: [this.exportSettings.expense_group_settings?.ccc_export_date_type], bankAccount: [this.exportSettings.general_mappings?.bank_account?.id ? this.exportSettings.general_mappings.bank_account : null], defaultCCCAccount: [this.exportSettings.general_mappings?.default_ccc_account?.id ? this.exportSettings.general_mappings.default_ccc_account : null], diff --git a/src/app/shared/components/configuration/export-settings/export-settings.fixture.ts b/src/app/shared/components/configuration/export-settings/export-settings.fixture.ts index c69c5426..42f8fdf6 100644 --- a/src/app/shared/components/configuration/export-settings/export-settings.fixture.ts +++ b/src/app/shared/components/configuration/export-settings/export-settings.fixture.ts @@ -1,7 +1,7 @@ import { ExportSettingFormOption, ExportSettingGet } from "src/app/core/models/configuration/export-setting.model"; import { GroupedDestinationAttribute } from "src/app/core/models/db/destination-attribute.model"; import { WorkspaceGeneralSetting } from "src/app/core/models/db/workspace-general-setting.model"; -import { AutoMapEmployee, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseState, CCCExpenseState, ExportDateType, ReimbursableExpensesObject, NameInJournalEntry } from "src/app/core/models/enum/enum.model"; +import { AutoMapEmployee, CorporateCreditCardExpensesObject, EmployeeFieldMapping, ExpenseState, CCCExpenseState, ExportDateType, ReimbursableExpensesObject, ExpenseGroupingFieldOption, NameInJournalEntry } from "src/app/core/models/enum/enum.model"; export const export_settings: ExportSettingFormOption[] = [ { @@ -152,7 +152,8 @@ export const exportResponse1: ExportSettingGet = { workspace_general_settings: { reimbursable_expenses_object: ReimbursableExpensesObject.BILL, corporate_credit_card_expenses_object: null, - name_in_journal_entry: NameInJournalEntry.EMPLOYEE + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + is_simplify_report_closure_enabled: true }, workspace_id: 1, general_mappings: { @@ -186,7 +187,7 @@ export const exportResponse: ExportSettingGet = { expense_group_settings: { expense_state: ExpenseState.PAID, ccc_expense_state: CCCExpenseState.PAID, - reimbursable_expense_group_fields: ['sample'], + reimbursable_expense_group_fields: [ExpenseGroupingFieldOption.EXPENSE_ID], reimbursable_export_date_type: ExportDateType.APPROVED_AT, corporate_credit_card_expense_group_fields: ['sipper'], ccc_export_date_type: ExportDateType.SPENT_AT @@ -194,7 +195,8 @@ export const exportResponse: ExportSettingGet = { workspace_general_settings: { reimbursable_expenses_object: ReimbursableExpensesObject.BILL, corporate_credit_card_expenses_object: CorporateCreditCardExpensesObject.BILL, - name_in_journal_entry: NameInJournalEntry.EMPLOYEE + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + is_simplify_report_closure_enabled: true }, general_mappings: { bank_account: { id: '1', name: 'Fyle' }, @@ -227,3 +229,125 @@ export const errorResponse = { company_name: 'QBO' } }; + +export const mockReimbursableExpenseGroupingFieldOptions = [ + { + label: 'Report', + value: ExpenseGroupingFieldOption.CLAIM_NUMBER + }, + { + label: 'Payment', + value: ExpenseGroupingFieldOption.SETTLEMENT_ID + }, + { + label: 'Expense', + value: ExpenseGroupingFieldOption.EXPENSE_ID + } +]; + +export const mockReimbursableExpenseGroupingDateOptions = [ + + { + label: 'Current Date', + value: ExportDateType.CURRENT_DATE + }, + { + label: 'Verification Date', + value: ExportDateType.VERIFIED_AT + }, + { + label: 'Spend Date', + value: ExportDateType.SPENT_AT + }, + { + label: 'Approval Date', + value: ExportDateType.APPROVED_AT + }, + { + label: 'Last Spend Date', + value: ExportDateType.LAST_SPENT_AT + } +]; + +export const mockCreditCardExportType = [ + { + label: 'Bill', + value: CorporateCreditCardExpensesObject.BILL + }, + { + label: 'Credit Card Purchase', + value: CorporateCreditCardExpensesObject.CREDIT_CARD_PURCHASE + }, + { + label: 'Journal Entry', + value: CorporateCreditCardExpensesObject.JOURNAL_ENTRY + }, + { + label: 'Debit Card Expense', + value: CorporateCreditCardExpensesObject.DEBIT_CARD_EXPENSE + } +]; + +export const mockReimbursableExportTypeOptions = { + EMPLOYEE: [ + { + label: 'Check', + value: ReimbursableExpensesObject.CHECK + }, + { + label: 'Expense', + value: ReimbursableExpensesObject.EXPENSE + }, + { + label: 'Journal Entry', + value: ReimbursableExpensesObject.JOURNAL_ENTRY + } + ], + VENDOR: [ + { + label: 'Bill', + value: ReimbursableExpensesObject.BILL + }, + { + label: 'Expense', + value: ReimbursableExpensesObject.EXPENSE + }, + { + label: 'Journal Entry', + value: ReimbursableExpensesObject.JOURNAL_ENTRY + } + ] +}; + +export const mockCCCExpenseStateOptions = [ + { + label: 'Payment Processing', + value: CCCExpenseState.PAYMENT_PROCESSING + }, + { + label: 'Paid', + value: CCCExpenseState.PAID + } +]; + +export const mockReimbursableExpenseStateOptions = [ + { + label: 'Processing', + value: ExpenseState.PAYMENT_PROCESSING + }, + { + label: 'Closed', + value: ExpenseState.PAID + } +]; + +export const mockNameInJournalEntry = [ + { + label: 'Merchant Name', + value: NameInJournalEntry.MERCHANT + }, + { + label: 'Employee Name', + value: NameInJournalEntry.EMPLOYEE + } +]; diff --git a/src/app/shared/components/configuration/import-settings/expense-field-creation-dialog/expense-field-creation-dialog.component.ts b/src/app/shared/components/configuration/import-settings/expense-field-creation-dialog/expense-field-creation-dialog.component.ts index a98e1714..4c686404 100644 --- a/src/app/shared/components/configuration/import-settings/expense-field-creation-dialog/expense-field-creation-dialog.component.ts +++ b/src/app/shared/components/configuration/import-settings/expense-field-creation-dialog/expense-field-creation-dialog.component.ts @@ -39,6 +39,7 @@ export class ExpenseFieldCreationDialogComponent implements OnInit { name: this.expenseFieldsCreationForm.get('name')?.value, source_placeholder: this.expenseFieldsCreationForm.get('placeholder')?.value }; + this.dialogRef.close(expenseField); } diff --git a/src/app/shared/components/configuration/import-settings/import-settings.component.spec.ts b/src/app/shared/components/configuration/import-settings/import-settings.component.spec.ts index 0eb468a5..10fc6809 100644 --- a/src/app/shared/components/configuration/import-settings/import-settings.component.spec.ts +++ b/src/app/shared/components/configuration/import-settings/import-settings.component.spec.ts @@ -8,7 +8,7 @@ import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from 'src/app/shared/shared.module'; import { Router } from '@angular/router'; import { of, throwError } from 'rxjs'; -import { chartOfAccountTypesList, errorResponse, destinationAttribute, expenseFieldresponse, getImportsettingResponse, postImportsettingresponse, QBOCredentialsResponse, qboField } from './import-settings.fixture'; +import { chartOfAccountTypesList, errorResponse, destinationAttribute, expenseFieldresponse, getImportsettingResponse, postImportsettingresponse, QBOCredentialsResponse, qboField, mockExpenseFieldsFormArray, mockPatchExpenseFieldsFormArray } from './import-settings.fixture'; import { MappingDestinationField, OnboardingState } from 'src/app/core/models/enum/enum.model'; import { ImportSettingService } from 'src/app/core/services/configuration/import-setting.service'; import { WorkspaceService } from 'src/app/core/services/workspace/workspace.service'; @@ -41,7 +41,9 @@ describe('ImportSettingsComponent', () => { beforeEach(async () => { service1 = { getImportSettings: () => of(getImportsettingResponse), - postImportSettings: () => of(postImportsettingresponse) + postImportSettings: () => of(postImportsettingresponse), + getExpenseFieldsFormArray: () => mockExpenseFieldsFormArray, + patchExpenseFieldEmitter: of(mockPatchExpenseFieldsFormArray) }; service2 = { getFyleExpenseFields: () => of(expenseFieldresponse), diff --git a/src/app/shared/components/configuration/import-settings/import-settings.component.ts b/src/app/shared/components/configuration/import-settings/import-settings.component.ts index 9ad70db9..36420e4e 100644 --- a/src/app/shared/components/configuration/import-settings/import-settings.component.ts +++ b/src/app/shared/components/configuration/import-settings/import-settings.component.ts @@ -120,42 +120,28 @@ export class ImportSettingsComponent implements OnInit, OnDestroy { }); } + private setupExpenseFieldWatcher(): void { + this.importSettingService.patchExpenseFieldEmitter.subscribe((expenseField) => { + if (expenseField.addSourceField) { + this.fyleExpenseFields.push(expenseField.source_field); + } + this.expenseFields.controls.filter(field => field.value.destination_field === expenseField.destination_field)[0].patchValue(expenseField); + }); + } + private setCustomValidatorsAndWatchers(): void { + this.setupExpenseFieldWatcher(); this.updateTaxGroupVisibility(); this.createTaxCodeWatcher(); } - private importToggleWatcher(): ValidatorFn { - return (control: AbstractControl): {[key: string]: object} | null => { - if (control.value) { - // Mark Fyle field as mandatory if toggle is enabled - control.parent?.get('source_field')?.setValidators(Validators.required); - control.parent?.get('source_field')?.setValidators(RxwebValidators.unique()); - } else { - // Reset Fyle field if toggle is disabled - control.parent?.get('source_field')?.clearValidators(); - control.parent?.get('source_field')?.setValue(null); - } - - return null; - }; - } - showImportVendors(): boolean { return !this.autoCreateMerchantsAsVendors; } private setupForm(): void { const chartOfAccountTypeFormArray = this.chartOfAccountTypesList.map((type) => this.createChartOfAccountField(type)); - const expenseFieldsFormArray = this.qboExpenseFields.map((field) => { - return this.formBuilder.group({ - source_field: [field.source_field], - destination_field: [field.destination_field], - import_to_fyle: [field.import_to_fyle, this.importToggleWatcher()], - disable_import_to_fyle: [field.disable_import_to_fyle], - source_placeholder: [''] - }); - }); + const expenseFieldsFormArray = this.importSettingService.getExpenseFieldsFormArray(this.qboExpenseFields, true); this.importSettingsForm = this.formBuilder.group({ chartOfAccount: [this.importSettings.workspace_general_settings.import_categories], diff --git a/src/app/shared/components/configuration/import-settings/import-settings.fixture.ts b/src/app/shared/components/configuration/import-settings/import-settings.fixture.ts index fd7a5eac..e63d1894 100644 --- a/src/app/shared/components/configuration/import-settings/import-settings.fixture.ts +++ b/src/app/shared/components/configuration/import-settings/import-settings.fixture.ts @@ -6,6 +6,7 @@ import { ExpenseField } from "src/app/core/models/misc/expense-field.model"; import { MappingSetting } from "src/app/core/models/db/mapping-setting.model"; import { DestinationAttribute } from "src/app/core/models/db/destination-attribute.model"; import { QBOCredentials } from "src/app/core/models/configuration/qbo-connector.model"; +import { FormBuilder, FormGroup } from "@angular/forms"; const workspaceresponse:WorkspaceGeneralSetting = { auto_create_destination_entity: true, @@ -184,3 +185,28 @@ export const errorResponse = { company_name: 'QBO' } }; +export const mockExpenseFieldsFormArray: FormGroup[] = [ + new FormBuilder().group({ + source_field: ['PROJECT'], + destination_field: ['CUSTOMER'], + disable_import_to_fyle: [false], + import_to_fyle: [true], + source_placeholder: [''] + }) +]; + +export const mockAdditionalEmailOptions: FormGroup[] = [ + new FormBuilder().group({ + name: ['NILESH'], + email: ['nilesh.p@fyle.in'] + }) +]; + +export const mockPatchExpenseFieldsFormArray = { + source_field: 'PROJECT', + destination_field: 'CUSTOMER', + import_to_fyle: true, + disable_import_to_fyle: false, + source_placeholder: '', + addSourceField: true +}; diff --git a/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.spec.ts b/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.spec.ts index 73a78887..e8a411a3 100644 --- a/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.spec.ts +++ b/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.spec.ts @@ -13,6 +13,7 @@ import { WorkspaceService } from 'src/app/core/services/workspace/workspace.serv import { ConfirmationDialog } from 'src/app/core/models/misc/confirmation-dialog.model'; import { MatLegacyDialog as MatDialog, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; import { AuthService } from 'src/app/core/services/core/auth.service'; +import { environment } from 'src/environments/environment'; describe('QboConnectorComponent', () => { let component: QboConnectorComponent; @@ -49,7 +50,8 @@ describe('QboConnectorComponent', () => { }; service3 = { refreshQBODimensions: () => of({}), - setOnboardingState: () => undefined + setOnboardingState: () => undefined, + getWorkspaceId: () => environment.tests.workspaceId }; service4 = { logout: () => undefined, @@ -126,7 +128,6 @@ describe('QboConnectorComponent', () => { component.isContinueDisabled = false; fixture.detectChanges(); expect(component.continueToNextStep()).toBeUndefined(); - expect(router.navigate).toHaveBeenCalledWith([`/workspaces/onboarding/employee_settings`]); }); it('continueToNextStep => isContinueDisabled = true function check', () => { diff --git a/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.ts b/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.ts index a7aa8e18..712a4896 100644 --- a/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.ts +++ b/src/app/shared/components/configuration/qbo-connector/qbo-connector.component.ts @@ -15,6 +15,9 @@ import { ConfirmationDialog } from 'src/app/core/models/misc/confirmation-dialog import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { ConfirmationDialogComponent } from '../../core/confirmation-dialog/confirmation-dialog.component'; import { TrackingService } from 'src/app/core/services/integration/tracking.service'; +import { HelperService } from 'src/app/core/services/core/helper.service'; +import { CloneSettingService } from 'src/app/core/services/configuration/clone-setting.service'; +import { CloneSettingExist } from 'src/app/core/models/configuration/clone-setting.model'; @Component({ selector: 'app-qbo-connector', @@ -49,6 +52,9 @@ export class QboConnectorComponent implements OnInit, OnDestroy { private timeSpentEventRecorded: boolean = false; + private disableCloneSettings: boolean; + + constructor( private authService: AuthService, private dialog: MatDialog, @@ -60,7 +66,9 @@ export class QboConnectorComponent implements OnInit, OnDestroy { private trackingService: TrackingService, private userService: UserService, private windowService: WindowService, - private workspaceService: WorkspaceService + private workspaceService: WorkspaceService, + private cloneSettingService: CloneSettingService, + private helperService: HelperService ) { this.windowReference = this.windowService.nativeWindow; } @@ -72,13 +80,26 @@ export class QboConnectorComponent implements OnInit, OnDestroy { this.trackingService.trackTimeSpent(OnboardingStep.CONNECT_QBO, {phase: ProgressPhase.ONBOARDING, durationInSeconds: Math.floor(differenceInMs / 1000), eventState: eventState}); } + checkCloneSettingsAvailablity(): void { + this.cloneSettingService.checkCloneSettingsExists().subscribe((response: CloneSettingExist) => { + if (response.is_available) { + this.showCloneSettingsDialog(response.workspace_name); + } else { + this.router.navigate(['/workspaces/onboarding/employee_settings']); + } + }); + } + continueToNextStep(): void { if (this.isContinueDisabled) { return; + } else if (this.disableCloneSettings) { + this.router.navigate(['/workspaces/onboarding/employee_settings']); + return; } this.trackSessionTime('success'); - this.router.navigate([`/workspaces/onboarding/employee_settings`]); + this.checkCloneSettingsAvailablity(); } switchFyleOrg(): void { @@ -116,6 +137,21 @@ export class QboConnectorComponent implements OnInit, OnDestroy { }); } + private showCloneSettingsDialog(workspaceName: string): void { + this.isContinueDisabled = false; + this.disableCloneSettings = true; + const data: ConfirmationDialog = { + title: 'Your settings are pre-filled', + contents: `Your previous organization's settings (${workspaceName}) have been copied over to the current organization +

You can change the settings or reset the configuration to restart the process from the beginning
`, + primaryCtaText: 'Continue', + hideSecondaryCTA: true, + hideWarningIcon: true + }; + + this.helperService.openDialogAndSetupRedirection(data, '/workspaces/onboarding/clone_settings'); + } + private showWarningDialog(): void { const data: ConfirmationDialog = { title: 'Incorrect account selected', diff --git a/src/app/shared/components/configuration/qbo-connector/qbo-connector.fixture.ts b/src/app/shared/components/configuration/qbo-connector/qbo-connector.fixture.ts index 49a8a415..eb7a1e23 100644 --- a/src/app/shared/components/configuration/qbo-connector/qbo-connector.fixture.ts +++ b/src/app/shared/components/configuration/qbo-connector/qbo-connector.fixture.ts @@ -46,7 +46,8 @@ export const exportResponse: ExportSettingGet = { workspace_general_settings: { reimbursable_expenses_object: null, corporate_credit_card_expenses_object: null, - name_in_journal_entry: NameInJournalEntry.EMPLOYEE + name_in_journal_entry: NameInJournalEntry.EMPLOYEE, + is_simplify_report_closure_enabled: true }, general_mappings: { bank_account: { id: '1', name: 'Fyle' }, diff --git a/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.html b/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.html index 67fbd414..8f5e9fea 100644 --- a/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.html +++ b/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.html @@ -2,11 +2,11 @@
-
+
-

- {{ data.title }} +

+ {{ data.title }}

diff --git a/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.ts b/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.ts index de7c06a0..7b1a9db1 100644 --- a/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/app/shared/components/core/confirmation-dialog/confirmation-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, OnInit } from '@angular/core'; -import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; import { ConfirmationDialog } from 'src/app/core/models/misc/confirmation-dialog.model'; +import { MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, MatLegacyDialogRef as MatDialogRef} from '@angular/material/legacy-dialog'; @Component({ selector: 'app-confirmation-dialog', @@ -11,6 +11,8 @@ export class ConfirmationDialogComponent implements OnInit { hideSecondaryCTA: boolean; + hideWarningIcon: boolean; + constructor( @Inject(MAT_DIALOG_DATA) public data: ConfirmationDialog, public dialogRef: MatDialogRef @@ -18,6 +20,7 @@ export class ConfirmationDialogComponent implements OnInit { ngOnInit(): void { this.hideSecondaryCTA = this.data.hideSecondaryCTA ? this.data.hideSecondaryCTA : false; + this.hideWarningIcon = this.data.hideWarningIcon ? this.data.hideWarningIcon : false; } } diff --git a/src/app/shared/components/core/select/select.component.html b/src/app/shared/components/core/select/select.component.html new file mode 100644 index 00000000..46761f87 --- /dev/null +++ b/src/app/shared/components/core/select/select.component.html @@ -0,0 +1,57 @@ +
+ + + + {{ option.label }} + + + + +
+ +

{{ option.value }}

+

{{ option.value }}

+ +
+
+
+
+
+ + +
+

Click here to Preivew on how + it + looks on QuickBooks

+

Click here to Preivew on how it + looks on QuickBooks

+
+ +
+
+ Example: Ryan Clark will map to Ryan Clark in QuickBooks. +
+
+ Example: ryan.clark@acme.com will map to ryan.clark@acme.com in QuickBooks. +
+
+ Example: E0146 will map to E0146 in QuickBooks. +
+
+
diff --git a/src/app/shared/components/core/select/select.component.scss b/src/app/shared/components/core/select/select.component.scss new file mode 100644 index 00000000..7310ebbc --- /dev/null +++ b/src/app/shared/components/core/select/select.component.scss @@ -0,0 +1,20 @@ +.select-field { + &--preview-text { + font-size: 12px; + @media only screen and (max-width: 1440px) { + padding-left: 10px; + } + } + + &--preview-btn { + color: #0660F6; + } + + &--select-example-section { + padding: 0px 8px 0px 10px; + } + + &--clone-settings-tax { + width: 280px; + } +} diff --git a/src/app/shared/components/core/select/select.component.spec.ts b/src/app/shared/components/core/select/select.component.spec.ts new file mode 100644 index 00000000..4744b572 --- /dev/null +++ b/src/app/shared/components/core/select/select.component.spec.ts @@ -0,0 +1,63 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectComponent } from './select.component'; +import { CorporateCreditCardExpensesObject, ReimbursableExpensesObject } from 'src/app/core/models/enum/enum.model'; +import { Router } from '@angular/router'; +import { SearchPipe } from 'src/app/shared/pipes/search.pipe'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatLegacyDialogModule as MatDialogModule, MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { of } from 'rxjs'; + +describe('SelectComponent', () => { + let component: SelectComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + const routerSpy = { navigate: jasmine.createSpy('navigate'), url: '/path' }; + let dialogSpy: jasmine.Spy; + const dialogRefSpyObj = jasmine.createSpyObj({ afterClosed: of({}), close: null }); + dialogRefSpyObj.componentInstance = { body: '' }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectComponent, SearchPipe ], + providers: [ + FormBuilder, + { provide: Router, useValue: routerSpy } + ], + imports: [ + MatDialogModule, NoopAnimationsModule + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectComponent); + component = fixture.componentInstance; + + formBuilder = TestBed.inject(FormBuilder); + const form = new FormGroup({ + employeeMapping: new FormControl(['EMPLOYEE']), + autoMapEmployee: new FormControl([true]), + emails: new FormControl(['fyle@fyle.in', 'integrations@fyle.in' ]) + }); + component.form = form; + component.formControllerName = 'employeeMapping'; + component.isFieldMandatory = true; + component.mandatoryErrorListName = 'option'; + component.placeholder = 'Select representation'; + dialogSpy = spyOn(TestBed.get(MatDialog), 'open').and.returnValue(dialogRefSpyObj); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('showQboExportPreview function check', () => { + component.showQboExportPreview(ReimbursableExpensesObject.BILL, null); + expect(dialogSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/app/shared/components/core/select/select.component.ts b/src/app/shared/components/core/select/select.component.ts new file mode 100644 index 00000000..1da2c752 --- /dev/null +++ b/src/app/shared/components/core/select/select.component.ts @@ -0,0 +1,69 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { AdvancedSettingFormOption } from 'src/app/core/models/configuration/advanced-setting.model'; +import { ExportSettingFormOption } from 'src/app/core/models/configuration/export-setting.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { ClickEvent, CorporateCreditCardExpensesObject, ProgressPhase, ReimbursableExpensesObject, SimpleSearchPage, SimpleSearchType } from 'src/app/core/models/enum/enum.model'; +import { HelperService } from 'src/app/core/services/core/helper.service'; +import { PreviewDialogComponent } from '../../configuration/preview-dialog/preview-dialog.component'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { TrackingService } from 'src/app/core/services/integration/tracking.service'; + +@Component({ + selector: 'app-select', + templateUrl: './select.component.html', + styleUrls: ['./select.component.scss'] +}) +export class SelectComponent implements OnInit { + + @Input() exportType: ReimbursableExpensesObject | CorporateCreditCardExpensesObject; + + @Input() placeholder: string; + + @Input() formControllerName: string; + + @Input() form: FormGroup; + + @Input() options: ExportSettingFormOption[] | AdvancedSettingFormOption[] | any[]; + + @Input() qboAttributes: DestinationAttribute[]; + + @Input() isFieldMandatory: boolean; + + @Input() mandatoryErrorListName: string; + + @Input() customErrorMessage: string; + + @Input() phase: ProgressPhase; + + @Input() isCloneSettingField: boolean; + + SimpleSearchPage = SimpleSearchPage; + + SimpleSearchType = SimpleSearchType; + + constructor( + public dialog: MatDialog, + public helperService: HelperService, + private trackingService: TrackingService + ) { } + + showQboExportPreview(reimbursableExportType: ReimbursableExpensesObject | null, creditCardExportType: CorporateCreditCardExpensesObject | null): void { + const data = { + qboReimburse: reimbursableExportType, + qboCCC: creditCardExportType + }; + + this.trackingService.onClickEvent(ClickEvent.PREVIEW_QBO_EXPORT, {phase: this.phase, exportType: reimbursableExportType || creditCardExportType}); + + this.dialog.open(PreviewDialogComponent, { + width: '960px', + height: '530px', + data: data + }); + } + + ngOnInit(): void { + } + +} diff --git a/src/app/shared/components/core/toggle/toggle.component.html b/src/app/shared/components/core/toggle/toggle.component.html new file mode 100644 index 00000000..8860628f --- /dev/null +++ b/src/app/shared/components/core/toggle/toggle.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/shared/components/core/toggle/toggle.component.scss b/src/app/shared/components/core/toggle/toggle.component.scss new file mode 100644 index 00000000..9129ccf0 --- /dev/null +++ b/src/app/shared/components/core/toggle/toggle.component.scss @@ -0,0 +1,24 @@ +:host { + .mat-slide-toggle { + &.mat-checked { + ::ng-deep.mat-slide-toggle-bar::after { + content: 'Yes'; + font-size: 75%; + color: #FFFFFF; + position: absolute; + left: 8px; + top: -1px; + } + } + &:not(.mat-checked) { + ::ng-deep.mat-slide-toggle-bar::after { + content: 'No'; + font-size: 75%; + color: #FFFFFF; + position: absolute; + left: 25px; + top: -1px; + } + } + } +} diff --git a/src/app/shared/components/core/toggle/toggle.component.spec.ts b/src/app/shared/components/core/toggle/toggle.component.spec.ts new file mode 100644 index 00000000..d5343fe5 --- /dev/null +++ b/src/app/shared/components/core/toggle/toggle.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToggleComponent } from './toggle.component'; + +describe('ToggleComponent', () => { + let component: ToggleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ToggleComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/core/toggle/toggle.component.ts b/src/app/shared/components/core/toggle/toggle.component.ts new file mode 100644 index 00000000..ee3810d3 --- /dev/null +++ b/src/app/shared/components/core/toggle/toggle.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-toggle', + templateUrl: './toggle.component.html', + styleUrls: ['./toggle.component.scss'] +}) +export class ToggleComponent implements OnInit { + + @Input() form: FormGroup; + + @Input() formControllerName: string; + + @Input() isCloneSettings: boolean; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/shared/components/export-log/skip-export-log-table/skip-export-log-table.component.spec.ts b/src/app/shared/components/export-log/skip-export-log-table/skip-export-log-table.component.spec.ts index a48caaf5..577336c7 100644 --- a/src/app/shared/components/export-log/skip-export-log-table/skip-export-log-table.component.spec.ts +++ b/src/app/shared/components/export-log/skip-export-log-table/skip-export-log-table.component.spec.ts @@ -1,13 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SkipExportLogTableComponent } from './skip-export-log-table.component'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; describe('SkipExportLogTableComponent', () => { let component: SkipExportLogTableComponent; let fixture: ComponentFixture; beforeEach(async () => { + await TestBed.configureTestingModule({ - declarations: [ SkipExportLogTableComponent ] + declarations: [ SkipExportLogTableComponent ], + imports: [MatDialogModule] }) .compileComponents(); }); diff --git a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.html b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.html index 7a63e1d5..fe3a9e98 100644 --- a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.html +++ b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.html @@ -1,8 +1,7 @@

- Complete the following steps to configure your Integration. -

+ {{ isCloneSettingsActive ? 'Fill in the below fields to complete your integration set up' : 'Complete the following steps to configure your Integration'}}

@@ -14,11 +13,10 @@
-
+
-
{{onboardingStep.step}}
-
+
{{onboardingStep.step}}
diff --git a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.scss b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.scss index b593a83d..99c0be4b 100644 --- a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.scss +++ b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.scss @@ -58,6 +58,10 @@ margin-left: -24px; } + &--step-6-text { + margin-left: -10px; + } + &--icon { width: 24px; height: 24px; diff --git a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.spec.ts b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.spec.ts index db6dc9bd..97bdffda 100644 --- a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.spec.ts +++ b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.spec.ts @@ -49,4 +49,9 @@ describe('OnboardingStepperComponent', () => { component.navigate(true, '/login'); expect(component.navigate).toBeTruthy(); }); + + it('updateActiveAndCompletedSteps', () => { + component.currentStep = 'Clone Settings'; + expect((component as any).updateActiveAndCompletedSteps()); + }); }); diff --git a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.ts b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.ts index 46ec33e2..9dc282a0 100644 --- a/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.ts +++ b/src/app/shared/components/helpers/onboarding-stepper/onboarding-stepper.component.ts @@ -14,6 +14,8 @@ export class OnboardingStepperComponent implements OnInit { @Input() currentStep: string; + isCloneSettingsActive: boolean; + onboardingSteps: OnboardingStepper[] = [ { active: false, @@ -83,25 +85,46 @@ export class OnboardingStepperComponent implements OnInit { ) { } private updateActiveAndCompletedSteps(): void { - this.onboardingSteps.forEach(step => { - if (step.step === this.currentStep) { - step.active = true; - } - }); - const onboardingState: OnboardingState = this.workspaceService.getOnboardingState(); + if (this.currentStep === 'Clone Settings') { + this.isCloneSettingsActive = true; + this.onboardingSteps[0].completed = true; + this.onboardingSteps = [this.onboardingSteps[0]]; + this.onboardingSteps.push( + { + active: true, + completed: false, + number: 6, + step: 'Complete the Configurations', + icon: 'advanced-setting', + route: 'clone_settings', + size: { + height: '20px', + width: '20px' + } + } + ); + } else { + this.onboardingSteps.forEach(step => { + if (step.step === this.currentStep) { + step.active = true; + } + }); + + const onboardingState: OnboardingState = this.workspaceService.getOnboardingState(); - const onboardingStateStepMap = { - [OnboardingState.CONNECTION]: 1, - [OnboardingState.MAP_EMPLOYEES]: 2, - [OnboardingState.EXPORT_SETTINGS]: 3, - [OnboardingState.IMPORT_SETTINGS]: 4, - [OnboardingState.ADVANCED_CONFIGURATION]: 5, - [OnboardingState.COMPLETE]: 6 - }; + const onboardingStateStepMap = { + [OnboardingState.CONNECTION]: 1, + [OnboardingState.MAP_EMPLOYEES]: 2, + [OnboardingState.EXPORT_SETTINGS]: 3, + [OnboardingState.IMPORT_SETTINGS]: 4, + [OnboardingState.ADVANCED_CONFIGURATION]: 5, + [OnboardingState.COMPLETE]: 6 + }; - for (let index = onboardingStateStepMap[onboardingState] - 1; index > 0; index--) { - this.onboardingSteps[index - 1].completed = true; + for (let index = onboardingStateStepMap[onboardingState] - 1; index > 0; index--) { + this.onboardingSteps[index - 1].completed = true; + } } } @@ -114,5 +137,4 @@ export class OnboardingStepperComponent implements OnInit { ngOnInit(): void { this.updateActiveAndCompletedSteps(); } - } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a33eb1e1..acb566f9 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -24,6 +24,7 @@ import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/ import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; import { MatDatepickerModule } from '@angular/material/datepicker'; + // Pipes import { TrimCharacterPipe } from './pipes/trim-character.pipe'; import { SearchPipe } from './pipes/search.pipe'; @@ -64,6 +65,9 @@ import { MandatoryErrorMessageComponent } from './components/helpers/mandatory-e import { AddEmailDialogComponent } from './components/configuration/advanced-settings/add-email-dialog/add-email-dialog.component'; import { EmailMultiSelectFieldComponent } from './components/configuration/email-multi-select-field/email-multi-select-field.component'; import { SkipExportLogTableComponent } from './components/export-log/skip-export-log-table/skip-export-log-table.component'; +import { ToggleComponent } from './components/core/toggle/toggle.component'; +import { SelectComponent } from './components/core/select/select.component'; +import { EmailMultiSelectComponent } from '../core/email-multi-select/email-multi-select.component'; @NgModule({ declarations: [ @@ -104,7 +108,10 @@ import { SkipExportLogTableComponent } from './components/export-log/skip-export MandatoryErrorMessageComponent, AddEmailDialogComponent, EmailMultiSelectFieldComponent, - SkipExportLogTableComponent + SkipExportLogTableComponent, + ToggleComponent, + SelectComponent, + EmailMultiSelectComponent ], imports: [ CommonModule, @@ -158,7 +165,10 @@ import { SkipExportLogTableComponent } from './components/export-log/skip-export DashboardResolveMappingErrorDialogComponent, ExportLogChildTableComponent, MandatoryErrorMessageComponent, - MatChipsModule + MatChipsModule, + ToggleComponent, + SelectComponent, + EmailMultiSelectComponent ], schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ diff --git a/src/assets/images/svgs/actions/arrow-mark-down-pink.svg b/src/assets/images/svgs/actions/arrow-mark-down-pink.svg new file mode 100644 index 00000000..55911b42 --- /dev/null +++ b/src/assets/images/svgs/actions/arrow-mark-down-pink.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/svgs/general/calendar-disabled.svg b/src/assets/images/svgs/general/calendar-disabled.svg new file mode 100644 index 00000000..6cbbf2af --- /dev/null +++ b/src/assets/images/svgs/general/calendar-disabled.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/svgs/general/connect-arrow.svg b/src/assets/images/svgs/general/connect-arrow.svg new file mode 100644 index 00000000..667a5bd2 --- /dev/null +++ b/src/assets/images/svgs/general/connect-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/svgs/general/document-disabled.svg b/src/assets/images/svgs/general/document-disabled.svg new file mode 100644 index 00000000..4b4802d2 --- /dev/null +++ b/src/assets/images/svgs/general/document-disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/svgs/general/employee-disabled.svg b/src/assets/images/svgs/general/employee-disabled.svg new file mode 100644 index 00000000..b6d5c85d --- /dev/null +++ b/src/assets/images/svgs/general/employee-disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/svgs/general/info-disabled.svg b/src/assets/images/svgs/general/info-disabled.svg new file mode 100644 index 00000000..aa39e986 --- /dev/null +++ b/src/assets/images/svgs/general/info-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/svgs/general/question-disabled.svg b/src/assets/images/svgs/general/question-disabled.svg new file mode 100644 index 00000000..e746dc09 --- /dev/null +++ b/src/assets/images/svgs/general/question-disabled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/svgs/general/setting.svg b/src/assets/images/svgs/general/setting.svg new file mode 100644 index 00000000..18fb8b0a --- /dev/null +++ b/src/assets/images/svgs/general/setting.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/images/svgs/general/sync-disabled.svg b/src/assets/images/svgs/general/sync-disabled.svg new file mode 100644 index 00000000..04be8acf --- /dev/null +++ b/src/assets/images/svgs/general/sync-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/svgs/general/tabs-disabled.svg b/src/assets/images/svgs/general/tabs-disabled.svg new file mode 100644 index 00000000..a8b3382d --- /dev/null +++ b/src/assets/images/svgs/general/tabs-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/svgs/general/tick-green.svg b/src/assets/images/svgs/general/tick-green.svg new file mode 100644 index 00000000..c622e5da --- /dev/null +++ b/src/assets/images/svgs/general/tick-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/styles.scss b/src/styles.scss index a0f6ac22..71a28716 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -55,7 +55,7 @@ h3, h4, h5, h6 { margin: 0; } -h1, h2 { +h1, h2, ul { margin: 0; } @@ -336,6 +336,13 @@ th.mat-header-cell:last-of-type { transform: rotate(270deg); } } + + &.clone-settings { + @extend .mat-tooltip, .above, .align-center; + white-space: normal !important; + margin-right: 6px !important; + max-width: 600px !important; + } &.left { overflow: initial; @@ -448,6 +455,20 @@ th.mat-header-cell:last-of-type { right: 2px !important; } +.mat-mdc-menu-panel { + margin-top: 10px !important; + margin-left: 10px !important; + min-height: 40px!important; +} + +.mat-mdc-menu-item { + height: 24px!important; + font-size: 14px!important; + line-height: 0!important; + width: 230px!important; + min-height: 30px!important; +} + .selected-value-check-mark { width: 15px; height: 15px; @@ -460,6 +481,11 @@ th.mat-header-cell:last-of-type { box-shadow: 0px 4px 4px rgba(44, 48, 78, 0.1); } +.cdk-overlay-dark-backdrop { + background: #2C304E; + opacity: 0.7 !important; +} + .configuration { &--section { padding-bottom: 100px; @@ -838,3 +864,119 @@ th.mat-header-cell:last-of-type { } } } + +.import-settings { + &--field-checkbox-contents { + padding: 16px 32px 26px; + } + + &--chart-of-account-header { + padding-right: 8px; + } + + &--chart-of-account-list-section { + padding-top: 24px; + } + + &--chart-of-account-list { + padding-right: 81.67px; + padding-bottom: 24px; + } + + &--mapping-qbo-fyle-section { + width: 633.5px; + } + + &--qbo-field { + background: #F5F5F5; + border: 1px solid #DFDFE2; + box-sizing: border-box; + border-radius: 4px; + width: 280px; + height: 36px; + } + + &--qbo-field-text { + padding: 7px 161px 11px 17px; + } + + &--fyle-field { + width: 305px; + height: 40px; + margin-top: -5px; + } + + &--preview-text { + padding-top: 10px; + font-size: 12px; + } + + &--preview-btn { + color: #0660F6; + } + + &--fields-separator { + border-top: 1px solid #DFDFE2; + align-self: center; + width: 48px; + height: 26px; + } + + &--or-text { + padding: 8px 16px; + } + + &--field-label-section { + width: 650px; + padding-right: 46px; + } + + &--create-custom-field { + padding: 8px 0px; + } + + &--tax-section { + min-height: 102px; + } + + &--default-tax-section { + height: 160px; + } + + &--default-tax-contents { + padding: 16px 32px 40px; + } + + &--default-tax-field { + padding-right: 729.5px; + } + + &--default-tax-input { + width: 305px; + height: 40px; + } + + &--default-tax-header { + padding-bottom: 6px; + } + + &--default-tax-note { + padding-top: 12px; + } + + &--field-label-note { + padding-top: 18px; + } + + &--field { + background: #FAFCFF; + } + + &--field-toggle-section { + padding: 30px 8px 30px 32px; + background: #FAFCFF; + box-sizing: border-box; + border: 1px solid #F5F5F5; + } + +} \ No newline at end of file