diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index c6017804d..2297000c9 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -91,7 +91,8 @@ const kbArticles: KbArticle = { INTACCT: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle`, TRAVELPERK: `${brandingConfig.helpArticleDomain}/en/articles/7549535-how-are-travelperk-invoices-created-as-expenses-in-fyle`, SAGE300: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle`, - BUSINESS_CENTRAL: `${brandingConfig.helpArticleDomain}/en/articles/8911018-how-to-configure-the-fyle-dynamics-365-business-central-integration` + BUSINESS_CENTRAL: `${brandingConfig.helpArticleDomain}/en/articles/8911018-how-to-configure-the-fyle-dynamics-365-business-central-integration`, + XERO: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle#xero-2-0` }, onboardingArticles: { INTACCT: { @@ -133,6 +134,15 @@ const kbArticles: KbArticle = { PAYMENT_PROFILE_SETTINGS: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration#h_0f8ebdfa10', ADVANCED_SETTING: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration#h_281acb3026', LANDING: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration' + }, + // TODO + XERO: { + LANDING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration`, + CONNECTOR: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_e3ade308dc`, + EMPLOYEE_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6208620-how-to-set-up-the-fyle-xero-integration-v2-0#h_d70f1d54cc`, + EXPORT_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_ad07470d98`, + IMPORT_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_04d289fd42`, + ADVANCED_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_d95b791edd` } } }, @@ -145,7 +155,8 @@ const kbArticles: KbArticle = { NETSUITE: `${brandingConfig.helpArticleDomain}/en/articles/4424242-fyle-netsuite-integration`, TRAVELPERK: `${brandingConfig.helpArticleDomain}/en/articles/7549535-how-are-travelperk-invoices-created-as-expenses-in-fyle`, SAGE300: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle`, - BUSINESS_CENTRAL: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle` + BUSINESS_CENTRAL: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle`, + XERO: `${brandingConfig.helpArticleDomain}/en/collections/215867-integrations-with-fyle` }, onboardingArticles: { INTACCT: { @@ -186,6 +197,15 @@ const kbArticles: KbArticle = { PAYMENT_PROFILE_SETTINGS: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration#h_0f8ebdfa10', ADVANCED_SETTING: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration#h_281acb3026', LANDING: 'https://help.fylehq.com/en/articles/7193187-how-to-set-up-the-fyle-travelperk-integration' + }, + // TODO + XERO: { + LANDING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration`, + CONNECTOR: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_e3ade308dc`, + EMPLOYEE_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6208620-how-to-set-up-the-fyle-xero-integration-v2-0#h_d70f1d54cc`, + EXPORT_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_ad07470d98`, + IMPORT_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_04d289fd42`, + ADVANCED_SETTING: `${brandingConfig.helpArticleDomain}/en/articles/6721333-how-to-set-up-the-fyle-xero-integration-v2-0#h_d95b791edd` } } } @@ -203,7 +223,8 @@ const demoVideoLinks: DemoVideo = { QBO: 'https://www.youtube.com/embed/b63lS2DG5j4', // TODO: Update link for MS Dynamics BUSINESS_CENTRAL: 'https://www.youtube.com/embed/2oYdc8KcQnk', - TRAVELPERK: 'https://www.youtube.com/embed/2oYdc8KcQnk' + TRAVELPERK: 'https://www.youtube.com/embed/2oYdc8KcQnk', + XERO: 'https://www.youtube.com/embed/IplJd7tGWBk' } }, co: { @@ -214,7 +235,8 @@ const demoVideoLinks: DemoVideo = { QBO: 'https://www.youtube.com/embed/b63lS2DG5j4', // TODO: Update link for MS Dynamics BUSINESS_CENTRAL: 'https://www.youtube.com/embed/2oYdc8KcQnk', - TRAVELPERK: 'https://www.youtube.com/embed/2oYdc8KcQnk' + TRAVELPERK: 'https://www.youtube.com/embed/2oYdc8KcQnk', + XERO: 'https://www.youtube.com/embed/IplJd7tGWBk' } } }; @@ -225,6 +247,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: ' Export Corporate Card Expenses', + 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. ', @@ -404,6 +493,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: 'Export Corporate Card Expenses', + 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..890515d7c 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/landing'); } 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 1ee0c5a63..58d755fdd 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/branding/demo-video.model.ts b/src/app/core/models/branding/demo-video.model.ts index 700e3f9db..3225b7503 100644 --- a/src/app/core/models/branding/demo-video.model.ts +++ b/src/app/core/models/branding/demo-video.model.ts @@ -6,6 +6,7 @@ export type DemoVideo = { QBO: string; BUSINESS_CENTRAL: string; TRAVELPERK: string; + XERO:string; } } }; diff --git a/src/app/core/models/branding/kb-article.model.ts b/src/app/core/models/branding/kb-article.model.ts index 4df62828e..5000d7b60 100644 --- a/src/app/core/models/branding/kb-article.model.ts +++ b/src/app/core/models/branding/kb-article.model.ts @@ -9,6 +9,7 @@ export type KbArticle = { TRAVELPERK: string; SAGE300: string; BUSINESS_CENTRAL: string; + XERO: string; }, onboardingArticles: { INTACCT: { @@ -49,7 +50,15 @@ export type KbArticle = { PAYMENT_PROFILE_SETTINGS: string; ADVANCED_SETTING: string; LANDING: string; - } + }, + XERO: { + LANDING: string; + CONNECTOR: string; + EMPLOYEE_SETTING: string; + EXPORT_SETTING: string; + IMPORT_SETTING: string; + ADVANCED_SETTING: string; + }, } } }; diff --git a/src/app/core/models/enum/enum.model.ts b/src/app/core/models/enum/enum.model.ts index aafb0060e..90dbde902 100644 --- a/src/app/core/models/enum/enum.model.ts +++ b/src/app/core/models/enum/enum.model.ts @@ -248,9 +248,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 { @@ -338,6 +345,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', @@ -655,6 +669,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-destination-attribute.model.ts b/src/app/core/models/xero/db/xero-destination-attribute.model.ts new file mode 100644 index 000000000..2a150f0a2 --- /dev/null +++ b/src/app/core/models/xero/db/xero-destination-attribute.model.ts @@ -0,0 +1,11 @@ +import { DestinationAttribute, GroupedDestinationAttribute } from "../../db/destination-attribute.model"; + +export type DestinationAttributeDetail = { + email: string; + fully_qualified_name: string; +}; + +export interface XeroDestinationAttributes extends DestinationAttribute { + auto_created: boolean; + detail: DestinationAttributeDetail | null; +} 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/clone-setting.model.ts b/src/app/core/models/xero/xero-configuration/clone-setting.model.ts index 0a5649c26..74701e800 100644 --- a/src/app/core/models/xero/xero-configuration/clone-setting.model.ts +++ b/src/app/core/models/xero/xero-configuration/clone-setting.model.ts @@ -19,17 +19,15 @@ export type XeroCloneSettingPost = { } export class XeroCloneSettingModel { - static constructPayload(XeroCloneSettingsForm: FormGroup, customMappingSettings: MappingSetting[]): XeroCloneSettingPost { - const exportSettingPayload = XeroExportSettingModel.constructPayload(XeroCloneSettingsForm); - const importSettingPayload = XeroImportSettingModel.constructPayload(XeroCloneSettingsForm, customMappingSettings); - const advancedSettingPayload = XeroAdvancedSettingModel.constructPayload(XeroCloneSettingsForm); + static constructPayload(exportSettingForm: FormGroup, importSettingForm: FormGroup, advancedSettingForm: FormGroup): XeroCloneSettingPost { + const exportSettingPayload = XeroExportSettingModel.constructPayload(exportSettingForm); + const importSettingPayload = XeroImportSettingModel.constructPayload(importSettingForm); + const advancedSettingPayload = XeroAdvancedSettingModel.constructPayload(advancedSettingForm); - const XeroCloneSettingPayload: XeroCloneSettingPost = { + return { export_settings: exportSettingPayload, import_settings: importSettingPayload, advanced_settings: advancedSettingPayload }; - - return XeroCloneSettingPayload; } } 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..9da0fcb43 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 ? true : 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..df34b279f 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,17 +57,47 @@ 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 COA = importSettingsForm.get('chartOfAccountTypes')?.value.map((name: string) => name.toUpperCase()); + 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, - charts_of_accounts: importSettingsForm.get('chartOfAccount')?.value ? chartOfAccounts : ['Expense'], + import_categories: importSettingsForm.get('importCategories')?.value ?? false, + charts_of_accounts: importSettingsForm.get('chartOfAccountTypes')?.value ? COA : ['EXPENSE'], import_tax_codes: importSettingsForm.get('taxCode')?.value, import_suppliers_as_merchants: importSettingsForm.get('importSuppliersAsMerchants')?.value, import_customers: importSettingsForm.get('importCustomers')?.value ? importSettingsForm.get('importCustomers')?.value : false @@ -78,8 +109,4 @@ export class XeroImportSettingModel extends ImportSettingsModel { }; return importSettingPayload; } - - static formatChartOfAccounts(chartOfAccounts: {enabled: boolean, name: string}[]): string[] { - return chartOfAccounts.filter(chartOfAccount => chartOfAccount.enabled).map(chartOfAccount => chartOfAccount.name.toUpperCase()); - } } 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/common/clone-setting.service.ts b/src/app/core/services/common/clone-setting.service.ts index 1b96d326a..5680eb8ed 100644 --- a/src/app/core/services/common/clone-setting.service.ts +++ b/src/app/core/services/common/clone-setting.service.ts @@ -4,6 +4,7 @@ import { ApiService } from './api.service'; import { WorkspaceService } from './workspace.service'; import { CloneSettingExist } from '../../models/common/clone-setting.model'; import { QBOCloneSetting, QBOCloneSettingPost } from '../../models/qbo/qbo-configuration/qbo-clone-setting.model'; +import { XeroCloneSetting, XeroCloneSettingPost } from '../../models/xero/xero-configuration/clone-setting.model'; @Injectable({ providedIn: 'root' @@ -22,11 +23,11 @@ export class CloneSettingService { return this.apiService.get(`/user/clone_settings/exists/`, {}); } - getCloneSettings(): Observable { + getCloneSettings(): Observable { return this.apiService.get(`/v2/workspaces/${this.workspaceId}/clone_settings/`, {}); } - postCloneSettings(cloneSettingsPayload: QBOCloneSettingPost): Observable { + postCloneSettings(cloneSettingsPayload: QBOCloneSettingPost | XeroCloneSettingPost): Observable { return this.apiService.put(`/v2/workspaces/${this.workspaceId}/clone_settings/`, cloneSettingsPayload); } } diff --git a/src/app/core/services/common/helper.service.ts b/src/app/core/services/common/helper.service.ts index 1511bf7f5..b017e6009 100644 --- a/src/app/core/services/common/helper.service.ts +++ b/src/app/core/services/common/helper.service.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { ApiService } from './api.service'; import { environment } from 'src/environments/environment'; import { AppUrlMap } from '../../models/integrations/integrations.model'; -import { AppUrl, BusinessCentralExportType, ExpenseState, FyleField, ProgressPhase, Sage300ExportType } from '../../models/enum/enum.model'; +import { AppUrl, BusinessCentralExportType, ExpenseGroupingFieldOption, ExpenseState, FyleField, ProgressPhase, Sage300ExportType, XeroCorporateCreditCardExpensesObject, XeroReimbursableExpensesObject } from '../../models/enum/enum.model'; import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { ExportModuleRule, ExportSettingValidatorRule } from '../../models/sage300/sage300-configuration/sage300-export-setting.model'; import { TitleCasePipe } from '@angular/common'; @@ -85,6 +85,16 @@ export class HelperService { form.controls[controllerName].setValue(Sage300ExportType.PURCHASE_INVOICE); } + setXeroExportTypeControllerValue(form: FormGroup, controllerName: string): void { + if (controllerName === 'reimbursableExportType') { + form.controls[controllerName].patchValue(XeroReimbursableExpensesObject.PURCHASE_BILL); + form.controls.reimbursableExportGroup.patchValue(ExpenseGroupingFieldOption.CLAIM_NUMBER); + } else { + form.controls[controllerName].patchValue(XeroCorporateCreditCardExpensesObject.BANK_TRANSACTION); + form.controls.creditCardExportGroup.patchValue(ExpenseGroupingFieldOption.EXPENSE_ID); + } + } + enableFormField(form: FormGroup, controllerName: string): void { form.controls[controllerName].enable(); } @@ -104,6 +114,8 @@ export class HelperService { const urlSplit = this.router.url.split('/'); if (urlSplit[2] === AppUrl.SAGE300 && (controllerName === 'cccExportType' || controllerName === 'reimbursableExportType')) { this.setSage300ExportTypeControllerValue(form, controllerName); + } else if (urlSplit[2] === AppUrl.XERO && (controllerName === 'creditCardExportType' || controllerName === 'reimbursableExportType')) { + this.setXeroExportTypeControllerValue(form, controllerName); } }); } else { 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..2d7381fa0 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(); @@ -50,7 +50,7 @@ export class XeroConnectorService { } getXeroTenants(): Observable { - return this.apiService.get(`/workspaces/${this.workspaceId}/xero/tenants/`, {attribute_type: 'TENANT'}); + return this.apiService.get(`/workspaces/${this.workspaceId}/xero/tenants/`, {attribute_type__exact: 'TENANT'}); } postXeroTenants(): Observable { 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-main/xero-configuration/xero-configuration.component.html b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.html index 387638c4b..30cee6ef6 100644 --- a/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.html +++ b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.html @@ -1 +1,10 @@ -

xero-configuration works!

+
+
+
+
+ +
+
+ +
+
diff --git a/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.ts b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.ts index e264dfbd1..2f71cdd42 100644 --- a/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.ts +++ b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from '@angular/core'; +import { MenuItem } from 'primeng/api'; +import { brandingConfig, brandingContent, brandingFeatureConfig } from 'src/app/branding/branding-config'; @Component({ selector: 'app-xero-configuration', @@ -7,6 +9,20 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroConfigurationComponent implements OnInit { + readonly brandingContent = brandingContent.xero.configuration; + + modules: MenuItem[] = [ + {label: this.brandingContent.exportSetting.stepName, routerLink: '/integrations/xero/main/configuration/export_settings'}, + {label: this.brandingContent.importSetting.stepName, routerLink: '/integrations/xero/main/configuration/import_settings'}, + {label: this.brandingContent.advancedSettings.stepName, routerLink: '/integrations/xero/main/configuration/advanced_settings'} + ]; + + activeModule: MenuItem = this.modules[0]; + + readonly isGradientAllowed: boolean = brandingFeatureConfig.isGradientAllowed; + + readonly brandingConfig = brandingConfig; + constructor() { } ngOnInit(): void { diff --git a/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.module.ts b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.module.ts index e6059944a..8c01c16d7 100644 --- a/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.module.ts +++ b/src/app/integrations/xero/xero-main/xero-configuration/xero-configuration.module.ts @@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common'; import { XeroConfigurationRoutingModule } from './xero-configuration-routing.module'; import { XeroConfigurationComponent } from './xero-configuration.component'; import { XeroSharedModule } from '../../xero-shared/xero-shared.module'; +import { SharedModule } from 'src/app/shared/shared.module'; @NgModule({ @@ -13,6 +14,7 @@ import { XeroSharedModule } from '../../xero-shared/xero-shared.module'; imports: [ CommonModule, XeroSharedModule, + SharedModule, XeroConfigurationRoutingModule ] }) diff --git a/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.html b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.html new file mode 100644 index 000000000..086364e0f --- /dev/null +++ b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.html @@ -0,0 +1,404 @@ + +
+ +
+ +
+
+ + +
+
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+ + +
+
+ +
+
+ + + +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + +
+
+ + + +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+
+ + + +
+ + +
+
+ + + +
+ + +
+ +
+
+ +
+

+ Send Error Notification to + +

+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+
+ +
+ + +
+
+ + + + + + + + + + +
+
\ No newline at end of file diff --git a/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.scss b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.scss new file mode 100644 index 000000000..c5d9724aa --- /dev/null +++ b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.scss @@ -0,0 +1,25 @@ +.clone-setting { + &--field-section { + @apply tw-px-24-px; + } + + &--field { + @apply tw-p-24-px; + } + + &--export-setting-header { + @apply tw-flex tw-justify-between; + } + + &--dependent-field { + @apply tw-pt-24-px; + } +} + +:host ::ng-deep .reimbursableExpense .clone-setting-field--label-text, :host ::ng-deep .creditCardExpense .clone-setting-field--label-text { + @apply tw-text-16-px tw-text-text-primary tw-pl-0 #{!important}; +} + +:host ::ng-deep p-dropdown .p-disabled { + @apply tw-text-input-read-only-text tw-bg-select-disabled-bg tw-border-select-disabled-border tw-border-solid tw-opacity-100 #{!important}; +} diff --git a/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.spec.ts b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.spec.ts new file mode 100644 index 000000000..f56b788bc --- /dev/null +++ b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { XeroCloneSettingsComponent } from './xero-clone-settings.component'; + +describe('XeroCloneSettingsComponent', () => { + let component: XeroCloneSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ XeroCloneSettingsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(XeroCloneSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.ts b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.ts new file mode 100644 index 000000000..abc3b07d1 --- /dev/null +++ b/src/app/integrations/xero/xero-onboarding/xero-clone-settings/xero-clone-settings.component.ts @@ -0,0 +1,376 @@ +import { Component, OnInit } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { forkJoin } from 'rxjs'; +import { brandingConfig, brandingContent } from 'src/app/branding/branding-config'; +import { ExportSettingModel } from 'src/app/core/models/common/export-settings.model'; +import { ExpenseField, ImportSettingsModel } from 'src/app/core/models/common/import-settings.model'; +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 { FyleField, IntegrationField } from 'src/app/core/models/db/mapping.model'; +import { AppName, ConfigurationCta, ConfigurationWarningEvent, InputType, ToastSeverity, XeroFyleField } 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 { EmailOptions } from 'src/app/core/models/qbd/qbd-configuration/advanced-setting.model'; +import { XeroCloneSetting, XeroCloneSettingModel } from 'src/app/core/models/xero/xero-configuration/clone-setting.model'; +import { XeroAdvancedSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-advanced-settings.model'; +import { XeroExportSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-export-settings.model'; +import { XeroImportSettingModel } from 'src/app/core/models/xero/xero-configuration/xero-import-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 { ConfigurationService } from 'src/app/core/services/common/configuration.service'; +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 { 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 { XeroImportSettingsService } from 'src/app/core/services/xero/xero-configuration/xero-import-settings.service'; + +@Component({ + selector: 'app-xero-clone-settings', + templateUrl: './xero-clone-settings.component.html', + styleUrls: ['./xero-clone-settings.component.scss'] +}) +export class XeroCloneSettingsComponent implements OnInit { + + isLoading: boolean = true; + + onboardingSteps: OnboardingStepper[] = []; + + brandingConfig = brandingConfig; + + bankAccounts: DefaultDestinationAttribute[]; + + reimbursableExportTypes = 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; + + xeroFields: IntegrationField[]; + + fyleFields: FyleField[]; + + cloneSetting: XeroCloneSetting; + + appName: AppName = AppName.XERO; + + InputType = InputType; + + customFieldOption: ExpenseField[] = ImportSettingsModel.getCustomFieldOption(); + + chartOfAccountTypesList: string[] = XeroImportSettingModel.getChartOfAccountTypesList().map((name: string) => name[0]+name.substr(1).toLowerCase()); + + isTaxGroupSyncAllowed: boolean; + + isProjectMapped: boolean; + + isCustomerPresent: boolean; + + taxCodes: DefaultDestinationAttribute[]; + + importSettingForm: FormGroup; + + paymentSyncOptions: SelectFormOption[] = XeroAdvancedSettingModel.getPaymentSyncOptions(); + + scheduleIntervalHours: SelectFormOption[] = [...Array(24).keys()].map(day => { + return { + label: (day + 1).toString(), + value: day + 1 + }; + }); + + advancedSettingForm: FormGroup; + + adminEmails: EmailOptions[]; + + billPaymentAccounts: DefaultDestinationAttribute[]; + + isSaveInProgress: boolean; + + ConfigurationCtaText = ConfigurationCta; + + isWarningDialogVisible: boolean; + + warningEvent: ConfigurationWarningEvent; + + warningHeaderText: string; + + warningContextText: string; + + primaryButtonText: string; + + customFieldForm: FormGroup = this.formBuilder.group({ + attribute_type: ['', Validators.required], + display_name: [''], + source_placeholder: ['', Validators.required] + }); + + showCustomFieldDialog: boolean; + + isPreviewDialogVisible: boolean; + + customField: { attribute_type: any; display_name: any; source_placeholder: any; is_dependent: boolean; }; + + customFieldControl: any; + + customFieldType: string; + + brandingContent = brandingContent; + + constructor( + private cloneSettingService: CloneSettingService, + private configurationService: ConfigurationService, + private formBuilder: FormBuilder, + private exportSettingService: XeroExportSettingsService, + public helperService: HelperService, + private mappingService: MappingService, + private xeroConnectorService: XeroConnectorService, + private xeroImportSettingsService: XeroImportSettingsService, + private router: Router, + private toastService: IntegrationsToastService, + private workspaceService: WorkspaceService + ) { } + + resetCloneSetting(): void { + this.warningHeaderText = 'Are you sure?'; + this.warningContextText = `By resetting the configuration, you will be configuring each setting individually from the beginning.

Would you like to continue?`; + this.primaryButtonText = 'Yes'; + this.warningEvent = ConfigurationWarningEvent.RESET_CONFIGURATION; + + this.isWarningDialogVisible = true; + } + + acceptWarning(data: ConfigurationWarningOut): void { + this.isWarningDialogVisible = false; + if (data.hasAccepted) { + this.router.navigate([`/integrations/xero/onboarding/export_settings`]); + } + } + + navigateToPreviousStep(): void { + this.router.navigate([`/integrations/xero/onboarding/connector`]); + } + + closeModel() { + this.customFieldForm.reset(); + this.showCustomFieldDialog = false; + } + + showPreviewDialog(visible: boolean) { + this.isPreviewDialogVisible = visible; + } + + closeDialog() { + this.isPreviewDialogVisible = false; + } + + 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.fyleFields.pop(); + this.fyleFields.push(this.customField); + this.fyleFields.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.importSettingForm.get('expenseFields') as FormArray).controls.filter(field => field.value.destination_field === this.customFieldControl.value.destination_field)[0].patchValue(expenseField); + ((this.importSettingForm.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; + } + } + + save(): void { + this.isSaveInProgress = true; + const cloneSettingPayload = XeroCloneSettingModel.constructPayload(this.exportSettingForm, this.importSettingForm, this.advancedSettingForm); + + this.cloneSettingService.postCloneSettings(cloneSettingPayload).subscribe((response) => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.SUCCESS, 'Cloned settings successfully'); + this.router.navigate([`/integrations/xero/onboarding/done`]); + }, () => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Failed to clone settings'); + }); + + } + + private setupOnboardingSteps(): void { + const onboardingSteps = new XeroOnboardingModel().getOnboardingSteps('Clone Settings', this.workspaceService.getOnboardingState()); + this.onboardingSteps.push(onboardingSteps[0]); + this.onboardingSteps.push({ + active: false, + completed: false, + step: 'Clone Settings', + icon: 'gear-medium', + route: '/integrations/xero/onboarding/clone_settings', + styleClasses: ['step-name-export--text'] + }); + } + + private initializeCustomFieldForm(shouldShowDialog: boolean) { + this.customFieldForm.reset(); + this.showCustomFieldDialog = shouldShowDialog; + } + + private createTaxCodeWatcher(): void { + this.importSettingForm.controls.taxCode.valueChanges.subscribe((isTaxCodeEnabled) => { + if (isTaxCodeEnabled) { + this.importSettingForm.controls.defaultTaxCode.setValidators(Validators.required); + } else { + this.importSettingForm.controls.defaultTaxCode.clearValidators(); + this.importSettingForm.controls.defaultTaxCode.setValue(null); + } + }); + } + + private createCOAWatcher(): void { + this.importSettingForm.controls.importCategories.valueChanges.subscribe((isImportCategoriesEnabled) => { + if (!isImportCategoriesEnabled) { + this.importSettingForm.controls.chartOfAccountTypes.setValue(['Expense']); + } + }); + } + + private createImportCustomerWatcher(): void { + this.importSettingForm.controls.importCustomers.valueChanges.subscribe((isCustomerImportEnabled) => { + if (isCustomerImportEnabled) { + this.fyleFields = this.fyleFields.filter((field) => field.attribute_type !== XeroFyleField.PROJECT); + } else { + const fyleField = this.fyleFields.filter((field) => field.attribute_type === XeroFyleField.PROJECT); + if (fyleField.length === 0) { + this.fyleFields.pop(); + this.fyleFields.push({ attribute_type: XeroFyleField.PROJECT, display_name: 'Project', is_dependent: false }); + this.fyleFields.push(this.customFieldOption[0]); + } + } + }); + } + + private setupImportSettingFormWatcher(): void { + this.createTaxCodeWatcher(); + this.createCOAWatcher(); + this.createImportCustomerWatcher(); + const expenseFieldArray = this.importSettingForm.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 + }); + } + }); + }); + } + + setupAdvancedSettingFormWatcher() { + XeroAdvancedSettingModel.setConfigurationSettingValidatorsAndWatchers(this.advancedSettingForm); + } + + updateCustomerImportAvailability(isMapped: boolean) { + this.isProjectMapped = isMapped; + } + + private setupPage(): void { + this.setupOnboardingSteps(); + const destinationAttributes = [ + XeroFyleField.TAX_CODE, XeroFyleField.BANK_ACCOUNT + ]; + + forkJoin([ + this.cloneSettingService.getCloneSettings(), + this.mappingService.getGroupedDestinationAttributes(destinationAttributes, 'v1', 'xero'), + this.mappingService.getFyleFields('v1'), + this.xeroConnectorService.getXeroCredentials(this.workspaceService.getWorkspaceId()), + this.configurationService.getAdditionalEmails(), + this.xeroImportSettingsService.getXeroField() + ]).subscribe(([cloneSetting, destinationAttributes, fyleFieldsResponse, xeroCredentials, adminEmails, xeroFields]) => { + this.cloneSetting = cloneSetting; + + // Export Settings + this.bankAccounts = destinationAttributes.BANK_ACCOUNT.map((option: DestinationAttribute) => ExportSettingModel.formatGeneralMappingPayload(option)); + + this.reimbursableExportTypes = XeroExportSettingModel.getReimbursableExportTypes(); + this.exportSettingForm = XeroExportSettingModel.mapAPIResponseToFormGroup(cloneSetting.export_settings); + + this.helperService.addExportSettingFormValidator(this.exportSettingForm); + const [exportSettingValidatorRule, exportModuleRule] = XeroExportSettingModel.getValidators(); + + this.helperService.setConfigurationSettingValidatorsAndWatchers(exportSettingValidatorRule, this.exportSettingForm); + + this.helperService.setExportTypeValidatorsAndWatchers(exportModuleRule, this.exportSettingForm); + + // Import Settings + this.xeroFields = xeroFields; + this.taxCodes = destinationAttributes.TAX_CODE.map((option: DestinationAttribute) => ExportSettingModel.formatGeneralMappingPayload(option)); + + if (xeroCredentials && xeroCredentials.country !== 'US') { + this.isTaxGroupSyncAllowed = true; + } + + this.isCustomerPresent = this.xeroFields.findIndex((data:IntegrationField) => data.attribute_type === XeroFyleField.CUSTOMER) !== -1 ? true : false; + + this.xeroFields = this.xeroFields.filter((data) => data.attribute_type !== XeroFyleField.CUSTOMER); + + cloneSetting.import_settings.workspace_general_settings.charts_of_accounts = cloneSetting.import_settings.workspace_general_settings.charts_of_accounts.map((name: string) => name[0]+name.substr(1).toLowerCase()); + + this.isProjectMapped = cloneSetting.import_settings.mapping_settings.findIndex((data: { source_field: XeroFyleField; destination_field: XeroFyleField; }) => data.source_field === XeroFyleField.PROJECT && data.destination_field !== XeroFyleField.CUSTOMER) !== -1 ? true : false; + + this.importSettingForm = XeroImportSettingModel.mapAPIResponseToFormGroup(cloneSetting.import_settings, this.xeroFields, this.isCustomerPresent); + this.fyleFields = fyleFieldsResponse; + this.fyleFields.push({ attribute_type: 'custom_field', display_name: 'Create a Custom Field', is_dependent: true }); + this.setupImportSettingFormWatcher(); + this.initializeCustomFieldForm(false); + + // Advanced Settings + this.adminEmails = adminEmails; + if (this.cloneSetting.advanced_settings.workspace_schedules?.additional_email_options && this.cloneSetting.advanced_settings.workspace_schedules?.additional_email_options.length > 0) { + this.adminEmails = this.adminEmails.concat(this.cloneSetting.advanced_settings.workspace_schedules?.additional_email_options).flat(); + } + + this.billPaymentAccounts = destinationAttributes.BANK_ACCOUNT.map((option: DestinationAttribute) => ExportSettingModel.formatGeneralMappingPayload(option)); + this.advancedSettingForm = XeroAdvancedSettingModel.mapAPIResponseToFormGroup(this.cloneSetting.advanced_settings, this.adminEmails); + this.setupAdvancedSettingFormWatcher(); + + this.isLoading = false; + }); + } + + ngOnInit(): void { + this.setupPage(); + } + +} 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..542a7bf4f 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.xeroCompanyName) { + this.router.navigate(['/integrations/xero/onboarding/export_settings']); + return; + } + if (this.xeroTenantselected && !this.xeroCompanyName) { + 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-landing/xero-onboarding-landing.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.html index 6d618195e..79332a4a8 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.html @@ -1 +1,17 @@ -

xero-onboarding-landing works!

+
+
+ +
+
+ +
+
+ + + diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.ts index 2cd2b35ac..9bfd84628 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-landing/xero-onboarding-landing.component.ts @@ -1,15 +1,102 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { brandingConfig, brandingDemoVideoLinks, brandingKbArticles } from 'src/app/branding/branding-config'; +import { AppName, ToastSeverity, XeroOnboardingState } from 'src/app/core/models/enum/enum.model'; +import { ConfigurationWarningOut } from 'src/app/core/models/misc/configuration-warning.model'; +import { XeroCredentials } from 'src/app/core/models/xero/db/xero-credential.model'; +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 { XeroConnectorService } from 'src/app/core/services/xero/xero-configuration/xero-connector.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-landing', templateUrl: './xero-onboarding-landing.component.html', styleUrls: ['./xero-onboarding-landing.component.scss'] }) -export class XeroOnboardingLandingComponent implements OnInit { +export class XeroOnboardingLandingComponent implements OnInit, OnDestroy { - constructor() { } + isIncorrectXeroConnectedDialogVisible: boolean = false; + + appName: string = AppName.XERO; + + xeroConnectionInProgress: boolean = false; + + isIntegrationConnected: boolean = false; + + redirectLink: string = brandingKbArticles.onboardingArticles.XERO.LANDING; + + embedVideoLink = brandingDemoVideoLinks.onboarding.XERO; + + private oauthCallbackSubscription: Subscription; + + readonly brandingConfig = brandingConfig; + + constructor( + private helperService: HelperService, + private xeroConnectorService: XeroConnectorService, + private workspaceService: WorkspaceService, + private router: Router, + private xeroHelper: XeroHelperService, + private toastService: IntegrationsToastService + ) { } + + acceptWarning(data: ConfigurationWarningOut): void { + this.isIncorrectXeroConnectedDialogVisible = false; + if (data.hasAccepted) { + this.router.navigate([`/integrations/xero/onboarding/landing`]); + } + } + + private postXeroCredentials(code: string): void { + this.xeroConnectorService.connectXero(this.workspaceService.getWorkspaceId(), code).subscribe((xeroCredentials: XeroCredentials) => { + this.isIntegrationConnected = true; + this.xeroConnectionInProgress = false; + this.checkProgressAndRedirect(); + }, (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 account') { + this.isIntegrationConnected = false; + this.xeroConnectionInProgress = false; + this.isIncorrectXeroConnectedDialogVisible = true; + } else { + this.toastService.displayToastMessage(ToastSeverity.ERROR, errorMessage); + this.router.navigate([`/integrations/xero/onboarding/landing`]); + } + }); + } + + private checkProgressAndRedirect(): void { + const onboardingState: XeroOnboardingState = this.workspaceService.getOnboardingState(); + if (onboardingState !== XeroOnboardingState.COMPLETE) { + this.xeroConnectorService.postXeroTenants().subscribe(() => { + this.router.navigate(['integrations/xero/onboarding/connector']); + }); + } else { + this.router.navigate(['integrations/xero/main/dashboard']); + } + } + + connectXero() { + 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); + } ngOnInit(): void { } + ngOnDestroy(): void { + if (this.oauthCallbackSubscription) { + this.oauthCallbackSubscription.unsubscribe(); + } + } + } 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..4c9f6ff9b 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 @@ -9,6 +9,7 @@ import { XeroOnboardingLandingComponent } from './xero-onboarding-landing/xero-o import { XeroOnboardingComponent } from './xero-onboarding.component'; import { XeroTokenGuard } from 'src/app/core/guard/xero-token.guard'; import { TenantGuard } from 'src/app/core/guard/tenant.guard'; +import { XeroCloneSettingsComponent } from './xero-clone-settings/xero-clone-settings.component'; const routes: Routes = [ { @@ -30,17 +31,22 @@ 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] + }, + { + path: 'clone_settings', + component: XeroCloneSettingsComponent, + canActivate: [XeroTokenGuard] } ] } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding.module.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding.module.ts index 0387ba663..f9935b5f6 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding.module.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding.module.ts @@ -10,6 +10,8 @@ import { XeroOnboardingImportSettingsComponent } from './xero-onboarding-import- import { XeroOnboardingAdvancedSettingsComponent } from './xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component'; import { XeroOnboardingDoneComponent } from './xero-onboarding-done/xero-onboarding-done.component'; import { XeroSharedModule } from '../xero-shared/xero-shared.module'; +import { SharedModule } from 'src/app/shared/shared.module'; +import { XeroCloneSettingsComponent } from './xero-clone-settings/xero-clone-settings.component'; @NgModule({ @@ -20,12 +22,14 @@ import { XeroSharedModule } from '../xero-shared/xero-shared.module'; XeroOnboardingExportSettingsComponent, XeroOnboardingImportSettingsComponent, XeroOnboardingAdvancedSettingsComponent, - XeroOnboardingDoneComponent + XeroOnboardingDoneComponent, + XeroCloneSettingsComponent ], imports: [ CommonModule, XeroSharedModule, - XeroOnboardingRoutingModule + XeroOnboardingRoutingModule, + SharedModule ] }) export class XeroOnboardingModule { } 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..4805b9d77 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,205 @@ -

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..f02c8754c 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,124 @@ 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; + + }); + } 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..bdd4710ba 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,264 @@ 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 setupFormWatchers(): void { + this.createTaxCodeWatcher(); + this.createImportCustomerWatcher(); + this.createCOAWatcher(); + 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/integrations/xero/xero.module.ts b/src/app/integrations/xero/xero.module.ts index e43b6d6ac..f4000278f 100644 --- a/src/app/integrations/xero/xero.module.ts +++ b/src/app/integrations/xero/xero.module.ts @@ -2,12 +2,16 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { XeroRoutingModule } from './xero-routing.module'; +import { XeroSharedModule } from './xero-shared/xero-shared.module'; +import { SharedModule } from 'src/app/shared/shared.module'; @NgModule({ declarations: [], imports: [ CommonModule, - XeroRoutingModule + XeroRoutingModule, + XeroSharedModule, + SharedModule ] }) export class XeroModule { } 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 ) { } diff --git a/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.html b/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.html index 00a20ffbf..6e9204ddc 100644 --- a/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.html +++ b/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.html @@ -52,6 +52,11 @@ (mouseover)="qboConnectButtonSource = 'assets/buttons/connect-to-qbo-active.svg'" (mouseout)="qboConnectButtonSource = 'assets/buttons/connect-to-qbo.svg'">
+
+ +
diff --git a/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.ts b/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.ts index dd59939ab..cda9f112a 100644 --- a/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.ts +++ b/src/app/shared/components/helper/app-landing-page-header/app-landing-page-header.component.ts @@ -51,6 +51,8 @@ export class AppLandingPageHeaderComponent implements OnInit { @Input() showQBOButton: boolean; + @Input() showXeroButton: boolean; + @Input() logoWidth: string = '140px'; @Input() logoStyleClasses: string = 'tw-py-10-px tw-px-20-px'; @@ -59,6 +61,8 @@ export class AppLandingPageHeaderComponent implements OnInit { qboConnectButtonSource: string = 'assets/buttons/connect-to-qbo.svg'; + xeroConnectButtonSource: string = 'assets/buttons/connect-to-xero.svg'; + readonly brandingConfig = brandingConfig; readonly isGradientAllowed: boolean = brandingFeatureConfig.isGradientAllowed; @@ -87,7 +91,7 @@ export class AppLandingPageHeaderComponent implements OnInit { connect(): void { - if (this.appName === AppName.TRAVELPERK || this.appName === AppName.BUSINESS_CENTRAL || this.appName === AppName.BAMBOO_HR) { + if (this.appName === AppName.TRAVELPERK || this.appName === AppName.BUSINESS_CENTRAL || this.appName === AppName.BAMBOO_HR || this.appName === AppName.XERO) { this.initiateOAuth(); return; } else if (this.postConnectionRoute === 'qbd/onboarding/export_settings') { diff --git a/src/app/shared/components/input/dropdown/dropdown.component.html b/src/app/shared/components/input/dropdown/dropdown.component.html index 613bc17eb..2eafb9c8a 100644 --- a/src/app/shared/components/input/dropdown/dropdown.component.html +++ b/src/app/shared/components/input/dropdown/dropdown.component.html @@ -4,6 +4,7 @@ [ngClass]="additionalClasses ? additionalClasses : ''" [placeholder]="placeholder" [options]="options" + [disabled]="isDisabled" [formControlName]="formControllerName">
- +
diff --git a/src/app/shared/components/input/toggle/toggle.component.ts b/src/app/shared/components/input/toggle/toggle.component.ts index 7f7344e95..7d9f11064 100644 --- a/src/app/shared/components/input/toggle/toggle.component.ts +++ b/src/app/shared/components/input/toggle/toggle.component.ts @@ -12,6 +12,8 @@ export class ToggleComponent { @Input() formControllerName: string; + @Input() isDisabled: boolean; + constructor() { } } diff --git a/src/app/shared/components/onboarding/clone-setting/clone-setting-field/clone-setting-field.component.html b/src/app/shared/components/onboarding/clone-setting/clone-setting-field/clone-setting-field.component.html index 30142dbf5..febfb0120 100644 --- a/src/app/shared/components/onboarding/clone-setting/clone-setting-field/clone-setting-field.component.html +++ b/src/app/shared/components/onboarding/clone-setting/clone-setting-field/clone-setting-field.component.html @@ -1,6 +1,6 @@
- + {{ label }} @@ -12,12 +12,14 @@ [placeholder]="placeholder" [options]="options" [form]="form" + [isDisabled]="isDisabled" [formControllerName]="formControllerName" [displayKey]="dropdownDisplayKey" [additionalClasses]="additionalClasses"> \ No newline at end of file diff --git a/src/assets/flow-charts/fyle-xero-data-flow.svg b/src/assets/flow-charts/fyle-xero-data-flow.svg new file mode 100644 index 000000000..90a18895f --- /dev/null +++ b/src/assets/flow-charts/fyle-xero-data-flow.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/scss/tab-secondary.scss b/src/assets/scss/tab-secondary.scss index f3257ca15..f15890597 100644 --- a/src/assets/scss/tab-secondary.scss +++ b/src/assets/scss/tab-secondary.scss @@ -15,7 +15,7 @@ } :host ::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem.p-highlight .p-menuitem-link, :host ::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem.p-highlight .p-menuitem-link:not(.p-disabled):hover { - @apply tw-text-text-primary tw-border-b-transparent tw-bg-nav-tabs-active-bg #{!important}; + @apply tw-text-text-primary tw-border-b-border-white tw-bg-nav-tabs-active-bg #{!important}; } :host ::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem:not(.p-highlight) .p-menuitem-link{ @@ -24,7 +24,7 @@ :host ::ng-deep .p-tabmenu .p-tabmenu-nav { background-color: transparent; - @apply tw-border-b tw-border-b-nav-tabs-active-border #{!important}; + @apply tw-border-b tw-border-b-nav-tabs-active-border tw-pl-14-px #{!important}; } :host ::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem {