diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index 447dc7472..b5515aa04 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -241,6 +241,73 @@ export const brandingDemoVideoLinks = demoVideoLinks[brandingConfig.brandId]; const content: ContentConfiguration = { fyle: { + xero: { + landing: { + contentText: 'Import data from Xero to ' + brandingConfig.brandName + ' and export expenses from ' + brandingConfig.brandName + ' to Xero. ', + guideHeaderText: 'Guide to setup your integrations' + }, + common: { + readMoreText: 'Read more', + exportLogTabName: 'Export log', + viewExpenseText: 'View expense', + corporateCard: 'Corporate card', + errors: 'errors', + autoMap: 'Auto map', + customField: 'Add new custom field', + customFieldName: 'Field name', + customFieldPlaceholderName: 'Placeholder name', + customFieldType: 'Field type', + customFieldCreateandSave: 'Create and save', + tenantMapping: 'Tenant Mapping', + descriptionText: 'of the description field' + }, + configuration: { + connector: { + configurationHeaderText: 'Connect to Xero Tenant', + configurationSubHeaderText: 'Connect to the Xero Tenant from which you would like to import and export data. The ' + brandingConfig.brandName + ' org and Xero Tenant cannot be changed once the configuration steps are complete.', + stepName: 'Connect to Xero', + subLabel: 'Expenses will be posted to the Xero Tenant Mapping selected here. Once configured, you can not change ' + brandingConfig.brandName + ' organization or Tenant Mapping.' + }, + exportSetting: { + stepName: 'Export settings', + headerText: '', + contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', + corporateCard: { + cccExpenseBankAccountSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', + creditCardExportTypeSubLabel: '', + expenseState: '', + creditCardExpenseSubLabel: '' + } + }, + importSetting: { + stepName: 'Import Settings', + headerText: '', + contentText: '', + importCategoriesLabel: 'Import the Chart of Accounts as Categories in ', + importCategoriesSubLabel: 'Imported account will be available as Categories in ' + brandingConfig.brandName + '.', + importCustomersLabel: 'Import Customers from Xero', + importCustomersSubLabel: 'The Customers in Xero will be imported as Projects in Fyle and will be a selectable field while creating an expense', + taxCodeLabel: 'Import Tax from Xero', + taxCodeSubLabel: 'The imported Tax codes from Xero will be set as Tax group in ', + defaultTaxCodeLabel: 'Select Default Tax Code', + importSuppliersAsMerchantsLabel: 'Import Suppliers from Xero as Merchants', + notes: 'NOTE: To export billable expenses from Fyle, import Customers from Xero as Projects in Fyle.', + toggleToastMessage: 'You have already mapped a tracking category from Xero to the Project field in '+ brandingConfig.brandName +'. Change the configured mapping to a new field to be able to import Customers in the Project field.' + }, + advancedSettings: { + stepName: 'Advanced settings', + scheduleAutoExport: 'Schedule automatic export', + email: 'Send error notification to', + autoSyncPayments: 'Auto-sync payment status for reimbursable expenses', + defaultPaymentAccount: 'Select payment account', + autoCreateEmployeeVendor: 'Auto-create ', + postEntriesCurrentPeriod: 'Post entries in the current accounting period', + setDescriptionField: 'Set the description field in Xero', + dfvLabel: 'Default field values', + dfvSubLabel: 'If you\'ve made a field mandatory in Xero but don\'t collect a value from your employees in the expense form, you can set a default value here to be added to all the expenses. For location and department, you can opt to use the values from your employee records in Xero.' + } + } + }, intacct: { landing: { contentText: 'Import data from Sage Intacct to ' + brandingConfig.brandName + ' and Export expenses from ' + brandingConfig.brandName + ' to Sage Intacct. ', @@ -418,6 +485,73 @@ const content: ContentConfiguration = { } }, co: { + xero: { + landing: { + contentText: 'Import data from Xero to ' + brandingConfig.brandName + ' and export expenses from ' + brandingConfig.brandName + ' to Xero. ', + guideHeaderText: 'Guide to setup your integrations' + }, + common: { + readMoreText: 'Read more', + exportLogTabName: 'Export log', + viewExpenseText: 'View expense', + corporateCard: 'Corporate card', + errors: 'errors', + autoMap: 'Auto map', + customField: 'Add new custom field', + customFieldName: 'Field name', + customFieldPlaceholderName: 'Placeholder name', + customFieldType: 'Field type', + customFieldCreateandSave: 'Create and save', + tenantMapping: 'Tenant Mapping', + descriptionText: 'of the description field' + }, + configuration: { + connector: { + configurationHeaderText: 'Connect to Xero tenant', + configurationSubHeaderText: 'Connect to the Xero tenant from which you would like to import and export data. The ' + brandingConfig.brandName + ' org and Xero tenant cannot be changed once the configuration steps are complete.', + stepName: 'Connect to Xero', + subLabel: 'Expenses will be posted to the Xero tenant Mapping selected here. Once configured, you can not change ' + brandingConfig.brandName + ' organization or tenant mapping.' + }, + exportSetting: { + stepName: 'Export settings', + headerText: '', + contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', + corporateCard: { + cccExpenseBankAccountSubLabel: '', + creditCardExportTypeSubLabel: '', + expenseState: '', + creditCardExpenseSubLabel: '' + } + }, + importSetting: { + stepName: 'Import Settings', + headerText: '', + contentText: '', + importCategoriesLabel: 'Import the chart of accounts as categories in ', + importCategoriesSubLabel: 'Imported account will be available as categories in ' + brandingConfig.brandName + '.', + importCustomersLabel: 'Import customers from Xero', + importCustomersSubLabel: 'The customers in Xero will be imported as projects in ' + brandingConfig.brandName + ' and will be a selectable field while creating an expense', + taxCodeLabel: 'Import tax from Xero', + taxCodeSubLabel: 'The imported tax codes from Xero will be set as tax group in ', + defaultTaxCodeLabel: 'Select default tax code', + importSuppliersAsMerchantsLabel: 'Import suppliers from Xero as merchants', + notes: 'NOTE: To export billable expenses from ' + brandingConfig.brandName + ', import customers from Xero as projects in ' + brandingConfig.brandName, + toggleToastMessage: 'You have already mapped a tracking category from Xero to the project field in '+ brandingConfig.brandName +'. Change the configured mapping to a new field to be able to import customers in the project field.' + }, + advancedSettings: { + stepName: 'Advanced settings', + scheduleAutoExport: 'Schedule automatic export', + email: 'Send error notification to', + autoSyncPayments: 'Auto-sync payment status for reimbursable expenses', + defaultPaymentAccount: 'Select payment account', + autoCreateEmployeeVendor: 'Auto-create ', + postEntriesCurrentPeriod: 'Post entries in the current accounting period', + setDescriptionField: 'Set the description field in Xero', + dfvLabel: 'Default field values', + dfvSubLabel: 'If you\'ve made a field mandatory in Xero but don\'t collect a value from your employees in the expense form, you can set a default value here to be added to all the expenses. For location and department, you can opt to use the values from your employee records in Xero.' + } + } + }, intacct: { landing: { contentText: 'Import data from Sage Intacct to ' + brandingConfig.brandName + ' and export expenses from ' + brandingConfig.brandName + ' to Sage Intacct. ', diff --git a/src/app/core/guard/tenant.guard.ts b/src/app/core/guard/tenant.guard.ts index eddbbf2a4..e7ab47dec 100644 --- a/src/app/core/guard/tenant.guard.ts +++ b/src/app/core/guard/tenant.guard.ts @@ -36,10 +36,11 @@ export class TenantGuard implements CanActivate { ).pipe( map(response => !!response), catchError(error => { - if (error.status === 400) { + + if (error.status === 404) { globalCacheBusterNotifier.next(); this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Oops! You will need to select a tenant to proceed with the onboarding.'); - return this.router.navigateByUrl('integrations/xero/onboarding/xero_connector'); + return this.router.navigateByUrl('/integrations/xero/onboarding/connector'); } return throwError(error); }) diff --git a/src/app/core/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index a089e8fcc..01fc2a3df 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -1,5 +1,72 @@ export type ContentConfiguration = { [brandingId: string]: { + xero: { + landing: { + contentText: string; + guideHeaderText: string; + }, + common: { + readMoreText: string; + exportLogTabName: string; + viewExpenseText: string; + corporateCard: string; + errors: string; + autoMap: string; + customField: string; + customFieldName: string; + customFieldPlaceholderName: string; + customFieldType: string; + customFieldCreateandSave: string; + tenantMapping: string; + descriptionText: string; + }, + configuration: { + connector: { + stepName: string; + subLabel: string; + configurationHeaderText: string; + configurationSubHeaderText: string; + }, + exportSetting: { + stepName: string; + headerText: string; + contentText: string; + corporateCard: { + cccExpenseBankAccountSubLabel: string; + creditCardExportTypeSubLabel: string; + expenseState: string; + creditCardExpenseSubLabel: string + } + }, + importSetting: { + stepName: string; + headerText: string; + contentText: string; + importCategoriesLabel: string; + importCategoriesSubLabel: string; + importCustomersLabel: string; + importCustomersSubLabel: string; + taxCodeLabel: string; + taxCodeSubLabel: string; + defaultTaxCodeLabel: string; + importSuppliersAsMerchantsLabel: string; + notes: string, + toggleToastMessage: string + }, + advancedSettings: { + stepName: string; + scheduleAutoExport: string; + email: string; + autoSyncPayments: string; + defaultPaymentAccount: string; + autoCreateEmployeeVendor: string; + postEntriesCurrentPeriod: string; + setDescriptionField: string; + dfvLabel: string; + dfvSubLabel: string; + } + }, + }, intacct : { landing: { contentText: string; diff --git a/src/app/core/models/enum/enum.model.ts b/src/app/core/models/enum/enum.model.ts index 54159772a..48580322c 100644 --- a/src/app/core/models/enum/enum.model.ts +++ b/src/app/core/models/enum/enum.model.ts @@ -246,9 +246,16 @@ export enum ExpenseState { PAID = 'PAID' } + export enum CCCExpenseState { PAID = 'PAID', - APPROVED = 'APPROVED' + APPROVED = 'APPROVED', +} + +export enum XeroCCCExpenseState { + PAID = 'PAID', + APPROVED = 'APPROVED', + PAYMENT_PROCESSING = "PAYMENT_PROCESSING" } export enum ExpenseGroupedBy { @@ -336,6 +343,13 @@ export enum QBDFyleField { COST_CENTER = 'COST_CENTER' } +export enum XeroFyleField { + PROJECT = 'PROJECT', + CUSTOMER = 'CUSTOMER', + TAX_CODE = 'TAX_CODE', + BANK_ACCOUNT = 'BANK_ACCOUNT' +} + export enum QBDAccountingExportsState { COMPLETE = 'COMPLETE', ENQUEUED = 'ENQUEUED', @@ -648,6 +662,7 @@ export enum ConfigurationWarningEvent { CLONE_SETTINGS = 'CLONE_SETTINGS', INCORRECT_QBO_ACCOUNT_CONNECTED = 'INCORRECT_QBO_ACCOUNT_CONNECTED', QBO_EXPORT_SETTINGS = 'QBO_EXPORT_SETTINGS', + XERO_EXPORT_SETTINGS = 'XERO_EXPORT_SETTINGS', RESET_CONFIGURATION = 'RESET_CONFIGURATION' } diff --git a/src/app/core/models/xero/db/xero-tenant-mapping.model.ts b/src/app/core/models/xero/db/xero-tenant-mapping.model.ts index 891236ab3..a413fca14 100644 --- a/src/app/core/models/xero/db/xero-tenant-mapping.model.ts +++ b/src/app/core/models/xero/db/xero-tenant-mapping.model.ts @@ -1,3 +1,5 @@ +import { DestinationAttribute } from "../../db/destination-attribute.model"; + /* Tslint:disable */ export type TenantMapping = { id: number; @@ -13,3 +15,12 @@ export type TenantMappingPost = { tenant_id: string; tenant_name: string; } + +export class TenantMappingModel { + static constructPayload(tenantMapping: DestinationAttribute): TenantMappingPost { + return { + tenant_id: tenantMapping.id.toString(), + tenant_name: tenantMapping.value + }; + } +} diff --git a/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts b/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts index cd6b38a46..c2cd37376 100644 --- a/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts @@ -1,8 +1,9 @@ -import { FormGroup } from "@angular/forms"; -import { EmailOption } from "../../common/advanced-settings.model"; +import { FormControl, FormGroup } from "@angular/forms"; +import { AdvancedSettingValidatorRule, AdvancedSettingsModel, EmailOption } from "../../common/advanced-settings.model"; import { SelectFormOption } from "../../common/select-form-option.model"; import { DefaultDestinationAttribute } from "../../db/destination-attribute.model"; import { PaymentSyncDirection } from "../../enum/enum.model"; +import { HelperUtility } from "../../common/helper.model"; export type XeroAdvancedSettingWorkspaceGeneralSetting = { @@ -56,7 +57,69 @@ export interface XeroAdvancedSettingFormOption extends SelectFormOption { value: PaymentSyncDirection | number | 'None'; } -export class XeroAdvancedSettingModel { +export class XeroAdvancedSettingModel extends HelperUtility{ + + static getPaymentSyncOptions(): SelectFormOption[] { + return [ + { + label: 'None', + value: 'None' + }, + { + label: 'Export Fyle ACH Payments to Xero', + value: PaymentSyncDirection.FYLE_TO_XERO + }, + { + label: 'Import Xero Payments into Fyle', + value: PaymentSyncDirection.XERO_TO_FYLE + } + ]; + } + + static getValidators(): AdvancedSettingValidatorRule { + return { + paymentSync: 'billPaymentAccount', + exportSchedule: 'exportScheduleFrequency' + }; + } + + static setConfigurationSettingValidatorsAndWatchers(form: FormGroup): void { + const validatorRule = this.getValidators(); + const keys = Object.keys(validatorRule); + + Object.values(validatorRule).forEach((value, index) => { + form.controls[keys[index]].valueChanges.subscribe((selectedValue) => { + if (selectedValue && ((keys[index] === 'paymentSync' && selectedValue === PaymentSyncDirection.FYLE_TO_XERO) || (keys[index] !== 'paymentSync'))) { + this.markControllerAsRequired(form, value); + } else { + this.clearValidatorAndResetValue(form, value); + } + }); + }); + } + + static mapAPIResponseToFormGroup(advancedSettings: XeroAdvancedSettingGet, adminEmails: EmailOption[]): FormGroup { + let paymentSync = ''; + if (advancedSettings.workspace_general_settings.sync_fyle_to_xero_payments) { + paymentSync = PaymentSyncDirection.FYLE_TO_XERO; + } else if (advancedSettings.workspace_general_settings.sync_xero_to_fyle_payments) { + paymentSync = PaymentSyncDirection.XERO_TO_FYLE; + } + return new FormGroup({ + paymentSync: new FormControl(paymentSync), + billPaymentAccount: new FormControl(advancedSettings.general_mappings.payment_account.id ? advancedSettings.general_mappings.payment_account : null), + changeAccountingPeriod: new FormControl(advancedSettings.workspace_general_settings.change_accounting_period), + autoCreateVendors: new FormControl(advancedSettings.workspace_general_settings.auto_create_destination_entity), + exportSchedule: new FormControl(advancedSettings.workspace_schedules?.enabled ? advancedSettings.workspace_schedules.interval_hours : false), + exportScheduleFrequency: new FormControl(advancedSettings.workspace_schedules?.enabled ? advancedSettings.workspace_schedules.interval_hours : 1), + autoCreateMerchantDestinationEntity: new FormControl(advancedSettings.workspace_general_settings.auto_create_merchant_destination_entity ? advancedSettings.workspace_general_settings.auto_create_merchant_destination_entity : false), + search: new FormControl(), + searchOption: new FormControl(), + email: new FormControl(advancedSettings?.workspace_schedules?.emails_selected && advancedSettings?.workspace_schedules?.emails_selected?.length > 0 ? AdvancedSettingsModel.filterAdminEmails(advancedSettings?.workspace_schedules?.emails_selected, adminEmails) : []), + additionalEmails: new FormControl([]) + }); + } + static constructPayload(advancedSettingsForm: FormGroup): XeroAdvancedSettingPost { const emptyDestinationAttribute = {id: null, name: null}; const advancedSettingPayload: XeroAdvancedSettingPost = { @@ -74,8 +137,8 @@ export class XeroAdvancedSettingModel { enabled: advancedSettingsForm.get('exportSchedule')?.value ? true : false, interval_hours: advancedSettingsForm.get('exportScheduleFrequency')?.value ? advancedSettingsForm.get('exportScheduleFrequency')?.value : null, start_datetime: new Date(), - emails_selected: advancedSettingsForm.get('emails')?.value ? advancedSettingsForm.get('emails')?.value : [], - additional_email_options: advancedSettingsForm.get('addedEmail')?.value ? advancedSettingsForm.get('addedEmail')?.value : [] + emails_selected: advancedSettingsForm.get('email')?.value ? AdvancedSettingsModel.formatSelectedEmails(advancedSettingsForm.get('email')?.value) : [], + additional_email_options: advancedSettingsForm.get('additionalEmails')?.value ? advancedSettingsForm.get('additionalEmails')?.value : [] } }; return advancedSettingPayload; diff --git a/src/app/core/models/xero/xero-configuration/xero-export-settings.model.ts b/src/app/core/models/xero/xero-configuration/xero-export-settings.model.ts index 06d5de6e7..30438a0a4 100644 --- a/src/app/core/models/xero/xero-configuration/xero-export-settings.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-export-settings.model.ts @@ -1,10 +1,20 @@ -import { FormGroup } from "@angular/forms"; +import { FormControl, FormGroup } from "@angular/forms"; import { SelectFormOption } from "../../common/select-form-option.model"; import { DefaultDestinationAttribute } from "../../db/destination-attribute.model"; import { ExpenseGroupSettingGet, ExpenseGroupSettingPost } from "../../db/expense-group-setting.model"; -import { AutoMapEmployeeOptions, CCCExpenseState, ExpenseGroupingFieldOption, ExpenseState, ExportDateType, XeroCorporateCreditCardExpensesObject, XeroReimbursableExpensesObject } from "../../enum/enum.model"; -import { ExportSettingGeneralMapping } from "../../intacct/intacct-configuration/export-settings.model"; +import { AutoMapEmployeeOptions, ExpenseGroupingFieldOption, ExpenseState, ExportDateType, XeroCCCExpenseState, XeroCorporateCreditCardExpensesObject, XeroReimbursableExpensesObject } from "../../enum/enum.model"; +import { ExportModuleRule, ExportSettingValidatorRule } from "../../common/export-settings.model"; +export type XeroExpenseGroupSettingPost = { + ccc_expense_state: XeroCCCExpenseState; + reimbursable_expense_group_fields?: string[] | null; + reimbursable_export_date_type: ExportDateType | null; + corporate_credit_card_expense_group_fields?: string[] | null; + ccc_export_date_type: ExportDateType | null; + reimbursable_expense_state: ExpenseState +}; + +export interface XeroExpenseGroupSettingGet extends XeroExpenseGroupSettingPost {} export type XeroExportSettingWorkspaceGeneralSettingPost = { reimbursable_expenses_object: XeroReimbursableExpensesObject | null, @@ -21,28 +31,186 @@ export type XeroExportSettingGeneralMapping = { } export type XeroExportSettingPost = { - expense_group_settings: ExpenseGroupSettingPost, + expense_group_settings: XeroExpenseGroupSettingPost, workspace_general_settings: XeroExportSettingWorkspaceGeneralSettingPost, general_mappings: XeroExportSettingGeneralMapping } export type XeroExportSettingGet = { - expense_group_settings: ExpenseGroupSettingGet, + expense_group_settings: XeroExpenseGroupSettingGet, workspace_general_settings: XeroExportSettingWorkspaceGeneralSetting, - general_mappings: ExportSettingGeneralMapping, + general_mappings: XeroExportSettingGeneralMapping, workspace_id: number } -export interface XeroExportSettingFormOption extends SelectFormOption { - value: ExpenseState | CCCExpenseState | XeroReimbursableExpensesObject | XeroCorporateCreditCardExpensesObject | ExpenseGroupingFieldOption | ExportDateType | AutoMapEmployeeOptions | null; +export interface XeroSelectFormOption extends SelectFormOption { + value: ExpenseState | XeroCCCExpenseState | XeroReimbursableExpensesObject | XeroCorporateCreditCardExpensesObject | ExpenseGroupingFieldOption | ExportDateType | AutoMapEmployeeOptions | null; } export class XeroExportSettingModel { + + static getReimbursableExportTypes() { + return [ + { + label: 'Purchase Bill', + value: XeroReimbursableExpensesObject.PURCHASE_BILL + } + ]; + } + + static getCreditCardExportTypes() { + return [ + { + label: 'Bank Transaction', + value: XeroCorporateCreditCardExpensesObject.BANK_TRANSACTION + } + ]; + } + + static getReimbursableExpenseGroupingOptions(): SelectFormOption[] { + return [ + { + label: 'Report', + value: ExpenseGroupingFieldOption.CLAIM_NUMBER + } + ]; + } + + static getCCCExpenseGroupingOptions(): SelectFormOption[] { + return [ + { + label: 'Expense', + value: ExpenseGroupingFieldOption.EXPENSE_ID + } + ]; + } + + static getAutoMapEmployeeOptions(): SelectFormOption[] { + return [ + { + label: 'None', + value: null + }, + { + label: 'Employee name on Fyle to contact name on Xero', + value: AutoMapEmployeeOptions.NAME + }, + { + label: 'Employee email on Fyle to contact email on Xero', + value: AutoMapEmployeeOptions.EMAIL + } + ]; + } + + static getReimbursableExpenseGroupingDateOptions(): SelectFormOption[] { + 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 + } + ]; + } + + static getCCCExpenseGroupingDateOptions(): SelectFormOption[] { + return [ + { + label: 'Spend Date', + value: ExportDateType.SPENT_AT + }, + { + label: 'Card Transaction Post date', + value: ExportDateType.POSTED_AT + } + ]; + } + + static getReimbursableExpenseStateOptions(): SelectFormOption[] { + return [ + { + label: 'Processing', + value: ExpenseState.PAYMENT_PROCESSING + }, + { + label: 'Closed', + value: ExpenseState.PAID + } + ]; + } + + static getCCCExpenseStateOptions(): SelectFormOption[] { + return [ + { + label: 'Payment Processing', + value: XeroCCCExpenseState.APPROVED + }, + { + label: 'Closed', + value: XeroCCCExpenseState.PAID + } + ]; + } + + static getValidators(): [ExportSettingValidatorRule, ExportModuleRule[]] { + const exportSettingValidatorRule: ExportSettingValidatorRule = { + reimbursableExpense: ['reimbursableExportType', 'reimbursableExportGroup', 'reimbursableExportDate', 'expenseState'], + creditCardExpense: ['creditCardExportType', 'creditCardExportGroup', 'creditCardExportDate', 'cccExpenseState', 'bankAccount'] + }; + + const exportModuleRule: ExportModuleRule[] = [ + { + formController: 'reimbursableExportType', + requiredValue: { + } + }, + { + formController: 'creditCardExportType', + requiredValue: { + } + } + ]; + + return [exportSettingValidatorRule, exportModuleRule]; + } + + static mapAPIResponseToFormGroup(exportSettings: XeroExportSettingGet | null): FormGroup { + return new FormGroup({ + expenseState: new FormControl(exportSettings?.expense_group_settings?.reimbursable_expense_state), + reimbursableExpense: new FormControl(exportSettings?.workspace_general_settings?.reimbursable_expenses_object ? true : false), + reimbursableExportType: new FormControl(exportSettings?.workspace_general_settings?.reimbursable_expenses_object ? exportSettings?.workspace_general_settings?.reimbursable_expenses_object : XeroReimbursableExpensesObject.PURCHASE_BILL), + reimbursableExportGroup: new FormControl(ExpenseGroupingFieldOption.CLAIM_NUMBER), + reimbursableExportDate: new FormControl(exportSettings?.expense_group_settings?.reimbursable_export_date_type), + cccExpenseState: new FormControl(exportSettings?.expense_group_settings?.ccc_expense_state), + creditCardExpense: new FormControl(exportSettings?.workspace_general_settings?.corporate_credit_card_expenses_object ? true : false), + creditCardExportType: new FormControl(exportSettings?.workspace_general_settings?.corporate_credit_card_expenses_object ? exportSettings?.workspace_general_settings?.corporate_credit_card_expenses_object : XeroCorporateCreditCardExpensesObject.BANK_TRANSACTION), + creditCardExportGroup: new FormControl(ExpenseGroupingFieldOption.EXPENSE_ID), + creditCardExportDate: new FormControl(exportSettings?.expense_group_settings?.ccc_export_date_type), + bankAccount: new FormControl(exportSettings?.general_mappings?.bank_account?.id ? exportSettings.general_mappings.bank_account : null), + autoMapEmployees: new FormControl(exportSettings?.workspace_general_settings?.auto_map_employees), + searchOption: new FormControl('') + }); + } + static constructPayload(exportSettingsForm: FormGroup): XeroExportSettingPost { const emptyDestinationAttribute = {id: null, name: null}; const exportSettingPayload: XeroExportSettingPost = { expense_group_settings: { - expense_state: exportSettingsForm.get('reimbursableExpenseState')?.value, + reimbursable_expense_state: exportSettingsForm.get('expenseState')?.value, reimbursable_export_date_type: exportSettingsForm.get('reimbursableExportDate')?.value ? exportSettingsForm.get('reimbursableExportDate')?.value : ExportDateType.CURRENT_DATE, ccc_expense_state: exportSettingsForm.get('cccExpenseState')?.value, ccc_export_date_type: exportSettingsForm.get('cccExportDate')?.value ? exportSettingsForm.get('cccExportDate')?.value : ExportDateType.SPENT_AT @@ -50,7 +218,7 @@ export class XeroExportSettingModel { workspace_general_settings: { reimbursable_expenses_object: exportSettingsForm.get('reimbursableExpense')?.value ? XeroReimbursableExpensesObject.PURCHASE_BILL : null, corporate_credit_card_expenses_object: exportSettingsForm.get('creditCardExpense')?.value ? XeroCorporateCreditCardExpensesObject.BANK_TRANSACTION : null, - auto_map_employees: exportSettingsForm.get('AutoMapEmployeeOptionss')?.value + auto_map_employees: exportSettingsForm.get('autoMapEmployees')?.value }, general_mappings: { bank_account: exportSettingsForm.get('bankAccount')?.value ? exportSettingsForm.get('bankAccount')?.value : emptyDestinationAttribute diff --git a/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts b/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts index 10bbff8b3..a4192ef7b 100644 --- a/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts @@ -1,12 +1,13 @@ -import { FormGroup } from "@angular/forms"; +import { FormArray, FormControl, FormGroup } from "@angular/forms"; import { SelectFormOption } from "../../common/select-form-option.model"; import { DefaultDestinationAttribute } from "../../db/destination-attribute.model"; import { MappingSetting } from "../../db/mapping-setting.model"; -import { MappingDestinationField, MappingSourceField } from "../../enum/enum.model"; -import { GeneralMapping } from "../../intacct/db/mappings.model"; +import { MappingDestinationField, MappingSourceField, XeroFyleField } from "../../enum/enum.model"; import { ImportSettingGeneralMapping } from "../../intacct/intacct-configuration/import-settings.model"; import { XeroWorkspaceGeneralSetting } from "../db/xero-workspace-general-setting.model"; -import { ImportSettingsModel } from "../../common/import-settings.model"; +import { ImportSettingMappingRow, ImportSettingsModel } from "../../common/import-settings.model"; +import { IntegrationField } from "../../db/mapping.model"; +import { brandingConfig } from "src/app/branding/branding-config"; export type XeroImportSettingWorkspaceGeneralSetting = { @@ -45,7 +46,7 @@ export type ExpenseFieldsFormOption = { export type XeroImportSettingGet = { workspace_general_settings: XeroWorkspaceGeneralSetting, - general_mappings: GeneralMapping, + general_mappings: XeroImportSettingGeneralMapping, mapping_settings: MappingSetting[], workspace_id:number } @@ -56,16 +57,46 @@ export interface XeroImportSettingFormOption extends SelectFormOption { export class XeroImportSettingModel extends ImportSettingsModel { + + static getChartOfAccountTypesList(): string[] { + return ['EXPENSE', 'ASSET', 'EQUITY', 'LIABILITY', 'REVENUE']; + } + + static mapAPIResponseToFormGroup(importSettings: XeroImportSettingGet | null, xeroFields: IntegrationField[], isCustomerPresent:boolean): FormGroup { + let additionalOption: any[] = []; + if (brandingConfig.brandId === 'co' && isCustomerPresent) { + const additionalMappingSetting = { + source_field: 'DISABLED_XERO_SOURCE_FIELD', + destination_field: XeroFyleField.CUSTOMER, + import_to_fyle: importSettings?.workspace_general_settings.import_customers || false, + is_custom: false, + source_placeholder: null + }; + additionalOption = [ImportSettingsModel.createFormGroup(additionalMappingSetting)]; + } + const expenseFieldsArray = importSettings?.mapping_settings ? additionalOption.concat(this.constructFormArray(importSettings.mapping_settings, xeroFields)) : []; + return new FormGroup({ + importCategories: new FormControl(importSettings?.workspace_general_settings.import_categories ?? false), + expenseFields: new FormArray(expenseFieldsArray), + chartOfAccountTypes: new FormControl(importSettings?.workspace_general_settings.charts_of_accounts ? importSettings.workspace_general_settings.charts_of_accounts : ['Expense']), + importCustomers: new FormControl(importSettings?.workspace_general_settings.import_customers ?? false), + taxCode: new FormControl(importSettings?.workspace_general_settings.import_tax_codes ?? false), + importSuppliersAsMerchants: new FormControl(importSettings?.workspace_general_settings.import_suppliers_as_merchants ?? false), + defaultTaxCode: new FormControl(importSettings?.general_mappings?.default_tax_code?.id ? importSettings.general_mappings.default_tax_code : null), + searchOption: new FormControl('') + }); + } + static constructPayload(importSettingsForm: FormGroup): XeroImportSettingPost { const emptyDestinationAttribute = {id: null, name: null}; const chartOfAccounts = XeroImportSettingModel.formatChartOfAccounts(importSettingsForm.get('chartOfAccountTypes')?.value); - const expenseFieldArray = importSettingsForm.getRawValue().expenseFields; + const expenseFieldArray = importSettingsForm.getRawValue().expenseFields.filter(((data:any) => data.destination_field !== XeroFyleField.CUSTOMER)); const mappingSettings = this.constructMappingSettingPayload(expenseFieldArray); const importSettingPayload: XeroImportSettingPost = { workspace_general_settings: { - import_categories: importSettingsForm.get('chartOfAccount')?.value, + import_categories: importSettingsForm.get('chartOfAccount')?.value ?? false, charts_of_accounts: importSettingsForm.get('chartOfAccount')?.value ? chartOfAccounts : ['Expense'], import_tax_codes: importSettingsForm.get('taxCode')?.value, import_suppliers_as_merchants: importSettingsForm.get('importSuppliersAsMerchants')?.value, diff --git a/src/app/core/models/xero/xero-configuration/xero-onboarding.model.ts b/src/app/core/models/xero/xero-configuration/xero-onboarding.model.ts index ea86aeaff..fd1aaa5c5 100644 --- a/src/app/core/models/xero/xero-configuration/xero-onboarding.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-onboarding.model.ts @@ -21,7 +21,7 @@ export class XeroOnboardingModel { step: 'Connect to Xero', icon: 'link-vertical-medium', route: '/integrations/xero/onboarding/connector', - styleClasses: ['step-name-connector--text'] + styleClasses: ['step-name-connector--text tw-pl-24-px'] }, { active: false, diff --git a/src/app/core/services/xero/xero-configuration/xero-connector.service.ts b/src/app/core/services/xero/xero-configuration/xero-connector.service.ts index e1e6c9d94..9df958633 100644 --- a/src/app/core/services/xero/xero-configuration/xero-connector.service.ts +++ b/src/app/core/services/xero/xero-configuration/xero-connector.service.ts @@ -5,8 +5,8 @@ import { WorkspaceService } from '../../common/workspace.service'; import { CacheBuster, Cacheable, globalCacheBusterNotifier } from 'ts-cacheable'; import { XeroCredentials } from 'src/app/core/models/xero/db/xero-credential.model'; import { environment } from 'src/environments/environment'; -import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; import { TenantMapping, TenantMappingPost } from 'src/app/core/models/xero/db/xero-tenant-mapping.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; const xeroCredentialsCache = new Subject(); diff --git a/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts b/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts index ffd740ae2..ac37b1ba9 100644 --- a/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts +++ b/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts @@ -4,6 +4,7 @@ import { WorkspaceService } from '../../common/workspace.service'; import { XeroImportSettingGet, XeroImportSettingPost } from 'src/app/core/models/xero/xero-configuration/xero-import-settings.model'; import { Observable, Subject } from 'rxjs'; import { CacheBuster, Cacheable } from 'ts-cacheable'; +import { IntegrationField } from 'src/app/core/models/db/mapping.model'; const xeroImportSettingGetCache$ = new Subject(); @@ -30,4 +31,8 @@ export class XeroImportSettingsService { postImportSettings(exportSettingsPayload: XeroImportSettingPost): Observable{ return this.apiService.put(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/import_settings/`, exportSettingsPayload); } + + getXeroField(): Observable { + return this.apiService.get(`/workspaces/${this.workspaceService.getWorkspaceId()}/xero/xero_fields/`, {}); + } } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html index 7065c19e6..70cdc677c 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html @@ -1 +1,4 @@ -

xero-onboarding-advanced-settings works!

+
+ + +
diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts index 0a88b4fae..5291c3320 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { brandingContent } from 'src/app/branding/branding-config'; +import { OnboardingStepper } from 'src/app/core/models/misc/onboarding-stepper.model'; +import { XeroOnboardingModel } from 'src/app/core/models/xero/xero-configuration/xero-onboarding.model'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; @Component({ selector: 'app-xero-onboarding-advanced-settings', @@ -7,7 +11,14 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingAdvancedSettingsComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.xero.configuration.advancedSettings; + + onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); + + constructor( + private workspaceService: WorkspaceService + ) { } + ngOnInit(): void { } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.html index aa6ca777b..05d4496d2 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.html @@ -1 +1,43 @@ -

xero-onboarding-connector works!

+
+ +
+
+ +
+
+
+ + +
+
+ + + + + +
+ +
+
+
diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts index e4b31e35b..2b4a5a2a4 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts @@ -1,4 +1,26 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { brandingConfig, brandingContent, brandingFeatureConfig, brandingKbArticles } from 'src/app/branding/branding-config'; +import { BrandingConfiguration } from 'src/app/core/models/branding/branding-configuration.model'; +import { CloneSettingExist } from 'src/app/core/models/common/clone-setting.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { ConfigurationCta, ConfigurationWarningEvent, ToastSeverity, XeroOnboardingState } from 'src/app/core/models/enum/enum.model'; +import { ConfigurationWarningOut } from 'src/app/core/models/misc/configuration-warning.model'; +import { OnboardingStepper } from 'src/app/core/models/misc/onboarding-stepper.model'; +import { XeroCredentials } from 'src/app/core/models/xero/db/xero-credential.model'; +import { TenantMapping, TenantMappingModel, TenantMappingPost } from 'src/app/core/models/xero/db/xero-tenant-mapping.model'; +import { XeroExportSettingGet } from 'src/app/core/models/xero/xero-configuration/xero-export-settings.model'; +import { XeroOnboardingModel } from 'src/app/core/models/xero/xero-configuration/xero-onboarding.model'; +import { CloneSettingService } from 'src/app/core/services/common/clone-setting.service'; +import { HelperService } from 'src/app/core/services/common/helper.service'; +import { IntegrationsToastService } from 'src/app/core/services/common/integrations-toast.service'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; +import { UserService } from 'src/app/core/services/misc/user.service'; +import { XeroConnectorService } from 'src/app/core/services/xero/xero-configuration/xero-connector.service'; +import { XeroExportSettingsService } from 'src/app/core/services/xero/xero-configuration/xero-export-settings.service'; +import { XeroHelperService } from 'src/app/core/services/xero/xero-core/xero-helper.service'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'app-xero-onboarding-connector', @@ -7,9 +29,231 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingConnectorComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.xero.configuration.connector; + + onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); + + isLoading: boolean = true; + + redirectLink: string = brandingKbArticles.onboardingArticles.XERO.CONNECTOR; + + brandingConfig: BrandingConfiguration = brandingConfig; + + ConfigurationCtaText = ConfigurationCta; + + saveInProgress: boolean = false; + + xeroConnectionInProgress: boolean = false; + + xeroCompanyName: string | null; + + isContinueDisabled: boolean = true; + + showDisconnectQBO: boolean = false; + + isWarningDialogVisible: boolean = false; + + xeroTokenExpired: boolean = false; + + isXeroConnected: boolean = false; + + private oauthCallbackSubscription: Subscription; + + readonly fyleOrgName: string = this.userService.getUserProfile().org_name; + + private isCloneSettingsDisabled: boolean; + + warningHeaderText: string; + + warningContextText: string; + + primaryButtonText: string; + + warningEvent: ConfigurationWarningEvent; + + showDisconnectXero: boolean; + + tenantList: DestinationAttribute[]; + + xeroTenantselected: DestinationAttribute; + + constructor( + private workspaceService: WorkspaceService, + private userService: UserService, + private xeroConnectorService: XeroConnectorService, + private exportSettingService: XeroExportSettingsService, + private helperService: HelperService, + private router: Router, + private toastService: IntegrationsToastService, + private cloneSettingService: CloneSettingService, + private xeroHelperService: XeroHelperService + ) { } + + private checkCloneSettingsAvailablity(): void { + this.cloneSettingService.checkCloneSettingsExists().subscribe((response: CloneSettingExist) => { + if (response.is_available) { + this.warningHeaderText = 'Your settings are pre-filled'; + this.warningContextText = `Your previous organization's settings (${response.workspace_name}) have been copied over to the current organization +

You can change the settings or reset the configuration to restart the process from the beginning
`; + this.primaryButtonText = 'Continue'; + this.warningEvent = ConfigurationWarningEvent.CLONE_SETTINGS; + this.isWarningDialogVisible = true; + this.isContinueDisabled = false; + this.isCloneSettingsDisabled = true; + } else { + this.router.navigate(['/integrations/xero/onboarding/export_settings']); + } + }); + } + + disconnectXero(): void { + this.isLoading = true; + this.xeroConnectorService.revokeXeroConnection(this.workspaceService.getWorkspaceId()).subscribe(() => { + this.showDisconnectXero = false; + this.xeroCompanyName = null; + this.xeroConnectionInProgress = false; + this.isXeroConnected = false; + this.isContinueDisabled = true; + this.xeroConnectorService.getXeroCredentials(this.workspaceService.getWorkspaceId()).subscribe((xeroCredentials: XeroCredentials) => { + this.showOrHideDisconnectXero(); + }, (error) => { + // Token expired + if ('id' in error.error) { + // We have a Xero row present in DB + this.xeroTokenExpired = error.error.is_expired; + if (this.xeroTokenExpired) { + this.xeroCompanyName = error.error.company_name; + } + } + this.isContinueDisabled = true; + this.isXeroConnected = false; + this.isLoading = false; + }); + }); + } + + private postXeroCredentials(code: string): void { + this.xeroConnectorService.connectXero(this.workspaceService.getWorkspaceId(), code).subscribe((xeroCredentials: XeroCredentials) => { + this.isXeroConnected = true; + this.xeroConnectionInProgress = false; + }, (error) => { + const errorMessage = 'message' in error.error ? error.error.message : 'Failed to connect to Xero Tenant. Please try again'; + if (errorMessage === 'Please choose the correct Xero Tenten') { + this.isXeroConnected = false; + this.xeroConnectionInProgress = false; + } else { + this.toastService.displayToastMessage(ToastSeverity.ERROR, errorMessage); + this.router.navigate([`/integrations/xero/onboarding/landing`]); + } + }); + } + + connectXero(companyDetails: DestinationAttribute): void { + this.xeroTenantselected = companyDetails; + this.isContinueDisabled = false; + } + + connectToXero() { + this.xeroConnectionInProgress = true; + const url = `${environment.xero_authorize_uri}?client_id=${environment.xero_oauth_client_id}&scope=${environment.xero_scope}&response_type=code&redirect_uri=${environment.xero_oauth_redirect_uri}&state=xero_local_redirect`; + this.oauthCallbackSubscription = this.helperService.oauthCallbackUrl.subscribe((callbackURL: string) => { + const code = callbackURL.split('code=')[1]?.split('&')[0]; + this.postXeroCredentials(code); + }); + this.helperService.oauthHandler(url); + } + + acceptWarning(data: ConfigurationWarningOut): void { + this.isWarningDialogVisible = false; + if (data.hasAccepted) { + if (data.event === ConfigurationWarningEvent.CLONE_SETTINGS) { + this.router.navigate(['/integrations/xero/onboarding/clone_settings']); + } + } + } + + private constructPayloadAndSave(): void { + if (this.isContinueDisabled) { + return; + } else if (this.isCloneSettingsDisabled) { + this.router.navigate(['/integrations/xero/onboarding/export_settings']); + return; + } + if (this.xeroTenantselected && !this.isContinueDisabled) { + this.xeroConnectionInProgress = true; + this.isContinueDisabled = true; + const tenantMappingPayload: TenantMappingPost = TenantMappingModel.constructPayload(this.xeroTenantselected); + this.xeroConnectorService.postTenantMapping(tenantMappingPayload).subscribe((response:TenantMapping) => { + this.xeroHelperService.refreshXeroDimensions().subscribe(() => { + this.workspaceService.setOnboardingState(XeroOnboardingState.EXPORT_SETTINGS); + this.xeroConnectionInProgress = false; + this.xeroTokenExpired = false; + this.showOrHideDisconnectXero(); + this.isXeroConnected = true; + this.xeroCompanyName = response.tenant_name; + this.checkCloneSettingsAvailablity(); + }); + }); + } else if (!this.isContinueDisabled && this.xeroCompanyName){ + this.checkCloneSettingsAvailablity(); + } + } + + save(): void { + if (this.isContinueDisabled) { + return; + } else if (this.isCloneSettingsDisabled) { + this.constructPayloadAndSave(); + return; + } + + if (!brandingFeatureConfig.featureFlags.cloneSettings) { + this.constructPayloadAndSave(); + } else { + this.checkCloneSettingsAvailablity(); + } + } + + getTenant() { + this.xeroConnectorService.getXeroTenants().subscribe((tenantList: DestinationAttribute[]) => { + this.tenantList = tenantList; + this.isXeroConnected = false; + this.isContinueDisabled = true; + this.xeroConnectionInProgress = false; + this.showOrHideDisconnectXero(); + }); + } + + private showOrHideDisconnectXero(): void { + this.exportSettingService.getExportSettings().subscribe((exportSettings: XeroExportSettingGet) => { + // Do nothing + this.isLoading = false; + + if (!(exportSettings.workspace_general_settings?.reimbursable_expenses_object || exportSettings.workspace_general_settings?.corporate_credit_card_expenses_object)) { + this.showDisconnectXero = true; + } + }, () => { + // Showing Disconnect Xero button since the customer didn't set up the next step + this.showDisconnectXero = true; + this.isLoading = false; + }); + } + + private setupPage() { + this.xeroConnectorService.getTenantMappings().subscribe((tenant: TenantMapping) => { + this.xeroCompanyName = tenant.tenant_name; + this.isXeroConnected = true; + this.isContinueDisabled = false; + this.showOrHideDisconnectXero(); + this.xeroConnectionInProgress = false; + }, + () => { + this.getTenant(); + }); + } ngOnInit(): void { + this.setupPage(); } } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.html index 2c41378ad..59345d37f 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.html @@ -1 +1 @@ -

xero-onboarding-done works!

+ diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.ts index 3ebe25424..4fdcf517d 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-done/xero-onboarding-done.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'app-xero-onboarding-done', @@ -7,7 +8,13 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingDoneComponent implements OnInit { - constructor() { } + constructor( + private router: Router + ) { } + + navigateToDashboard(): void { + this.router.navigate([`/integrations/xero/main/dashboard`]); + } ngOnInit(): void { } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.html index f2da483ce..8d71236de 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.html @@ -1 +1,4 @@ -

xero-onboarding-export-settings works!

+
+ + +
diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.ts index 157c772d5..1eb90695a 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-export-settings/xero-onboarding-export-settings.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { brandingContent } from 'src/app/branding/branding-config'; +import { OnboardingStepper } from 'src/app/core/models/misc/onboarding-stepper.model'; +import { XeroOnboardingModel } from 'src/app/core/models/xero/xero-configuration/xero-onboarding.model'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; @Component({ selector: 'app-xero-onboarding-export-settings', @@ -7,7 +11,13 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingExportSettingsComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.xero.configuration.exportSetting; + + onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); + + constructor( + private workspaceService: WorkspaceService + ) { } ngOnInit(): void { } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.html index 96c11da89..22c596262 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.html @@ -1 +1,4 @@ -

xero-onboarding-import-settings works!

+
+ + +
diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.ts index 27dfc9e63..76a104468 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-import-settings/xero-onboarding-import-settings.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { brandingContent } from 'src/app/branding/branding-config'; +import { OnboardingStepper } from 'src/app/core/models/misc/onboarding-stepper.model'; +import { XeroOnboardingModel } from 'src/app/core/models/xero/xero-configuration/xero-onboarding.model'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; @Component({ selector: 'app-xero-onboarding-import-settings', @@ -7,7 +11,13 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingImportSettingsComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.xero.configuration.importSetting; + + onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); + + constructor( + private workspaceService: WorkspaceService + ) { } ngOnInit(): void { } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts index 6daba1cec..97bb488f5 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts @@ -30,17 +30,17 @@ const routes: Routes = [ { path: 'import_settings', component: XeroOnboardingImportSettingsComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] }, { path: 'advanced_settings', component: XeroOnboardingAdvancedSettingsComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] }, { path: 'done', component: XeroOnboardingDoneComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] } ] } diff --git a/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.html b/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.html index 0b586be85..c1c09b99e 100644 --- a/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.html +++ b/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.html @@ -1 +1,111 @@ -

xero-advanced-settings works!

+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ + + +
+ + +
+
+
+ +
+ + +
+
+ + + + + + + + + + + + + + +
+
+ +
+
diff --git a/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.ts b/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.ts index 4615d75cf..c4cdcedd7 100644 --- a/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.ts +++ b/src/app/integrations/xero/xero-shared/xero-advanced-settings/xero-advanced-settings.component.ts @@ -1,4 +1,19 @@ import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { forkJoin } from 'rxjs'; +import { brandingConfig, brandingContent, brandingFeatureConfig, brandingKbArticles } from 'src/app/branding/branding-config'; +import { ConditionField, ExpenseFilterResponse } from 'src/app/core/models/common/advanced-settings.model'; +import { EmailOption, SelectFormOption } from 'src/app/core/models/common/select-form-option.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { AppName, ConfigurationCta, ToastSeverity, XeroFyleField, XeroOnboardingState } from 'src/app/core/models/enum/enum.model'; +import { XeroWorkspaceGeneralSetting } from 'src/app/core/models/xero/db/xero-workspace-general-setting.model'; +import { XeroAdvancedSettingGet, XeroAdvancedSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-advanced-settings.model'; +import { IntegrationsToastService } from 'src/app/core/services/common/integrations-toast.service'; +import { MappingService } from 'src/app/core/services/common/mapping.service'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; +import { XeroAdvancedSettingsService } from 'src/app/core/services/xero/xero-configuration/xero-advanced-settings.service'; +import { XeroHelperService } from 'src/app/core/services/xero/xero-core/xero-helper.service'; @Component({ selector: 'app-xero-advanced-settings', @@ -7,9 +22,108 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroAdvancedSettingsComponent implements OnInit { - constructor() { } + appName: AppName = AppName.XERO; + + isLoading: boolean = true; + + isOnboarding: boolean = false; + + supportArticleLink: string = brandingKbArticles.onboardingArticles.XERO.ADVANCED_SETTING; + + advancedSettings: XeroAdvancedSettingGet; + + workspaceGeneralSettings: XeroWorkspaceGeneralSetting; + + billPaymentAccounts: DestinationAttribute[]; + + advancedSettingForm: FormGroup; + + memoStructure: string[] = []; + + brandingConfig = brandingConfig; + + adminEmails: EmailOption[] = []; + + paymentSyncOptions: SelectFormOption[] = XeroAdvancedSettingModel.getPaymentSyncOptions(); + + hours: SelectFormOption[] = [...Array(24).keys()].map(day => { + return { + label: (day + 1).toString(), + value: day + 1 + }; + }); + + ConfigurationCtaText = ConfigurationCta; + + isSaveInProgress: boolean; + + readonly brandingFeatureConfig = brandingFeatureConfig; + + readonly brandingContent = brandingContent.configuration.advancedSettings; + + + constructor( + private advancedSettingService: XeroAdvancedSettingsService, + private router: Router, + private workspaceService: WorkspaceService, + private xeroHelperService: XeroHelperService, + private mappingService: MappingService, + private toastService: IntegrationsToastService + ) { } + + navigateToPreviousStep(): void { + this.router.navigate([`/integrations/xero/onboarding/import_settings`]); + } + + save(): void { + const advancedSettingPayload = XeroAdvancedSettingModel.constructPayload(this.advancedSettingForm); + this.isSaveInProgress = true; + + this.advancedSettingService.postAdvancedSettings(advancedSettingPayload).subscribe(() => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.SUCCESS, 'Advanced settings saved successfully'); + + if (this.isOnboarding) { + this.workspaceService.setOnboardingState(XeroOnboardingState.COMPLETE); + this.router.navigate([`/integrations/xero/onboarding/done`]); + } + }, () => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Error saving advanced settings, please try again later'); + }); + } + + refreshDimensions() { + this.xeroHelperService.refreshXeroDimensions().subscribe(); + } + + private setupFormWatchers() { + XeroAdvancedSettingModel.setConfigurationSettingValidatorsAndWatchers(this.advancedSettingForm); + } + + + private setupPage() { + this.isOnboarding = this.router.url.includes('onboarding'); + forkJoin([ + this.advancedSettingService.getAdvancedSettings(), + this.mappingService.getDestinationAttributes(XeroFyleField.BANK_ACCOUNT, 'v1', 'xero'), + this.workspaceService.getWorkspaceGeneralSettings(), + this.advancedSettingService.getWorkspaceAdmins() + ]).subscribe(response => { + this.advancedSettings = response[0]; + this.billPaymentAccounts = response[1]; + this.workspaceGeneralSettings = response[2]; + this.adminEmails = this.advancedSettings.workspace_schedules?.additional_email_options ? this.advancedSettings.workspace_schedules?.additional_email_options.concat(response[3]) : response[3]; + + this.advancedSettingForm = XeroAdvancedSettingModel.mapAPIResponseToFormGroup(this.advancedSettings, this.adminEmails); + + this.setupFormWatchers(); + this.isLoading = false; + }); + } ngOnInit(): void { + this.setupPage(); } } diff --git a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html index dd1a112dd..e34939645 100644 --- a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html +++ b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html @@ -1 +1,206 @@ -

xero-export-settings works!

+
+
+ +
+
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+ Note: In case the employee records are not auto matched by the integration, you could still manually map the records from the Mappings section of the integration. +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+
+ + + + diff --git a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts index 761b35d1c..314a5f21f 100644 --- a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts +++ b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts @@ -1,4 +1,19 @@ import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { forkJoin } from 'rxjs'; +import { brandingConfig, brandingContent, brandingFeatureConfig, brandingKbArticles } from 'src/app/branding/branding-config'; +import { SelectFormOption } from 'src/app/core/models/common/select-form-option.model'; +import { DefaultDestinationAttribute, DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { AppName, ConfigurationCta, ConfigurationWarningEvent, EmployeeFieldMapping, ToastSeverity, XeroCorporateCreditCardExpensesObject, XeroOnboardingState, XeroReimbursableExpensesObject } from 'src/app/core/models/enum/enum.model'; +import { ConfigurationWarningOut } from 'src/app/core/models/misc/configuration-warning.model'; +import { XeroExportSettingGet, XeroExportSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-export-settings.model'; +import { HelperService } from 'src/app/core/services/common/helper.service'; +import { IntegrationsToastService } from 'src/app/core/services/common/integrations-toast.service'; +import { MappingService } from 'src/app/core/services/common/mapping.service'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; +import { XeroExportSettingsService } from 'src/app/core/services/xero/xero-configuration/xero-export-settings.service'; +import { XeroHelperService } from 'src/app/core/services/xero/xero-core/xero-helper.service'; @Component({ selector: 'app-xero-export-settings', @@ -7,9 +22,128 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroExportSettingsComponent implements OnInit { - constructor() { } + isLoading: boolean = true; + + redirectLink: string = brandingKbArticles.onboardingArticles.XERO.EXPORT_SETTING; + + brandingConfig = brandingConfig; + + isOnboarding: boolean; + + windowReference: Window; + + exportSettings: XeroExportSettingGet; + + bankAccounts: DestinationAttribute[]; + + reimbursableExportTypes: SelectFormOption[] = XeroExportSettingModel.getReimbursableExportTypes(); + + creditCardExportTypes = XeroExportSettingModel.getCreditCardExportTypes(); + + reimbursableExpenseGroupByOptions = XeroExportSettingModel.getReimbursableExpenseGroupingOptions(); + + cccExpenseGroupByOptions = XeroExportSettingModel.getCCCExpenseGroupingOptions(); + + reimbursableExpenseGroupingDateOptions = XeroExportSettingModel.getReimbursableExpenseGroupingDateOptions(); + + cccExpenseGroupingDateOptions = XeroExportSettingModel.getCCCExpenseGroupingDateOptions(); + + autoMapEmployeeTypes = XeroExportSettingModel.getAutoMapEmployeeOptions(); + + expenseStateOptions = XeroExportSettingModel.getReimbursableExpenseStateOptions(); + + cccExpenseStateOptions = XeroExportSettingModel.getCCCExpenseStateOptions(); + + exportSettingForm: FormGroup; + + isSaveInProgress: boolean; + + isConfirmationDialogVisible: boolean; + + warningDialogText: string; + + appName: AppName = AppName.XERO; + + EmployeeFieldMapping = EmployeeFieldMapping; + + ConfigurationCtaText = ConfigurationCta; + + readonly brandingFeatureConfig = brandingFeatureConfig; + + readonly brandingContent = brandingContent.xero.configuration.exportSetting; + + constructor( + public helperService: HelperService, + private exportSettingService: XeroExportSettingsService, + private mappingService: MappingService, + private xeroHelperService: XeroHelperService, + private router : Router, + private workspaceService: WorkspaceService, + private toastService: IntegrationsToastService + ) { } + + refreshDimensions(isRefresh: boolean) { + this.xeroHelperService.refreshXeroDimensions().subscribe(); + } + + save() { + if (this.exportSettingForm.valid) { + this.constructPayloadAndSave({ + hasAccepted: true, + event: ConfigurationWarningEvent.XERO_EXPORT_SETTINGS + }); + } + } + + constructPayloadAndSave(event: ConfigurationWarningOut) { + if (event.hasAccepted) { + this.isSaveInProgress = true; + const exportSettingPayload = XeroExportSettingModel.constructPayload(this.exportSettingForm); + this.exportSettingService.postExportSettings(exportSettingPayload).subscribe((response: XeroExportSettingGet) => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.SUCCESS, 'Export settings saved successfully'); + + if (this.isOnboarding) { + this.workspaceService.setOnboardingState(XeroOnboardingState.IMPORT_SETTINGS); + this.router.navigate([`/integrations/xero/onboarding/import_settings`]); + } + }, () => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Error saving export settings, please try again later'); + }); + } + } + + setupPage() { + this.isOnboarding = this.router.url.includes('onboarding'); + const destinationAttributes = ['BANK_ACCOUNT']; + + forkJoin([ + this.exportSettingService.getExportSettings(), + this.mappingService.getGroupedDestinationAttributes(destinationAttributes, 'v1', 'xero') + ]).subscribe(response => { + this.exportSettings = response[0]; + this.bankAccounts = response[1].BANK_ACCOUNT; + this.exportSettingForm = XeroExportSettingModel.mapAPIResponseToFormGroup(this.exportSettings); + + this.helperService.addExportSettingFormValidator(this.exportSettingForm); + const [exportSettingValidatorRule, exportModuleRule] = XeroExportSettingModel.getValidators(); + + this.helperService.setConfigurationSettingValidatorsAndWatchers(exportSettingValidatorRule, this.exportSettingForm); + + this.helperService.setExportTypeValidatorsAndWatchers(exportModuleRule, this.exportSettingForm); + + this.isLoading = false; + + }); + } + + setupForm() { + throw new Error('Method not implemented.'); + } ngOnInit(): void { + this.setupPage(); } } diff --git a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html index c2207c283..87df98cbb 100644 --- a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html +++ b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html @@ -1 +1,120 @@ -

xero-import-settings works!

+
+ +
+
+
+ + +
+
+
+
+
+ + + +
+ + +
+
+ +
+ + +
+ {{brandingContent.notes}} +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + + + +
+
+ + +
+
+
+
+ + +
+
+ + +
diff --git a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.ts b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.ts index a0e7dfe9e..fc0fc738a 100644 --- a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.ts +++ b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.ts @@ -1,4 +1,21 @@ import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { forkJoin } from 'rxjs'; +import { brandingConfig, brandingContent, brandingFeatureConfig, brandingKbArticles } from 'src/app/branding/branding-config'; +import { ExpenseField, ImportSettingMappingRow, ImportSettingsModel } from 'src/app/core/models/common/import-settings.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; +import { FyleField, IntegrationField } from 'src/app/core/models/db/mapping.model'; +import { AppName, ConfigurationCta, ToastSeverity, XeroOnboardingState } from 'src/app/core/models/enum/enum.model'; +import { XeroFyleField } from 'src/app/core/models/enum/enum.model'; +import { XeroWorkspaceGeneralSetting } from 'src/app/core/models/xero/db/xero-workspace-general-setting.model'; +import { XeroImportSettingGet, XeroImportSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-import-settings.model'; +import { IntegrationsToastService } from 'src/app/core/services/common/integrations-toast.service'; +import { MappingService } from 'src/app/core/services/common/mapping.service'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; +import { XeroConnectorService } from 'src/app/core/services/xero/xero-configuration/xero-connector.service'; +import { XeroImportSettingsService } from 'src/app/core/services/xero/xero-configuration/xero-import-settings.service'; +import { XeroHelperService } from 'src/app/core/services/xero/xero-core/xero-helper.service'; @Component({ selector: 'app-xero-import-settings', @@ -7,9 +24,270 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroImportSettingsComponent implements OnInit { - constructor() { } + isLoading: boolean = true; + + appName: string = AppName.XERO; + + isOnboarding: boolean; + + importSettings: XeroImportSettingGet; + + fyleExpenseFields: FyleField[]; + + workspaceGeneralSettings: XeroWorkspaceGeneralSetting; + + xeroExpenseFields: IntegrationField[]; + + + taxCodes: DestinationAttribute[]; + + importSettingsForm: FormGroup; + + customFieldType: string; + + customFieldControl: AbstractControl; + + customFieldForm: FormGroup = this.formBuilder.group({ + attribute_type: ['', Validators.required], + display_name: [''], + source_placeholder: ['', Validators.required] + }); + + showCustomFieldDialog: boolean; + + isPreviewDialogVisible: boolean; + + customField: ExpenseField; + + customFieldOption: ExpenseField[] = ImportSettingsModel.getCustomFieldOption(); + + isSaveInProgress: boolean; + + ConfigurationCtaText = ConfigurationCta; + + chartOfAccountTypesList: string[] = XeroImportSettingModel.getChartOfAccountTypesList(); + + isTaxGroupSyncAllowed: boolean; + + isProjectMapped: boolean; + + isCustomerPresent: boolean; + + readonly brandingFeatureConfig = brandingFeatureConfig; + + readonly brandingContent = brandingContent.xero.configuration.importSetting; + + readonly supportArticleLink = brandingKbArticles.onboardingArticles.XERO.IMPORT_SETTING; + + readonly brandingConfig = brandingConfig; + + constructor( + private importSettingService: XeroImportSettingsService, + private workspaceService: WorkspaceService, + private router: Router, + private mappingService: MappingService, + private xeroHelperService: XeroHelperService, + private formBuilder: FormBuilder, + private toastService: IntegrationsToastService, + private xeroConnectorService: XeroConnectorService + ) { } + + closeModel() { + this.customFieldForm.reset(); + this.showCustomFieldDialog = false; + } + + showPreviewDialog(visible: boolean) { + this.isPreviewDialogVisible = visible; + } + + closeDialog() { + this.isPreviewDialogVisible = false; + } + + refreshDimensions() { + this.xeroHelperService.refreshXeroDimensions().subscribe(); + } + + navigateToPreviousStep(): void { + this.router.navigate([`/integrations/qbo/onboarding/export_settings`]); + } + + saveFyleExpenseField(): void { + this.customField = { + attribute_type: this.customFieldForm.value.attribute_type.split(' ').join('_').toUpperCase(), + display_name: this.customFieldForm.value.attribute_type, + source_placeholder: this.customFieldForm.value.source_placeholder, + is_dependent: false + }; + + if (this.customFieldControl) { + this.fyleExpenseFields.pop(); + this.fyleExpenseFields.push(this.customField); + this.fyleExpenseFields.push(this.customFieldOption[0]); + const expenseField = { + source_field: this.customField.attribute_type, + destination_field: this.customFieldControl.value.destination_field, + import_to_fyle: true, + is_custom: true, + source_placeholder: this.customField.source_placeholder + }; + (this.importSettingsForm.get('expenseFields') as FormArray).controls.filter(field => field.value.destination_field === this.customFieldControl.value.destination_field)[0].patchValue(expenseField); + ((this.importSettingsForm.get('expenseFields') as FormArray).controls.filter(field => field.value.destination_field === this.customFieldControl.value.destination_field)[0] as FormGroup).controls.import_to_fyle.disable(); + this.customFieldForm.reset(); + this.showCustomFieldDialog = false; + } + } + + private initializeCustomFieldForm(shouldShowDialog: boolean) { + this.customFieldForm.reset(); + this.showCustomFieldDialog = shouldShowDialog; + } + + private createTaxCodeWatcher(): void { + this.importSettingsForm.controls.taxCode.valueChanges.subscribe((isTaxCodeEnabled) => { + if (isTaxCodeEnabled) { + this.importSettingsForm.controls.defaultTaxCode.setValidators(Validators.required); + } else { + this.importSettingsForm.controls.defaultTaxCode.clearValidators(); + this.importSettingsForm.controls.defaultTaxCode.setValue(null); + } + }); + } + + private createCOAWatcher(): void { + this.importSettingsForm.controls.importCategories.valueChanges.subscribe((isImportCategoriesEnabled) => { + if (!isImportCategoriesEnabled) { + this.importSettingsForm.controls.chartOfAccountTypes.setValue(['Expense']); + } + }); + } + + private expenseFieldWatcher() { + + } + + private setupFormWatchers(): void { + this.createTaxCodeWatcher(); + this.createImportCustomerWatcher(); + this.createCOAWatcher(); + this.expenseFieldWatcher(); + const expenseFieldArray = this.importSettingsForm.get('expenseFields') as FormArray; + expenseFieldArray.controls.forEach((control:any) => { + control.valueChanges.subscribe((value: { source_field: string; destination_field: string; }) => { + if (value.source_field === 'custom_field') { + this.initializeCustomFieldForm(true); + this.customFieldType = ''; + this.customFieldControl = control; + this.customFieldControl.patchValue({ + source_field: '', + destination_field: control.value.destination_field, + import_to_fyle: control.value.import_to_fyle, + is_custom: control.value.is_custom, + source_placeholder: null + }); + } + }); + }); + } + + private constructPayloadAndSave() { + this.isSaveInProgress = true; + const importSettingPayload = XeroImportSettingModel.constructPayload(this.importSettingsForm); + this.importSettingService.postImportSettings(importSettingPayload).subscribe(() => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.SUCCESS, 'Import settings saved successfully'); + + if (this.isOnboarding) { + this.workspaceService.setOnboardingState(XeroOnboardingState.ADVANCED_CONFIGURATION); + this.router.navigate([`/integrations/xero/onboarding/advanced_settings`]); + } + }, () => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Error saving import settings, please try again later'); + }); + } + + save(): void { + if (this.importSettingsForm.valid) { + this.constructPayloadAndSave(); + } + } + + private createImportCustomerWatcher(): void { + if (brandingConfig.brandId === 'co') { + const formArray = this.importSettingsForm.get('expenseFields') as FormArray; + const index = formArray.value.findIndex((data:any) => data.destination_field === XeroFyleField.CUSTOMER); + formArray.controls.at(index)?.get('import_to_fyle')?.valueChanges.subscribe((isCustomerImportEnabled) => { + if (isCustomerImportEnabled) { + formArray.controls.at(index)?.get('source_field')?.patchValue(XeroFyleField.PROJECT); + this.importSettingsForm.controls.importCustomers.patchValue(true); + } else { + formArray.controls.at(index)?.get('source_field')?.patchValue('DISABLED_XERO_SOURCE_FIELD'); + this.importSettingsForm.controls.importCustomers.patchValue(false); + } + }); + } else { + this.importSettingsForm.controls.importCustomers.valueChanges.subscribe((isCustomerImportEnabled) => { + if (isCustomerImportEnabled) { + this.fyleExpenseFields = this.fyleExpenseFields.filter((field) => field.attribute_type !== XeroFyleField.PROJECT); + } else { + const fyleField = this.fyleExpenseFields.filter((field) => field.attribute_type === XeroFyleField.PROJECT); + if (fyleField.length === 0) { + this.fyleExpenseFields.pop(); + this.fyleExpenseFields.push({ attribute_type: XeroFyleField.PROJECT, display_name: 'Project', is_dependent: false }); + this.fyleExpenseFields.push(this.customFieldOption[0]); + } + } + }); + } + } + + setupPage() { + this.isOnboarding = this.router.url.includes('onboarding'); + forkJoin([ + this.importSettingService.getImportSettings(), + this.mappingService.getFyleFields('v1'), + this.importSettingService.getXeroField(), + this.mappingService.getDestinationAttributes(XeroFyleField.TAX_CODE, 'v1', 'xero'), + this.workspaceService.getWorkspaceGeneralSettings(), + this.xeroConnectorService.getXeroCredentials(this.workspaceService.getWorkspaceId()) + ]).subscribe(response => { + this.importSettings = response[0]; + this.fyleExpenseFields = response[1]; + this.xeroExpenseFields = response[2]; + this.taxCodes = response[3]; + this.workspaceGeneralSettings = response[4]; + + this.isCustomerPresent = this.xeroExpenseFields.findIndex((data:IntegrationField) => data.attribute_type === XeroFyleField.CUSTOMER) !== -1 ? true : false; + + this.importSettingsForm = XeroImportSettingModel.mapAPIResponseToFormGroup(this.importSettings, this.xeroExpenseFields, this.isCustomerPresent); + + if (response[5] && response[5].country !== 'US') { + this.isTaxGroupSyncAllowed = true; + } + + // This is only for Fyle + if (brandingConfig.brandId !== 'co') { + this.xeroExpenseFields = this.xeroExpenseFields.filter((data) => data.attribute_type !== XeroFyleField.CUSTOMER); + } + + this.isProjectMapped = this.importSettings.mapping_settings.findIndex((data) => data.source_field === XeroFyleField.PROJECT && data.destination_field !== XeroFyleField.CUSTOMER) !== -1 ? true : false; + + this.fyleExpenseFields.push({ attribute_type: 'custom_field', display_name: 'Create a Custom Field', is_dependent: true }); + this.setupFormWatchers(); + this.initializeCustomFieldForm(false); + + this.isLoading = false; + }); + } + + updateCustomerImportAvailability(isMapped: boolean) { + this.isProjectMapped = isMapped; + } ngOnInit(): void { + this.setupPage(); } } diff --git a/src/app/integrations/xero/xero-shared/xero-shared.module.ts b/src/app/integrations/xero/xero-shared/xero-shared.module.ts index 3b75313b4..3cb6c47d9 100644 --- a/src/app/integrations/xero/xero-shared/xero-shared.module.ts +++ b/src/app/integrations/xero/xero-shared/xero-shared.module.ts @@ -4,6 +4,7 @@ import { XeroAdvancedSettingsComponent } from './xero-advanced-settings/xero-adv import { XeroExportSettingsComponent } from './xero-export-settings/xero-export-settings.component'; import { XeroImportSettingsComponent } from './xero-import-settings/xero-import-settings.component'; import { SharedModule } from 'src/app/shared/shared.module'; +import { MultiSelectModule } from 'primeng/multiselect'; @NgModule({ declarations: [ @@ -13,7 +14,8 @@ import { SharedModule } from 'src/app/shared/shared.module'; ], imports: [ CommonModule, - SharedModule + SharedModule, + MultiSelectModule ], exports: [ XeroExportSettingsComponent, diff --git a/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.html b/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.html index a47007587..3441b6160 100644 --- a/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.html +++ b/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.html @@ -47,7 +47,7 @@
- + @@ -76,8 +76,8 @@
- +
@@ -90,7 +90,7 @@
+

diff --git a/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.ts b/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.ts index 0a9c727e1..fee783dcf 100644 --- a/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.ts +++ b/src/app/shared/components/configuration/configuration-import-field/configuration-import-field.component.ts @@ -3,7 +3,7 @@ import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; import { brandingConfig, brandingFeatureConfig } from 'src/app/branding/branding-config'; import { ImportDefaultField, ImportSettingMappingRow, ImportSettingsCustomFieldRow, ImportSettingsModel } from 'src/app/core/models/common/import-settings.model'; import { FyleField, IntegrationField } from 'src/app/core/models/db/mapping.model'; -import { AppName, MappingSourceField } from 'src/app/core/models/enum/enum.model'; +import { AppName, MappingSourceField, XeroFyleField } from 'src/app/core/models/enum/enum.model'; import { Sage300DefaultFields, Sage300DependentImportFields, Sage300ImportSettingModel } from 'src/app/core/models/sage300/sage300-configuration/sage300-import-settings.model'; import { MappingSetting } from 'src/app/core/models/intacct/intacct-configuration/import-settings.model'; import { HelperService } from 'src/app/core/services/common/helper.service'; @@ -50,12 +50,16 @@ export class ConfigurationImportFieldComponent implements OnInit { AppName = AppName; + isXeroProjectMapped: boolean; + readonly brandingConfig = brandingConfig; readonly brandingFeatureConfig = brandingFeatureConfig; readonly isAsterikAllowed: boolean = brandingFeatureConfig.isAsterikAllowed; + @Output() xeroProjectMapping:EventEmitter = new EventEmitter(); + constructor( public windowService: WindowService ) { } @@ -105,6 +109,23 @@ export class ConfigurationImportFieldComponent implements OnInit { } else { (this.form.get('expenseFields') as FormArray).at(index)?.get('import_to_fyle')?.setValue(true); } + + if (selectedValue === MappingSourceField.PROJECT && (this.form.get('expenseFields') as FormArray).at(index)?.get('source_field')?.value !== XeroFyleField.CUSTOMER && this.appName === AppName.XERO) { + this.isXeroProjectMapped = true; + this.xeroProjectMapping.emit(this.isXeroProjectMapped); + } else { + this.isXeroProjectMapped = false; + this.xeroProjectMapping.emit(this.isXeroProjectMapped); + } + } + + getOptions(expenseField: AbstractControl): FyleField[]{ + if (expenseField.value.destination_field === 'CUSTOMER' && this.appName === AppName.XERO && !expenseField.value.import_to_fyle) { + return this.filteredFyleFields; + } else if (expenseField.value.source_field === 'CATEGORY') { + return this.fyleFieldOptions; + } + return this.fyleFieldOptions; } removeFilter(expenseField: AbstractControl) { @@ -112,6 +133,8 @@ export class ConfigurationImportFieldComponent implements OnInit { (expenseField as FormGroup).controls.import_to_fyle.patchValue(false); (expenseField as FormGroup).controls.import_to_fyle.enable(); event?.stopPropagation(); + this.isXeroProjectMapped = false; + this.xeroProjectMapping.emit(this.isXeroProjectMapped); } onShowWarningForDependentFields(event: any, formGroup: AbstractControl): void { @@ -132,7 +155,7 @@ export class ConfigurationImportFieldComponent implements OnInit { } ngOnInit(): void { - this.filteredFyleFields = this.fyleFieldOptions.filter(option => option.attribute_type !== 'CATEGORY'); + this.filteredFyleFields = this.appName !== AppName.XERO ? this.fyleFieldOptions.filter(option => option.attribute_type !== 'CATEGORY') : [{ attribute_type: 'DISABLED_XERO_SOURCE_FIELD', display_name: 'Project', is_dependent: false }]; } } diff --git a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.html b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.html index 5cbb934a1..1c087c0ed 100644 --- a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.html +++ b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.html @@ -22,6 +22,6 @@

- +
diff --git a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.ts b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.ts index 4438c1277..924c295e7 100644 --- a/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.ts +++ b/src/app/shared/components/configuration/configuration-toggle-field/configuration-toggle-field.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { brandingConfig, brandingContent, brandingFeatureConfig } from 'src/app/branding/branding-config'; +import { AppName } from 'src/app/core/models/enum/enum.model'; import { WindowService } from 'src/app/core/services/common/window.service'; @Component({ @@ -28,6 +29,12 @@ export class ConfigurationToggleFieldComponent implements OnInit { @Input() hideToggle: boolean = false; + @Input() disabled: boolean = false; + + @Input() appName: string; + + AppName = AppName; + readonly brandingFeatureConfig = brandingFeatureConfig; readonly isAsterikAllowed: boolean = brandingFeatureConfig.isAsterikAllowed; @@ -36,6 +43,8 @@ export class ConfigurationToggleFieldComponent implements OnInit { readonly brandingConfig = brandingConfig; + readonly brandingXeroContent = brandingContent.xero.configuration.importSetting.toggleToastMessage; + constructor( public windowService: WindowService ) { }