From b8123a13b38dbe3de64c0bcc2a24623359d55aaf Mon Sep 17 00:00:00 2001 From: Fyle Date: Fri, 8 Mar 2024 15:23:10 +0530 Subject: [PATCH 1/6] xero connector page construction --- src/app/branding/branding-config.ts | 142 ++++++++++++ .../branding/content-configuration.model.ts | 71 ++++++ .../xero-onboarding.model.ts | 2 +- .../xero-onboarding-connector.component.html | 44 +++- .../xero-onboarding-connector.component.ts | 214 +++++++++++++++++- 5 files changed, 470 insertions(+), 3 deletions(-) diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index 447dc7472..31d99d480 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -241,6 +241,77 @@ 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', + userId: 'user ID', + companyId: 'company ID', + userPassword: 'User password', + password: 'password', + tenantMapping: 'Tenant Mapping', + descriptionText: 'of the description field' + }, + configuration: { + connector: { + stepName: 'Connect to Xero', + subLabel: 'Expenses will be posted to the Xero Tenant Mapping selected here. Once configured, you can not change ' + brandingConfig.brandName + ' organization or Tenant Mapping.' + }, + exportSetting: { + stepName: 'Export settings', + headerText: '', + contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', + corporateCard: { + cccExpensePaymentType: 'Set the default expense payment type as?', + cccExpensePaymentTypeSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', + creditCardVendor: 'Set the default credit card vendor as', + creditCardVendorSublabel: 'The vendor configured here will be added to all the credit card expenses exported as bills.', + chargeCard: 'Set the default charge card', + chargeCardPlaceholder: 'Select a charge card', + chargeCardSublabel: 'Expenses of corporate cards in ' + brandingConfig.brandName + ' that are not mapped to their respective cards in Xero will be posted to the card configured here. You can map your cards in the mapping section after configuring the integration.', + cccExpenseState: 'You can export expenses either when they are awaiting closure after approval (approved) or when the transactions has been settled (closed).', + cccExportGroup: 'Expenses can either be exported as single line items (expense) or as a grouped report with multiple line items (report).', + employeeFieldMapping: 'How are your employees represented in Xero?', + creditCard: 'To which general ledger account should the expenses be credited to?', + creditCardSubLabel: 'The integration will credit the account selected here for corporate credit card expenses exported as journal entries.' + } + }, + 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.', + location: 'location', + department: 'department', + project: 'project', + class: 'class', + item: 'item' + }, + done: { + ctaText: '', + hintText: '' + } + } + }, intacct: { landing: { contentText: 'Import data from Sage Intacct to ' + brandingConfig.brandName + ' and Export expenses from ' + brandingConfig.brandName + ' to Sage Intacct. ', @@ -418,6 +489,77 @@ 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', + userId: 'user ID', + companyId: 'company ID', + userPassword: 'User password', + password: 'password', + tenantMapping: 'Tenant Mapping', + descriptionText: 'of the description field' + }, + configuration: { + connector: { + stepName: 'Connect to Xero', + subLabel: 'Expenses will be posted to the Xero Tenant Mapping selected here. Once configured, you can not change ' + brandingConfig.brandName + ' organization or Tenant Mapping.' + }, + exportSetting: { + stepName: 'Export settings', + headerText: '', + contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', + corporateCard: { + cccExpensePaymentType: 'Set the default expense payment type as?', + cccExpensePaymentTypeSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', + creditCardVendor: 'Set the default credit card vendor as', + creditCardVendorSublabel: 'The vendor configured here will be added to all the credit card expenses exported as bills.', + chargeCard: 'Set the default charge card', + chargeCardPlaceholder: 'Select a charge card', + chargeCardSublabel: 'Expenses of corporate cards in ' + brandingConfig.brandName + ' that are not mapped to their respective cards in Xero will be posted to the card configured here. You can map your cards in the mapping section after configuring the integration.', + cccExpenseState: 'You can export expenses either when they are awaiting closure after approval (approved) or when the transactions has been settled (closed).', + cccExportGroup: 'Expenses can either be exported as single line items (expense) or as a grouped report with multiple line items (report).', + employeeFieldMapping: 'How are your employees represented in Xero?', + creditCard: 'To which general ledger account should the expenses be credited to?', + creditCardSubLabel: 'The integration will credit the account selected here for corporate credit card expenses exported as journal entries.' + } + }, + 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.', + location: 'location', + department: 'department', + project: 'project', + class: 'class', + item: 'item' + }, + done: { + ctaText: '', + hintText: '' + } + } + }, 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/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index a089e8fcc..d13b5e816 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -1,5 +1,76 @@ 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; + userId: string; + companyId: string; + userPassword: string; + password: string; + tenantMapping: string; + descriptionText: string; + }, + configuration: { + connector: { + stepName: string; + subLabel: string; + }, + exportSetting: { + stepName: string; + headerText: string; + contentText: string; + corporateCard: { + cccExpensePaymentType: string; + cccExpensePaymentTypeSubLabel: string; + creditCardVendor: string; + creditCardVendorSublabel: string; + chargeCard: string; + chargeCardPlaceholder: string; + chargeCardSublabel: string; + cccExpenseState: string; + cccExportGroup: string; + employeeFieldMapping: string; + creditCard: string; + creditCardSubLabel: string; + } + }, + advancedSettings: { + stepName: string; + scheduleAutoExport: string; + email: string; + autoSyncPayments: string; + defaultPaymentAccount: string; + autoCreateEmployeeVendor: string; + postEntriesCurrentPeriod: string; + setDescriptionField: string; + dfvLabel: string; + dfvSubLabel: string; + location: string; + department: string; + project: string; + class: string; + item: string; + }, + done: { + ctaText: string; + hintText: string; + } + }, + }, intacct : { landing: { contentText: string; 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/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..13f5d68bb 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..38b7d4f27 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,25 @@ 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 } 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 } 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 { environment } from 'src/environments/environment'; @Component({ selector: 'app-xero-onboarding-connector', @@ -7,9 +28,200 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingConnectorComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.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[]; + + 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 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/qbo/onboarding/employee_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(): void { + 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 { + // TODO + } + + 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; + }); + } + + 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(); + this.isXeroConnected = false; + this.isContinueDisabled = true; + this.showOrHideDisconnectXero(); + this.xeroConnectionInProgress = false; + }); + } ngOnInit(): void { + this.setupPage(); } } From 7712beb00c70a13d4efabd77df0f2b21580116c8 Mon Sep 17 00:00:00 2001 From: Fyle Date: Sat, 9 Mar 2024 16:26:52 +0530 Subject: [PATCH 2/6] Xero connection save --- .../db/xero-destination-attribute.model.ts | 11 ++++ .../xero-connector.service.ts | 6 +-- .../xero-onboarding-connector.component.html | 2 +- .../xero-onboarding-connector.component.ts | 53 +++++++++++++++---- 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 src/app/core/models/xero/db/xero-destination-attribute.model.ts 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/services/xero/xero-configuration/xero-connector.service.ts b/src/app/core/services/xero/xero-configuration/xero-connector.service.ts index e1e6c9d94..325788fc1 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 { XeroDestinationAttributes } from 'src/app/core/models/xero/db/xero-destination-attribute.model'; const xeroCredentialsCache = new Subject(); @@ -49,11 +49,11 @@ export class XeroConnectorService { return this.apiService.get(`/workspaces/${workspaceId}/xero/token_health/`, {}); } - getXeroTenants(): Observable { + getXeroTenants(): Observable { return this.apiService.get(`/workspaces/${this.workspaceId}/xero/tenants/`, {attribute_type: 'TENANT'}); } - postXeroTenants(): Observable { + postXeroTenants(): Observable { const workspaceId = this.workspaceService.getWorkspaceId(); return this.apiService.post(`/workspaces/${workspaceId}/xero/tenants/`, {}); 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 13f5d68bb..70bf3f2e9 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 @@ -24,7 +24,7 @@ [accountingCompanyList]="tenantList" [switchLinkText]="'Disconnect'" (switchCompany)="disconnectXero()" - (connectCompany)="connectXero()"> + (connectCompany)="connectXero($event)"> { @@ -164,7 +173,33 @@ export class XeroOnboardingConnectorComponent implements OnInit { } private constructPayloadAndSave(): void { - // TODO + if (this.isContinueDisabled) { + return; + } else if (this.isCloneSettingsDisabled) { + this.router.navigate(['/integrations/xero/onboarding/export_settings']); + return; + } + if (this.xeroTenantselected && !this.isContinueDisabled) { + this.xeroConnectionInProgress = true; + this.isContinueDisabled = true; + const tenantMappingPayload: TenantMappingPost = { + tenant_id: this.xeroTenantselected.id.toString(), + tenant_name: this.xeroTenantselected.value + }; + 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 { @@ -183,7 +218,7 @@ export class XeroOnboardingConnectorComponent implements OnInit { } getTenant() { - this.xeroConnectorService.getXeroTenants().subscribe((tenantList: DestinationAttribute[]) => { + this.xeroConnectorService.getXeroTenants().subscribe((tenantList: XeroDestinationAttributes[]) => { this.tenantList = tenantList; }); } From 0635a154a3f968458028c693124f89217c975a74 Mon Sep 17 00:00:00 2001 From: Dhaarani <55541808+DhaaraniCIT@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:19:17 +0530 Subject: [PATCH 3/6] xero onboarding export settings page construction (#653) xero onboarding export settings page construction --- src/app/branding/branding-config.ts | 32 +-- src/app/core/guard/tenant.guard.ts | 5 +- .../branding/content-configuration.model.ts | 16 +- src/app/core/models/enum/enum.model.ts | 10 +- .../xero-export-settings.model.ts | 188 +++++++++++++++- .../xero-onboarding-connector.component.ts | 2 +- ...-onboarding-export-settings.component.html | 5 +- ...ro-onboarding-export-settings.component.ts | 12 +- .../xero-onboarding-routing.module.ts | 6 +- .../xero-export-settings.component.html | 207 +++++++++++++++++- .../xero-export-settings.component.ts | 137 +++++++++++- 11 files changed, 563 insertions(+), 57 deletions(-) diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index 31d99d480..51df96d23 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -275,18 +275,10 @@ const content: ContentConfiguration = { headerText: '', contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', corporateCard: { - cccExpensePaymentType: 'Set the default expense payment type as?', - cccExpensePaymentTypeSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', - creditCardVendor: 'Set the default credit card vendor as', - creditCardVendorSublabel: 'The vendor configured here will be added to all the credit card expenses exported as bills.', - chargeCard: 'Set the default charge card', - chargeCardPlaceholder: 'Select a charge card', - chargeCardSublabel: 'Expenses of corporate cards in ' + brandingConfig.brandName + ' that are not mapped to their respective cards in Xero will be posted to the card configured here. You can map your cards in the mapping section after configuring the integration.', - cccExpenseState: 'You can export expenses either when they are awaiting closure after approval (approved) or when the transactions has been settled (closed).', - cccExportGroup: 'Expenses can either be exported as single line items (expense) or as a grouped report with multiple line items (report).', - employeeFieldMapping: 'How are your employees represented in Xero?', - creditCard: 'To which general ledger account should the expenses be credited to?', - creditCardSubLabel: 'The integration will credit the account selected here for corporate credit card expenses exported as journal entries.' + cccExpenseBankAccountSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', + creditCardExportTypeSubLabel: '', + expenseState: '', + creditCardExpenseSubLabel: '' } }, advancedSettings: { @@ -523,18 +515,10 @@ const content: ContentConfiguration = { headerText: '', contentText: 'Enable this to export non-reimbursable expenses from ' + brandingConfig.brandName + '. If not enabled, any corporate credit card expenses will not be exported to Xero.', corporateCard: { - cccExpensePaymentType: 'Set the default expense payment type as?', - cccExpensePaymentTypeSubLabel: 'The selected expense payment type will be added to the corporate credit card expenses exported from ' + brandingConfig.brandName + ' to Xero.', - creditCardVendor: 'Set the default credit card vendor as', - creditCardVendorSublabel: 'The vendor configured here will be added to all the credit card expenses exported as bills.', - chargeCard: 'Set the default charge card', - chargeCardPlaceholder: 'Select a charge card', - chargeCardSublabel: 'Expenses of corporate cards in ' + brandingConfig.brandName + ' that are not mapped to their respective cards in Xero will be posted to the card configured here. You can map your cards in the mapping section after configuring the integration.', - cccExpenseState: 'You can export expenses either when they are awaiting closure after approval (approved) or when the transactions has been settled (closed).', - cccExportGroup: 'Expenses can either be exported as single line items (expense) or as a grouped report with multiple line items (report).', - employeeFieldMapping: 'How are your employees represented in Xero?', - creditCard: 'To which general ledger account should the expenses be credited to?', - creditCardSubLabel: 'The integration will credit the account selected here for corporate credit card expenses exported as journal entries.' + cccExpenseBankAccountSubLabel: '', + creditCardExportTypeSubLabel: '', + expenseState: '', + creditCardExpenseSubLabel: '' } }, advancedSettings: { diff --git a/src/app/core/guard/tenant.guard.ts b/src/app/core/guard/tenant.guard.ts index eddbbf2a4..e7ab47dec 100644 --- a/src/app/core/guard/tenant.guard.ts +++ b/src/app/core/guard/tenant.guard.ts @@ -36,10 +36,11 @@ export class TenantGuard implements CanActivate { ).pipe( map(response => !!response), catchError(error => { - if (error.status === 400) { + + if (error.status === 404) { globalCacheBusterNotifier.next(); this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Oops! You will need to select a tenant to proceed with the onboarding.'); - return this.router.navigateByUrl('integrations/xero/onboarding/xero_connector'); + return this.router.navigateByUrl('/integrations/xero/onboarding/connector'); } return throwError(error); }) diff --git a/src/app/core/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index d13b5e816..039683d9f 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -34,18 +34,10 @@ export type ContentConfiguration = { headerText: string; contentText: string; corporateCard: { - cccExpensePaymentType: string; - cccExpensePaymentTypeSubLabel: string; - creditCardVendor: string; - creditCardVendorSublabel: string; - chargeCard: string; - chargeCardPlaceholder: string; - chargeCardSublabel: string; - cccExpenseState: string; - cccExportGroup: string; - employeeFieldMapping: string; - creditCard: string; - creditCardSubLabel: string; + cccExpenseBankAccountSubLabel: string; + creditCardExportTypeSubLabel: string; + expenseState: string; + creditCardExpenseSubLabel: string } }, advancedSettings: { diff --git a/src/app/core/models/enum/enum.model.ts b/src/app/core/models/enum/enum.model.ts index 54159772a..fb3e79c7a 100644 --- a/src/app/core/models/enum/enum.model.ts +++ b/src/app/core/models/enum/enum.model.ts @@ -246,9 +246,16 @@ export enum ExpenseState { PAID = 'PAID' } + export enum CCCExpenseState { PAID = 'PAID', - APPROVED = 'APPROVED' + APPROVED = 'APPROVED', +} + +export enum XeroCCCExpenseState { + PAID = 'PAID', + APPROVED = 'APPROVED', + PAYMENT_PROCESSING = "PAYMENT_PROCESSING" } export enum ExpenseGroupedBy { @@ -648,6 +655,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/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/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 73cd7234f..470a8d8d2 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 @@ -29,7 +29,7 @@ import { environment } from 'src/environments/environment'; }) export class XeroOnboardingConnectorComponent implements OnInit { - brandingContent = brandingContent.configuration.connector; + brandingContent = brandingContent.xero.configuration.connector; onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); 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-routing.module.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts index 6daba1cec..97bb488f5 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-routing.module.ts @@ -30,17 +30,17 @@ const routes: Routes = [ { path: 'import_settings', component: XeroOnboardingImportSettingsComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] }, { path: 'advanced_settings', component: XeroOnboardingAdvancedSettingsComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] }, { path: 'done', component: XeroOnboardingDoneComponent, - canActivate: [XeroTokenGuard, TenantGuard] + canActivate: [XeroTokenGuard] } ] } diff --git a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html index dd1a112dd..e34939645 100644 --- a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html +++ b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.html @@ -1 +1,206 @@ -

xero-export-settings works!

+
+
+ +
+
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+ Note: In case the employee records are not auto matched by the integration, you could still manually map the records from the Mappings section of the integration. +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+
+ + + + diff --git a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts index 761b35d1c..eb05a61ec 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,129 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroExportSettingsComponent implements OnInit { - constructor() { } + isLoading: boolean = true; + + redirectLink: string = brandingKbArticles.onboardingArticles.XERO.EXPORT_SETTING; + + brandingConfig = brandingConfig; + + isOnboarding: boolean; + + windowReference: Window; + + exportSettings: XeroExportSettingGet; + + bankAccounts: DestinationAttribute[]; + + reimbursableExportTypes: SelectFormOption[] = XeroExportSettingModel.getReimbursableExportTypes(); + + creditCardExportTypes = XeroExportSettingModel.getCreditCardExportTypes(); + + reimbursableExpenseGroupByOptions = XeroExportSettingModel.getReimbursableExpenseGroupingOptions(); + + cccExpenseGroupByOptions = XeroExportSettingModel.getCCCExpenseGroupingOptions(); + + reimbursableExpenseGroupingDateOptions = XeroExportSettingModel.getReimbursableExpenseGroupingDateOptions(); + + cccExpenseGroupingDateOptions = XeroExportSettingModel.getCCCExpenseGroupingDateOptions(); + + autoMapEmployeeTypes = XeroExportSettingModel.getAutoMapEmployeeOptions(); + + expenseStateOptions = XeroExportSettingModel.getReimbursableExpenseStateOptions(); + + cccExpenseStateOptions = XeroExportSettingModel.getCCCExpenseStateOptions(); + + exportSettingForm: FormGroup; + + isSaveInProgress: boolean; + + isConfirmationDialogVisible: boolean; + + warningDialogText: string; + + appName: AppName = AppName.XERO; + + EmployeeFieldMapping = EmployeeFieldMapping; + + ConfigurationCtaText = ConfigurationCta; + + readonly brandingFeatureConfig = brandingFeatureConfig; + + readonly brandingContent = brandingContent.xero.configuration.exportSetting; + + constructor( + public helperService: HelperService, + private exportSettingService: XeroExportSettingsService, + private mappingService: MappingService, + private xeroHelperService: XeroHelperService, + private router : Router, + private workspaceService: WorkspaceService, + private toastService: IntegrationsToastService + ) { } + + refreshDimensions(isRefresh: boolean) { + this.xeroHelperService.refreshXeroDimensions().subscribe(); + } + + save() { + if (this.exportSettingForm.valid) { + this.constructPayloadAndSave({ + hasAccepted: true, + event: ConfigurationWarningEvent.XERO_EXPORT_SETTINGS + }); + } + } + + constructPayloadAndSave(event: ConfigurationWarningOut) { + if (event.hasAccepted) { + this.isSaveInProgress = true; + const exportSettingPayload = XeroExportSettingModel.constructPayload(this.exportSettingForm); + this.exportSettingService.postExportSettings(exportSettingPayload).subscribe((response: XeroExportSettingGet) => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.SUCCESS, 'Export settings saved successfully'); + + if (this.isOnboarding) { + this.workspaceService.setOnboardingState(XeroOnboardingState.IMPORT_SETTINGS); + this.router.navigate([`/integrations/xero/onboarding/import_settings`]); + } + }, () => { + this.isSaveInProgress = false; + this.toastService.displayToastMessage(ToastSeverity.ERROR, 'Error saving export settings, please try again later'); + }); + } + } + + setupPage() { + this.isOnboarding = this.router.url.includes('onboarding'); + const destinationAttributes = ['BANK_ACCOUNT']; + + forkJoin([ + this.exportSettingService.getExportSettings(), + this.mappingService.getGroupedDestinationAttributes(destinationAttributes, 'v1', 'xero') + ]).subscribe(response => { + this.exportSettings = response[0]; + this.bankAccounts = response[1].BANK_ACCOUNT; + + this.exportSettingForm = XeroExportSettingModel.mapAPIResponseToFormGroup(this.exportSettings); + + this.helperService.addExportSettingFormValidator(this.exportSettingForm); + const [exportSettingValidatorRule, exportModuleRule] = XeroExportSettingModel.getValidators(); + + this.helperService.setConfigurationSettingValidatorsAndWatchers(exportSettingValidatorRule, this.exportSettingForm); + + this.helperService.setExportTypeValidatorsAndWatchers(exportModuleRule, this.exportSettingForm); + + this.isLoading = false; + + }); + } + + setupForm() { + throw new Error('Method not implemented.'); + } ngOnInit(): void { + this.setupPage(); } } From a28d569a9b7c9c8d04aa488c226df55c730b2838 Mon Sep 17 00:00:00 2001 From: Fyle Date: Fri, 15 Mar 2024 14:48:24 +0530 Subject: [PATCH 4/6] PR comments fix --- src/app/branding/branding-config.ts | 28 +++++-------------- .../branding/content-configuration.model.ts | 11 ++------ .../db/xero-destination-attribute.model.ts | 11 -------- .../xero/db/xero-tenant-mapping.model.ts | 11 ++++++++ .../xero-connector.service.ts | 6 ++-- .../xero-onboarding-connector.component.html | 4 +-- .../xero-onboarding-connector.component.ts | 25 ++++++++--------- 7 files changed, 36 insertions(+), 60 deletions(-) delete mode 100644 src/app/core/models/xero/db/xero-destination-attribute.model.ts diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index 51df96d23..093480aaf 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -258,15 +258,13 @@ const content: ContentConfiguration = { customFieldPlaceholderName: 'Placeholder name', customFieldType: 'Field type', customFieldCreateandSave: 'Create and save', - userId: 'user ID', - companyId: 'company ID', - userPassword: 'User password', - password: 'password', 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.' }, @@ -291,12 +289,7 @@ const content: ContentConfiguration = { 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.', - location: 'location', - department: 'department', - project: 'project', - class: 'class', - item: 'item' + 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.' }, done: { ctaText: '', @@ -498,17 +491,15 @@ const content: ContentConfiguration = { customFieldPlaceholderName: 'Placeholder name', customFieldType: 'Field type', customFieldCreateandSave: 'Create and save', - userId: 'user ID', - companyId: 'company ID', - userPassword: 'User password', - password: 'password', 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.' + 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', @@ -531,12 +522,7 @@ const content: ContentConfiguration = { 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.', - location: 'location', - department: 'department', - project: 'project', - class: 'class', - item: 'item' + 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.' }, done: { ctaText: '', diff --git a/src/app/core/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index 039683d9f..3da704f67 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -17,10 +17,6 @@ export type ContentConfiguration = { customFieldPlaceholderName: string; customFieldType: string; customFieldCreateandSave: string; - userId: string; - companyId: string; - userPassword: string; - password: string; tenantMapping: string; descriptionText: string; }, @@ -28,6 +24,8 @@ export type ContentConfiguration = { connector: { stepName: string; subLabel: string; + configurationHeaderText: string; + configurationSubHeaderText: string; }, exportSetting: { stepName: string; @@ -51,11 +49,6 @@ export type ContentConfiguration = { setDescriptionField: string; dfvLabel: string; dfvSubLabel: string; - location: string; - department: string; - project: string; - class: string; - item: string; }, done: { ctaText: string; 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 deleted file mode 100644 index 2a150f0a2..000000000 --- a/src/app/core/models/xero/db/xero-destination-attribute.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/services/xero/xero-configuration/xero-connector.service.ts b/src/app/core/services/xero/xero-configuration/xero-connector.service.ts index 325788fc1..9df958633 100644 --- a/src/app/core/services/xero/xero-configuration/xero-connector.service.ts +++ b/src/app/core/services/xero/xero-configuration/xero-connector.service.ts @@ -6,7 +6,7 @@ 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 { TenantMapping, TenantMappingPost } from 'src/app/core/models/xero/db/xero-tenant-mapping.model'; -import { XeroDestinationAttributes } from 'src/app/core/models/xero/db/xero-destination-attribute.model'; +import { DestinationAttribute } from 'src/app/core/models/db/destination-attribute.model'; const xeroCredentialsCache = new Subject(); @@ -49,11 +49,11 @@ export class XeroConnectorService { return this.apiService.get(`/workspaces/${workspaceId}/xero/token_health/`, {}); } - getXeroTenants(): Observable { + getXeroTenants(): Observable { return this.apiService.get(`/workspaces/${this.workspaceId}/xero/tenants/`, {attribute_type: 'TENANT'}); } - postXeroTenants(): Observable { + postXeroTenants(): Observable { const workspaceId = this.workspaceService.getWorkspaceId(); return this.apiService.post(`/workspaces/${workspaceId}/xero/tenants/`, {}); 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 70bf3f2e9..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 @@ -7,8 +7,8 @@
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 470a8d8d2..2b4a5a2a4 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-connector/xero-onboarding-connector.component.ts @@ -4,12 +4,12 @@ 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 { XeroDestinationAttributes } from 'src/app/core/models/xero/db/xero-destination-attribute.model'; -import { TenantMapping, TenantMappingPost } from 'src/app/core/models/xero/db/xero-tenant-mapping.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'; @@ -73,9 +73,9 @@ export class XeroOnboardingConnectorComponent implements OnInit { showDisconnectXero: boolean; - tenantList: XeroDestinationAttributes[]; + tenantList: DestinationAttribute[]; - xeroTenantselected: XeroDestinationAttributes; + xeroTenantselected: DestinationAttribute; constructor( private workspaceService: WorkspaceService, @@ -148,7 +148,7 @@ export class XeroOnboardingConnectorComponent implements OnInit { }); } - connectXero(companyDetails: XeroDestinationAttributes): void { + connectXero(companyDetails: DestinationAttribute): void { this.xeroTenantselected = companyDetails; this.isContinueDisabled = false; } @@ -182,10 +182,7 @@ export class XeroOnboardingConnectorComponent implements OnInit { if (this.xeroTenantselected && !this.isContinueDisabled) { this.xeroConnectionInProgress = true; this.isContinueDisabled = true; - const tenantMappingPayload: TenantMappingPost = { - tenant_id: this.xeroTenantselected.id.toString(), - tenant_name: this.xeroTenantselected.value - }; + const tenantMappingPayload: TenantMappingPost = TenantMappingModel.constructPayload(this.xeroTenantselected); this.xeroConnectorService.postTenantMapping(tenantMappingPayload).subscribe((response:TenantMapping) => { this.xeroHelperService.refreshXeroDimensions().subscribe(() => { this.workspaceService.setOnboardingState(XeroOnboardingState.EXPORT_SETTINGS); @@ -218,8 +215,12 @@ export class XeroOnboardingConnectorComponent implements OnInit { } getTenant() { - this.xeroConnectorService.getXeroTenants().subscribe((tenantList: XeroDestinationAttributes[]) => { + this.xeroConnectorService.getXeroTenants().subscribe((tenantList: DestinationAttribute[]) => { this.tenantList = tenantList; + this.isXeroConnected = false; + this.isContinueDisabled = true; + this.xeroConnectionInProgress = false; + this.showOrHideDisconnectXero(); }); } @@ -248,10 +249,6 @@ export class XeroOnboardingConnectorComponent implements OnInit { }, () => { this.getTenant(); - this.isXeroConnected = false; - this.isContinueDisabled = true; - this.showOrHideDisconnectXero(); - this.xeroConnectionInProgress = false; }); } From f6c7031546a96ed928f0b364f85cfbe73ef61094 Mon Sep 17 00:00:00 2001 From: Fyle Date: Fri, 15 Mar 2024 14:51:25 +0530 Subject: [PATCH 5/6] PR comments fix --- src/app/branding/branding-config.ts | 8 -------- .../core/models/branding/content-configuration.model.ts | 4 ---- 2 files changed, 12 deletions(-) diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index 093480aaf..b8a316326 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -290,10 +290,6 @@ const content: ContentConfiguration = { 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.' - }, - done: { - ctaText: '', - hintText: '' } } }, @@ -523,10 +519,6 @@ const content: ContentConfiguration = { 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.' - }, - done: { - ctaText: '', - hintText: '' } } }, diff --git a/src/app/core/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index 3da704f67..91edfceb5 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -49,10 +49,6 @@ export type ContentConfiguration = { setDescriptionField: string; dfvLabel: string; dfvSubLabel: string; - }, - done: { - ctaText: string; - hintText: string; } }, }, From dfba9532a105acb5460f1cde0b581a67dd5e54c6 Mon Sep 17 00:00:00 2001 From: Dhaarani <55541808+DhaaraniCIT@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:43:27 +0530 Subject: [PATCH 6/6] Xero onboarding import settings (#658) export settings basic functions --- src/app/branding/branding-config.ts | 30 ++ .../branding/content-configuration.model.ts | 15 + src/app/core/models/enum/enum.model.ts | 7 + .../xero-advanced-settings.model.ts | 73 ++++- .../xero-import-settings.model.ts | 45 ++- .../xero-import-settings.service.ts | 5 + ...nboarding-advanced-settings.component.html | 5 +- ...-onboarding-advanced-settings.component.ts | 13 +- .../xero-onboarding-done.component.html | 2 +- .../xero-onboarding-done.component.ts | 9 +- ...-onboarding-import-settings.component.html | 5 +- ...ro-onboarding-import-settings.component.ts | 12 +- .../xero-advanced-settings.component.html | 112 ++++++- .../xero-advanced-settings.component.ts | 116 +++++++- .../xero-export-settings.component.ts | 1 - .../xero-import-settings.component.html | 121 +++++++- .../xero-import-settings.component.ts | 280 +++++++++++++++++- .../xero/xero-shared/xero-shared.module.ts | 4 +- .../configuration-import-field.component.html | 8 +- .../configuration-import-field.component.ts | 27 +- .../configuration-toggle-field.component.html | 2 +- .../configuration-toggle-field.component.ts | 9 + 22 files changed, 870 insertions(+), 31 deletions(-) diff --git a/src/app/branding/branding-config.ts b/src/app/branding/branding-config.ts index b8a316326..b5515aa04 100644 --- a/src/app/branding/branding-config.ts +++ b/src/app/branding/branding-config.ts @@ -279,6 +279,21 @@ const content: ContentConfiguration = { 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', @@ -508,6 +523,21 @@ const content: ContentConfiguration = { 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', diff --git a/src/app/core/models/branding/content-configuration.model.ts b/src/app/core/models/branding/content-configuration.model.ts index 91edfceb5..01fc2a3df 100644 --- a/src/app/core/models/branding/content-configuration.model.ts +++ b/src/app/core/models/branding/content-configuration.model.ts @@ -38,6 +38,21 @@ export type ContentConfiguration = { 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; diff --git a/src/app/core/models/enum/enum.model.ts b/src/app/core/models/enum/enum.model.ts index fb3e79c7a..48580322c 100644 --- a/src/app/core/models/enum/enum.model.ts +++ b/src/app/core/models/enum/enum.model.ts @@ -343,6 +343,13 @@ export enum QBDFyleField { COST_CENTER = 'COST_CENTER' } +export enum XeroFyleField { + PROJECT = 'PROJECT', + CUSTOMER = 'CUSTOMER', + TAX_CODE = 'TAX_CODE', + BANK_ACCOUNT = 'BANK_ACCOUNT' +} + export enum QBDAccountingExportsState { COMPLETE = 'COMPLETE', ENQUEUED = 'ENQUEUED', diff --git a/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts b/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts index cd6b38a46..c2cd37376 100644 --- a/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-advanced-settings.model.ts @@ -1,8 +1,9 @@ -import { FormGroup } from "@angular/forms"; -import { EmailOption } from "../../common/advanced-settings.model"; +import { FormControl, FormGroup } from "@angular/forms"; +import { AdvancedSettingValidatorRule, AdvancedSettingsModel, EmailOption } from "../../common/advanced-settings.model"; import { SelectFormOption } from "../../common/select-form-option.model"; import { DefaultDestinationAttribute } from "../../db/destination-attribute.model"; import { PaymentSyncDirection } from "../../enum/enum.model"; +import { HelperUtility } from "../../common/helper.model"; export type XeroAdvancedSettingWorkspaceGeneralSetting = { @@ -56,7 +57,69 @@ export interface XeroAdvancedSettingFormOption extends SelectFormOption { value: PaymentSyncDirection | number | 'None'; } -export class XeroAdvancedSettingModel { +export class XeroAdvancedSettingModel extends HelperUtility{ + + static getPaymentSyncOptions(): SelectFormOption[] { + return [ + { + label: 'None', + value: 'None' + }, + { + label: 'Export Fyle ACH Payments to Xero', + value: PaymentSyncDirection.FYLE_TO_XERO + }, + { + label: 'Import Xero Payments into Fyle', + value: PaymentSyncDirection.XERO_TO_FYLE + } + ]; + } + + static getValidators(): AdvancedSettingValidatorRule { + return { + paymentSync: 'billPaymentAccount', + exportSchedule: 'exportScheduleFrequency' + }; + } + + static setConfigurationSettingValidatorsAndWatchers(form: FormGroup): void { + const validatorRule = this.getValidators(); + const keys = Object.keys(validatorRule); + + Object.values(validatorRule).forEach((value, index) => { + form.controls[keys[index]].valueChanges.subscribe((selectedValue) => { + if (selectedValue && ((keys[index] === 'paymentSync' && selectedValue === PaymentSyncDirection.FYLE_TO_XERO) || (keys[index] !== 'paymentSync'))) { + this.markControllerAsRequired(form, value); + } else { + this.clearValidatorAndResetValue(form, value); + } + }); + }); + } + + static mapAPIResponseToFormGroup(advancedSettings: XeroAdvancedSettingGet, adminEmails: EmailOption[]): FormGroup { + let paymentSync = ''; + if (advancedSettings.workspace_general_settings.sync_fyle_to_xero_payments) { + paymentSync = PaymentSyncDirection.FYLE_TO_XERO; + } else if (advancedSettings.workspace_general_settings.sync_xero_to_fyle_payments) { + paymentSync = PaymentSyncDirection.XERO_TO_FYLE; + } + return new FormGroup({ + paymentSync: new FormControl(paymentSync), + billPaymentAccount: new FormControl(advancedSettings.general_mappings.payment_account.id ? advancedSettings.general_mappings.payment_account : null), + changeAccountingPeriod: new FormControl(advancedSettings.workspace_general_settings.change_accounting_period), + autoCreateVendors: new FormControl(advancedSettings.workspace_general_settings.auto_create_destination_entity), + exportSchedule: new FormControl(advancedSettings.workspace_schedules?.enabled ? advancedSettings.workspace_schedules.interval_hours : false), + exportScheduleFrequency: new FormControl(advancedSettings.workspace_schedules?.enabled ? advancedSettings.workspace_schedules.interval_hours : 1), + autoCreateMerchantDestinationEntity: new FormControl(advancedSettings.workspace_general_settings.auto_create_merchant_destination_entity ? advancedSettings.workspace_general_settings.auto_create_merchant_destination_entity : false), + search: new FormControl(), + searchOption: new FormControl(), + email: new FormControl(advancedSettings?.workspace_schedules?.emails_selected && advancedSettings?.workspace_schedules?.emails_selected?.length > 0 ? AdvancedSettingsModel.filterAdminEmails(advancedSettings?.workspace_schedules?.emails_selected, adminEmails) : []), + additionalEmails: new FormControl([]) + }); + } + static constructPayload(advancedSettingsForm: FormGroup): XeroAdvancedSettingPost { const emptyDestinationAttribute = {id: null, name: null}; const advancedSettingPayload: XeroAdvancedSettingPost = { @@ -74,8 +137,8 @@ export class XeroAdvancedSettingModel { enabled: advancedSettingsForm.get('exportSchedule')?.value ? true : false, interval_hours: advancedSettingsForm.get('exportScheduleFrequency')?.value ? advancedSettingsForm.get('exportScheduleFrequency')?.value : null, start_datetime: new Date(), - emails_selected: advancedSettingsForm.get('emails')?.value ? advancedSettingsForm.get('emails')?.value : [], - additional_email_options: advancedSettingsForm.get('addedEmail')?.value ? advancedSettingsForm.get('addedEmail')?.value : [] + emails_selected: advancedSettingsForm.get('email')?.value ? AdvancedSettingsModel.formatSelectedEmails(advancedSettingsForm.get('email')?.value) : [], + additional_email_options: advancedSettingsForm.get('additionalEmails')?.value ? advancedSettingsForm.get('additionalEmails')?.value : [] } }; return advancedSettingPayload; diff --git a/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts b/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts index 10bbff8b3..a4192ef7b 100644 --- a/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts +++ b/src/app/core/models/xero/xero-configuration/xero-import-settings.model.ts @@ -1,12 +1,13 @@ -import { FormGroup } from "@angular/forms"; +import { FormArray, FormControl, FormGroup } from "@angular/forms"; import { SelectFormOption } from "../../common/select-form-option.model"; import { DefaultDestinationAttribute } from "../../db/destination-attribute.model"; import { MappingSetting } from "../../db/mapping-setting.model"; -import { MappingDestinationField, MappingSourceField } from "../../enum/enum.model"; -import { GeneralMapping } from "../../intacct/db/mappings.model"; +import { MappingDestinationField, MappingSourceField, XeroFyleField } from "../../enum/enum.model"; import { ImportSettingGeneralMapping } from "../../intacct/intacct-configuration/import-settings.model"; import { XeroWorkspaceGeneralSetting } from "../db/xero-workspace-general-setting.model"; -import { ImportSettingsModel } from "../../common/import-settings.model"; +import { ImportSettingMappingRow, ImportSettingsModel } from "../../common/import-settings.model"; +import { IntegrationField } from "../../db/mapping.model"; +import { brandingConfig } from "src/app/branding/branding-config"; export type XeroImportSettingWorkspaceGeneralSetting = { @@ -45,7 +46,7 @@ export type ExpenseFieldsFormOption = { export type XeroImportSettingGet = { workspace_general_settings: XeroWorkspaceGeneralSetting, - general_mappings: GeneralMapping, + general_mappings: XeroImportSettingGeneralMapping, mapping_settings: MappingSetting[], workspace_id:number } @@ -56,16 +57,46 @@ export interface XeroImportSettingFormOption extends SelectFormOption { export class XeroImportSettingModel extends ImportSettingsModel { + + static getChartOfAccountTypesList(): string[] { + return ['EXPENSE', 'ASSET', 'EQUITY', 'LIABILITY', 'REVENUE']; + } + + static mapAPIResponseToFormGroup(importSettings: XeroImportSettingGet | null, xeroFields: IntegrationField[], isCustomerPresent:boolean): FormGroup { + let additionalOption: any[] = []; + if (brandingConfig.brandId === 'co' && isCustomerPresent) { + const additionalMappingSetting = { + source_field: 'DISABLED_XERO_SOURCE_FIELD', + destination_field: XeroFyleField.CUSTOMER, + import_to_fyle: importSettings?.workspace_general_settings.import_customers || false, + is_custom: false, + source_placeholder: null + }; + additionalOption = [ImportSettingsModel.createFormGroup(additionalMappingSetting)]; + } + const expenseFieldsArray = importSettings?.mapping_settings ? additionalOption.concat(this.constructFormArray(importSettings.mapping_settings, xeroFields)) : []; + return new FormGroup({ + importCategories: new FormControl(importSettings?.workspace_general_settings.import_categories ?? false), + expenseFields: new FormArray(expenseFieldsArray), + chartOfAccountTypes: new FormControl(importSettings?.workspace_general_settings.charts_of_accounts ? importSettings.workspace_general_settings.charts_of_accounts : ['Expense']), + importCustomers: new FormControl(importSettings?.workspace_general_settings.import_customers ?? false), + taxCode: new FormControl(importSettings?.workspace_general_settings.import_tax_codes ?? false), + importSuppliersAsMerchants: new FormControl(importSettings?.workspace_general_settings.import_suppliers_as_merchants ?? false), + defaultTaxCode: new FormControl(importSettings?.general_mappings?.default_tax_code?.id ? importSettings.general_mappings.default_tax_code : null), + searchOption: new FormControl('') + }); + } + static constructPayload(importSettingsForm: FormGroup): XeroImportSettingPost { const emptyDestinationAttribute = {id: null, name: null}; const chartOfAccounts = XeroImportSettingModel.formatChartOfAccounts(importSettingsForm.get('chartOfAccountTypes')?.value); - const expenseFieldArray = importSettingsForm.getRawValue().expenseFields; + const expenseFieldArray = importSettingsForm.getRawValue().expenseFields.filter(((data:any) => data.destination_field !== XeroFyleField.CUSTOMER)); const mappingSettings = this.constructMappingSettingPayload(expenseFieldArray); const importSettingPayload: XeroImportSettingPost = { workspace_general_settings: { - import_categories: importSettingsForm.get('chartOfAccount')?.value, + import_categories: importSettingsForm.get('chartOfAccount')?.value ?? false, charts_of_accounts: importSettingsForm.get('chartOfAccount')?.value ? chartOfAccounts : ['Expense'], import_tax_codes: importSettingsForm.get('taxCode')?.value, import_suppliers_as_merchants: importSettingsForm.get('importSuppliersAsMerchants')?.value, diff --git a/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts b/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts index ffd740ae2..ac37b1ba9 100644 --- a/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts +++ b/src/app/core/services/xero/xero-configuration/xero-import-settings.service.ts @@ -4,6 +4,7 @@ import { WorkspaceService } from '../../common/workspace.service'; import { XeroImportSettingGet, XeroImportSettingPost } from 'src/app/core/models/xero/xero-configuration/xero-import-settings.model'; import { Observable, Subject } from 'rxjs'; import { CacheBuster, Cacheable } from 'ts-cacheable'; +import { IntegrationField } from 'src/app/core/models/db/mapping.model'; const xeroImportSettingGetCache$ = new Subject(); @@ -30,4 +31,8 @@ export class XeroImportSettingsService { postImportSettings(exportSettingsPayload: XeroImportSettingPost): Observable{ return this.apiService.put(`/v2/workspaces/${this.workspaceService.getWorkspaceId()}/import_settings/`, exportSettingsPayload); } + + getXeroField(): Observable { + return this.apiService.get(`/workspaces/${this.workspaceService.getWorkspaceId()}/xero/xero_fields/`, {}); + } } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html index 7065c19e6..70cdc677c 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.html @@ -1 +1,4 @@ -

xero-onboarding-advanced-settings works!

+
+ + +
diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts index 0a88b4fae..5291c3320 100644 --- a/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts +++ b/src/app/integrations/xero/xero-onboarding/xero-onboarding-advanced-settings/xero-onboarding-advanced-settings.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { brandingContent } from 'src/app/branding/branding-config'; +import { OnboardingStepper } from 'src/app/core/models/misc/onboarding-stepper.model'; +import { XeroOnboardingModel } from 'src/app/core/models/xero/xero-configuration/xero-onboarding.model'; +import { WorkspaceService } from 'src/app/core/services/common/workspace.service'; @Component({ selector: 'app-xero-onboarding-advanced-settings', @@ -7,7 +11,14 @@ import { Component, OnInit } from '@angular/core'; }) export class XeroOnboardingAdvancedSettingsComponent implements OnInit { - constructor() { } + brandingContent = brandingContent.xero.configuration.advancedSettings; + + onboardingSteps: OnboardingStepper[] = new XeroOnboardingModel().getOnboardingSteps(this.brandingContent.stepName, this.workspaceService.getOnboardingState()); + + constructor( + private workspaceService: WorkspaceService + ) { } + ngOnInit(): void { } diff --git a/src/app/integrations/xero/xero-onboarding/xero-onboarding-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-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-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.ts b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts index eb05a61ec..314a5f21f 100644 --- a/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts +++ b/src/app/integrations/xero/xero-shared/xero-export-settings/xero-export-settings.component.ts @@ -124,7 +124,6 @@ export class XeroExportSettingsComponent implements OnInit { ]).subscribe(response => { this.exportSettings = response[0]; this.bankAccounts = response[1].BANK_ACCOUNT; - this.exportSettingForm = XeroExportSettingModel.mapAPIResponseToFormGroup(this.exportSettings); this.helperService.addExportSettingFormValidator(this.exportSettingForm); diff --git a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html index c2207c283..87df98cbb 100644 --- a/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html +++ b/src/app/integrations/xero/xero-shared/xero-import-settings/xero-import-settings.component.html @@ -1 +1,120 @@ -

xero-import-settings works!

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

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

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