From c2c27606b6c8bb8d3bc8cdaf76cbff459afe6ed1 Mon Sep 17 00:00:00 2001 From: Katherine Fleming <2205659+kflemin@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:45:33 -0600 Subject: [PATCH 1/2] complete Salesforce settings migration and fix bugs --- .../api/salesforce/salesforce.service.ts | 16 +- src/@seed/api/salesforce/salesforce.types.ts | 2 +- .../salesforce/salesforce.component.html | 753 ++++++++++-------- .../salesforce/salesforce.component.ts | 35 +- 4 files changed, 448 insertions(+), 358 deletions(-) diff --git a/src/@seed/api/salesforce/salesforce.service.ts b/src/@seed/api/salesforce/salesforce.service.ts index d749ad47..54a671eb 100644 --- a/src/@seed/api/salesforce/salesforce.service.ts +++ b/src/@seed/api/salesforce/salesforce.service.ts @@ -50,8 +50,7 @@ export class SalesforceService { return response }), catchError((error: HttpErrorResponse) => { - // TODO need to figure out error handling - return this._errorService.handleError(error, 'Error fetching organization') + return this._errorService.handleError(error, 'Error fetching Salesforce config') }), ) } @@ -64,14 +63,12 @@ export class SalesforceService { return response }), catchError((error: HttpErrorResponse) => { - // TODO need to figure out error handling - return this._errorService.handleError(error, 'Error fetching organization') + return this._errorService.handleError(error, 'Error fetching Salesforce mappings') }), ) } create(organizationId: number, config: SalesforceConfig): Observable { - console.log('Creating: ', config) const url = `/api/v3/salesforce_configs/?organization_id=${organizationId}` return this._httpClient.post(url, { ...config }).pipe( map((response) => { @@ -79,7 +76,7 @@ export class SalesforceService { return response.salesforce_config }), catchError((error: HttpErrorResponse) => { - return this._errorService.handleError(error, 'Error fetching organization') + return this._errorService.handleError(error, 'Error creating Salesforce config') }), ) } @@ -93,7 +90,7 @@ export class SalesforceService { return response.salesforce_config }), catchError((error: HttpErrorResponse) => { - return this._errorService.handleError(error, 'Error fetching organization') + return this._errorService.handleError(error, 'Error updating Salesforce config') }), ) } @@ -119,10 +116,11 @@ export class SalesforceService { return response }), catchError((error: HttpErrorResponse) => { - return this._errorService.handleError(error, `Salesforce Mapping could not be created: ${error.message}`) + return this._errorService.handleError(error, 'Error creating Salesforce mapping') }), ) } + updateMapping(organizationId: number, mapping: SalesforceMapping): Observable { const url = `/api/v3/salesforce_mappings/${mapping.id}/?organization_id=${organizationId}` return this._httpClient.put(url, { ...mapping }).pipe( @@ -131,7 +129,7 @@ export class SalesforceService { return response }), catchError((error: HttpErrorResponse) => { - return this._errorService.handleError(error, `Salesforce Mapping could not be created: ${error.message}`) + return this._errorService.handleError(error, 'Error updating Salesforce mapping') }), ) } diff --git a/src/@seed/api/salesforce/salesforce.types.ts b/src/@seed/api/salesforce/salesforce.types.ts index e59aea35..d1e92ed9 100644 --- a/src/@seed/api/salesforce/salesforce.types.ts +++ b/src/@seed/api/salesforce/salesforce.types.ts @@ -45,7 +45,7 @@ export type SalesforceConfigsResponse = { export type SalesforceMapping = { id: number; organization_id: number; - column: 10; + column: number; salesforce_fieldname: string; } diff --git a/src/app/modules/organizations/settings/salesforce/salesforce.component.html b/src/app/modules/organizations/settings/salesforce/salesforce.component.html index dcc11295..7acb3d54 100644 --- a/src/app/modules/organizations/settings/salesforce/salesforce.component.html +++ b/src/app/modules/organizations/settings/salesforce/salesforce.component.html @@ -16,351 +16,426 @@
-
- -
{{ t('Salesforce Connection') }}
-
-
{{ t('Enter your Salesforce instance details and ensure your connection is successful') }}
-
- - {{ t('Salesforce URL') }} - - -
-
- - {{ t('Username') }} - - -
-
- - {{ t('Password') }} - - - -
-
- - {{ t('Security Token') }} - - - {{ t('Security token set in Salesforce') }} - -
-
- - {{ t('Domain') }} - - {{ - t("If your Salesforce instance is a sandbox, set this field to the value 'test'; otherwise leave blank.") - }} - -
-
- -
- - -
- -
{{ t('Scheduled Daily Updates') }}
-
-
- {{ t('If you would like to automatically update Salesforce on a daily basis, configure the fields below') }} -
-
- - {{ t('Hour') }} - - Enter the hour when the update should be run daily (0-23) Timezone: America/New_York - - - {{ t('Minute') }} - - Enter the minute after the hour when the update should be run daily (0-59) - -
-
- - {{ t('Logging Email') }} - - Enter the e-mail address to use when reporting errors during the Salesforce updating process - @if (salesforceForm.get('salesforceConfig.logging_email').hasError('email')) { - Please enter a valid email address - } - -
- @if (salesforceConfig) { -
-
- {{ t('Last Salesforce Update') }}: {{ salesforceConfig.last_update_date || 'N/A' }} -
- @if (salesforceConfig.last_update_date) { - - } + @if (salesforceForm.controls.salesforce_enabled.value) { +
+ +
{{ t('Salesforce Connection') }}
- } - +
{{ t('Enter your Salesforce instance details and ensure your connection is successful') }}
+
+ + {{ t('Salesforce URL') }} + + +
+
+ + {{ t('Username') }} + + +
+
+ + {{ t('Password') }} + + + +
+
+ + {{ t('Security Token') }} + + + {{ t('Security token set in Salesforce') }} + +
+
+ + {{ t('Domain') }} + + {{ + t("If your Salesforce instance is a sandbox, set this field to the value 'test'; otherwise leave blank.") + }} + +
+
+ +
+ @if (testConnectionStatus === 'success') { +
+ + {{ t('Salesforce connection successful') }} +
+ } + @if (testConnectionStatus === 'error') { +
+ + {{ t('Connection error') }}{{ testConnectionMessage ? ': ' + testConnectionMessage : '' }} +
+ } + -
- -
{{ t('Configuration') }}
-
-
{{ t('Configure a few parameters needed for data transfer to Salesforce') }}
-
- - {{ t('Indication Label') }} - - @for (l of labels; track l.id) { - {{ l.name }} - } - - Label used to designate that a SEED property should be updated in Salesforce. Example: 'Add to Salesforce' - -
-
- - - {{ t('Delete Indication Label After Successful Salesforce Update') }} - - - - Check this checkbox to automatically remove the Indication Label from properties that were successfully updated in - Salesforce in order to prevent future automatic updates. - -
-
- - {{ t('Violation Label') }} - - @for (l of labels; track l.id) { - {{ l.name }} +
+ +
{{ t('Scheduled Daily Updates') }}
+
+
+ {{ t('If you would like to automatically update Salesforce on a daily basis, configure the fields below') }} +
+
+ + {{ t('Hour') }} + + Enter the hour when the update should be run daily (0-23) Timezone: America/New_York + +
+
+ + {{ t('Minute') }} + + Enter the minute after the hour when the update should be run daily (0-59) + +
+
+ + {{ t('Logging Email') }} + + Enter the e-mail address to use when reporting errors during the Salesforce updating process + @if (salesforceForm.get('salesforceConfig.logging_email').hasError('email')) { + Please enter a valid email address } - - Label used to designate that a SEED property has a violation. Example: 'Violation - Insufficient Data' - -
-
- - {{ t('Compliance Label') }} - - @for (l of labels; track l.id) { - {{ l.name }} + +
+ @if (salesforceConfig) { +
+
+ {{ t('Last Salesforce Update') }}: {{ salesforceConfig.last_update_date || 'N/A' }} +
+ @if (salesforceConfig.last_update_date) { + } - - Label used to designate that a SEED property is in compliance. Example: 'Complied' - -
- - -
- -
{{ t('Contacts and Accounts') }}
-
-
- {{ - t( - 'Configure the contact information used for benchmark status notifications in Salesforce. When configured, this functionality will create or update contact records in Salesforce and link them to the Benchmark object' - ) - }} -
-
- - {{ t('Salesforce Account Object Record Type') }} - - If your Salesforce instance has multiple account types, provide the Record Type ID of the type of account to use when - accounts are automatically created from SEED - -
-
- - {{ t('Salesforce Contact Object Record Type') }} - - If your Salesforce instance has multiple contact types, provide the Record Type ID of the type to use when contacts - are automatically created from SEED - -
+
+ } + -
{{ t('Main Contact Settings') }}
-
- {{ - t( - 'The following fields will be used to retrieve or create a main contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' - ) - }} -
-
- - {{ t('Contact Account Name Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the account name for the contact record to be created in Salesforce. Ex: - 'Organization' - -
-
- - {{ t('Default Contact Account Name') }} - - Provide a default account name for Salesforce to use when there is no valid data in the Contact Account Name Column - specified above. Leave this field blank to report an error and abort sync instead when the Contact Account Name Column - is blank. - -
-
- - {{ t('Contact Name Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the contact name for the benchmark record. Ex: 'On Behalf Of' - -
-
- - {{ t('Contact Email Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the contact email for the benchmark record. Ex: 'Email' - -
-
- - {{ t('Contact Benchmark Field') }} - - If your Salesforce Benchmark Record stores a Salesforce Contact relation, provide the Salesforce field name here, ex: - Contact_Name__c - -
-
{{ t('Data Administrator Contact Settings') }}
-
- {{ - t( - 'The following fields will be used to retrieve or create a property data administrator contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' - ) - }} -
-
- - {{ t('Data Administrator Account Name Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the account name for the data administrator contact record to be created in - Salesforce. Ex: 'Organization' - -
-
- - {{ t('Default Data Administrator Account Name') }} - - Provide a default account name for Salesforce to use when there is no valid data in the Data Administrator Account - Name Column specified above. Leave this field blank to report an error and abort sync instead when the Data - Administrator Account Name Column is blank - -
-
- - {{ t('Data Administrator Name Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the property data administrator's name for the benchmark record. Ex: 'Property Data - Administrator' - -
-
- - {{ t('Data Administrator Email Column') }} - - @for (c of columns; track c.id) { - {{ c.display_name }} - } - - Select the SEED field that holds the property data administrator's email for the benchmark record. Ex: 'Property Data - Administrator - Email' - -
-
- - {{ t('Data Administrator Contact Field') }} - - - If your Salesforce Benchmark Record stores a Salesforce Contact relation for a Property Data Administrator, provide - the Salesforce field name here, ex: Property_Data_Administrator__c +
+ +
{{ t('Configuration') }}
+
+
{{ t('Configure a few parameters needed for data transfer to Salesforce') }}
+
+ + {{ t('Indication Label') }} + + @for (l of labels; track l.id) { + {{ l.name }} + } + + Label used to designate that a SEED property should be updated in Salesforce. Example: 'Add to + Salesforce' + +
+
+ + + {{ t('Delete Indication Label After Successful Salesforce Update') }} + + + + Check this checkbox to automatically remove the Indication Label from properties that were successfully updated in + Salesforce in order to prevent future automatic updates. - -
- +
+
+ + {{ t('Violation Label') }} + + @for (l of labels; track l.id) { + {{ l.name }} + } + + Label used to designate that a SEED property has a violation. Example: 'Violation - Insufficient Data' + +
+
+ + {{ t('Compliance Label') }} + + @for (l of labels; track l.id) { + {{ l.name }} + } + + Label used to designate that a SEED property is in compliance. Example: 'Complied' + +
+ + +
+ +
{{ t('Benchmark Configuration') }}
+
+
+ {{ t('Configure the fields used to identify and update benchmark records in Salesforce') }} +
+
+ + {{ t('Salesforce Unique Benchmark ID Fieldname') }} + + The API field name in the Salesforce Benchmark object that stores the unique identifier. Ex: + Unique_Benchmark_ID__c + +
+
+ + {{ t('SEED Unique Benchmark ID Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED property column that contains the unique identifier matching the Salesforce Benchmark ID + field + +
+
+ + {{ t('Cycle Name Benchmark Field') }} + + The API field name in the Salesforce Benchmark object that stores the cycle name. Ex: Cycle__c + +
+
+ + {{ t('Status Label Benchmark Field') }} + + The API field name in the Salesforce Benchmark object that stores the status label. Ex: Status__c + +
+
+ + {{ t('All Labels Benchmark Field') }} + + The API field name in the Salesforce Benchmark object that stores all labels. Ex: Labels__c + +
+ + +
+ +
{{ t('Contacts and Accounts') }}
+
+
+ {{ + t( + 'Configure the contact information used for benchmark status notifications in Salesforce. When configured, this functionality will create or update contact records in Salesforce and link them to the Benchmark object' + ) + }} +
+
+ + {{ t('Salesforce Account Object Record Type') }} + + If your Salesforce instance has multiple account types, provide the Record Type ID of the type of account to use + when accounts are automatically created from SEED + +
+
+ + {{ t('Salesforce Contact Object Record Type') }} + + If your Salesforce instance has multiple contact types, provide the Record Type ID of the type to use when contacts + are automatically created from SEED + +
+ +
{{ t('Main Contact Settings') }}
+
+ {{ + t( + 'The following fields will be used to retrieve or create a main contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' + ) + }} +
+
+ + {{ t('Contact Account Name Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the account name for the contact record to be created in Salesforce. Ex: + 'Organization' + +
+
+ + {{ t('Default Contact Account Name') }} + + Provide a default account name for Salesforce to use when there is no valid data in the Contact Account Name Column + specified above. Leave this field blank to report an error and abort sync instead when the Contact Account Name + Column is blank. + +
+
+ + {{ t('Contact Name Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the contact name for the benchmark record. Ex: 'On Behalf Of' + +
+
+ + {{ t('Contact Email Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the contact email for the benchmark record. Ex: 'Email' + +
+
+ + {{ t('Contact Benchmark Field') }} + + If your Salesforce Benchmark Record stores a Salesforce Contact relation, provide the Salesforce field name here, + ex: Contact_Name__c + +
+
{{ t('Data Administrator Contact Settings') }}
+
+ {{ + t( + 'The following fields will be used to retrieve or create a property data administrator contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' + ) + }} +
+
+ + {{ t('Data Administrator Account Name Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the account name for the data administrator contact record to be created in + Salesforce. Ex: 'Organization' + +
+
+ + {{ t('Default Data Administrator Account Name') }} + + Provide a default account name for Salesforce to use when there is no valid data in the Data Administrator Account + Name Column specified above. Leave this field blank to report an error and abort sync instead when the Data + Administrator Account Name Column is blank + +
+
+ + {{ t('Data Administrator Name Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the property data administrator's name for the benchmark record. Ex: 'Property + Data Administrator' + +
+
+ + {{ t('Data Administrator Email Column') }} + + @for (c of columns; track c.id) { + {{ c.display_name }} + } + + Select the SEED field that holds the property data administrator's email for the benchmark record. Ex: 'Property + Data Administrator - Email' + +
+
+ + {{ t('Data Administrator Contact Field') }} + + + If your Salesforce Benchmark Record stores a Salesforce Contact relation for a Property Data Administrator, provide + the Salesforce field name here, ex: Property_Data_Administrator__c + + +
+ + }
diff --git a/src/app/modules/organizations/settings/salesforce/salesforce.component.ts b/src/app/modules/organizations/settings/salesforce/salesforce.component.ts index 0c9971f5..465dad4b 100644 --- a/src/app/modules/organizations/settings/salesforce/salesforce.component.ts +++ b/src/app/modules/organizations/settings/salesforce/salesforce.component.ts @@ -26,6 +26,8 @@ export class SalesforceComponent implements OnDestroy, OnInit { private _dialog = inject(MatDialog) passwordHidden = true tokenHidden = true + testConnectionStatus: 'idle' | 'success' | 'error' = 'idle' + testConnectionMessage = '' labels: Label[] columns: Column[] organization: Organization @@ -74,6 +76,7 @@ export class SalesforceComponent implements OnDestroy, OnInit { this._organizationService.currentOrganization$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((organization) => { this.organization = organization this.salesforceForm.get('salesforce_enabled').setValue(this.organization.salesforce_enabled) + this._setFormEnabledState(this.organization.salesforce_enabled) }) this._salesforceService.config$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((config) => { this.salesforceConfig = config @@ -186,22 +189,25 @@ export class SalesforceComponent implements OnDestroy, OnInit { } testConnection(): void { + this.testConnectionStatus = 'idle' this.updateConfig() - this._salesforceService.test_connection(this.organization.id, this.salesforceConfig).subscribe() + this._salesforceService.test_connection(this.organization.id, this.salesforceConfig).subscribe({ + next: () => { + this.testConnectionStatus = 'success' + this.testConnectionMessage = '' + }, + error: (error: { message?: string }) => { + this.testConnectionStatus = 'error' + this.testConnectionMessage = error?.message || 'Connection failed' + }, + }) } toggleForm(): void { const enabled = this.salesforceForm.get('salesforce_enabled').value this.organization.salesforce_enabled = enabled this._organizationService.updateSettings(this.organization).subscribe() - const fg = this.salesforceForm.get('salesforceConfig') as FormGroup - for (const field of Object.keys(fg.controls)) { - if (enabled) { - this.salesforceForm.get(`salesforceConfig.${field}`).enable() - } else { - this.salesforceForm.get(`salesforceConfig.${field}`).disable() - } - } + this._setFormEnabledState(enabled) } updateConfig(): void { @@ -226,4 +232,15 @@ export class SalesforceComponent implements OnDestroy, OnInit { } } } + + private _setFormEnabledState(enabled: boolean): void { + const fg = this.salesforceForm.get('salesforceConfig') as FormGroup + for (const field of Object.keys(fg.controls)) { + if (enabled) { + this.salesforceForm.get(`salesforceConfig.${field}`).enable() + } else { + this.salesforceForm.get(`salesforceConfig.${field}`).disable() + } + } + } } From 89a234ef3ceb77a40c8c9775af6a90afc617b1db Mon Sep 17 00:00:00 2001 From: Katherine Fleming <2205659+kflemin@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:00:46 -0600 Subject: [PATCH 2/2] salesforce bb configs --- public/i18n/en_US.json | 14 ++ public/i18n/es.json | 14 ++ public/i18n/fr_CA.json | 14 ++ .../api/organization/organization.types.ts | 1 + .../api/salesforce/bb-salesforce.service.ts | 132 ++++++++++ src/@seed/api/salesforce/index.ts | 1 + src/@seed/api/salesforce/salesforce.types.ts | 37 +++ .../salesforce/salesforce.component.html | 229 ++++++++++-------- .../salesforce/salesforce.component.ts | 101 +++++++- 9 files changed, 443 insertions(+), 100 deletions(-) create mode 100644 src/@seed/api/salesforce/bb-salesforce.service.ts diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index a9720a7b..d4b6f918 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -169,6 +169,7 @@ "Back to Mapping": "Back to Mapping", "Baseline Cycle": "Baseline Cycle", "Begin Update": "Begin Update", + "Benchmark Configuration": "Benchmark Configuration", "Benchmarking": "Benchmarking", "Block Number": "Block Number", "Body": "Body", @@ -243,6 +244,8 @@ "Classification Type": "Classification Type", "Clear Filters": "Clear Filters", "Clear Labels": "Clear Labels", + "Client ID": "Client ID", + "Client Secret": "Client Secret", "Close": "Close", "Close Preview": "Close Preview", "Collapse Tabs": "Collapse Tabs", @@ -287,7 +290,10 @@ "Confirm Save Mappings?": "Confirm Save Mappings?", "Confirm delete": "Confirm delete", "Confirm new password": "Confirm new password", + "Connected to Salesforce": "Connected to Salesforce", "Connection": "Connection", + "Connection Status": "Connection Status", + "Connection error": "Connection error", "Contact": "Contact", "Contact Account Name Column": "Contact Account Name Column", "Contact Benchmark Field": "Contact Benchmark Field", @@ -498,6 +504,8 @@ "Enable Public Endpoints": "Enable Public Endpoints", "Enable Public GeoJSON": "Enable Public GeoJSON", "Enable Salesforce Integration": "Enable Salesforce Integration", + "Enable Salesforce Integration (Individual Properties)": "Enable Salesforce Integration (Individual Properties)", + "Enable Salesforce Integration (Portfolio of Properties)": "Enable Salesforce Integration (Portfolio of Properties)", "Energy": "Energy", "Energy Alerts": "Energy Alerts", "Energy Capacity (kWh)": "Energy Capacity (kWh)", @@ -755,6 +763,7 @@ "Log in": "Log in", "Log in to SEED Platform": "Log in to SEED Platform", "Logging Email": "Logging Email", + "Login": "Login", "Logout": "Logout", "Longitude": "Longitude", "MAPPING YOUR DATA TO SEED": "MAPPING YOUR DATA TO SEED", @@ -902,6 +911,7 @@ "Not Compliant": "Not Compliant", "Not Null": "Not Null", "Not all inventory items were successfully deleted": "Not all inventory items were successfully deleted", + "Not connected to Salesforce": "Not connected to Salesforce", "Not seeing your column?": "Not seeing your column?", "Note:": "Note:", "Note: Meters are labeled with the following format: \"Type - Source - Source ID\"": "Note: Meters are labeled with the following format: \"Type - Source - Source ID\"", @@ -1152,6 +1162,7 @@ "SF_ACCOUNT_RECORD_TYPE_TEXT": "If your Salesforce instance has multiple account types, provide the Record Type ID of the type of account to use when accounts are automatically created from SEED", "SF_BENCHMARK_CONTACT_FIELDNAME_TEXT": "If your Salesforce Benchmark Record stores a Salesforce Contact relation, provide the Salesforce field name here, ex: Contact_Name__c", "SF_BENCHMARK_ID_FIELDNAME_TEXT": "Fieldname of the Salesforce field that is used to uniquely identify Benchmark records. Example: 'Salesforce_Benchmark_ID__c'", + "SF_BUILDING_DESC": "This integration allows you to connect your organization with a Salesforce instance for enhanced data management and workflow automation. This integration assumes Salesforce is configured with Property and Benchmark objects, which will be updated with individual property data from SEED.", "SF_COMPLIANCE_LABEL_TEXT": "Label used to designate that a SEED property is in compliance. Example: 'Complied'", "SF_CONFIGURATION_TEXT": "Configure a few parameters needed for data transfer to Salesforce", "SF_CONNECTION_TEXT": "Enter your Salesforce instance details and ensure your connection is successful", @@ -1175,6 +1186,7 @@ "SF_LABELS_FIELDNAME_TEXT": "If your Salesforce Benchmark Record stores a string of all SEED labels applied, please enter the name of the Salesforce field here, ex: SEED_Labels__c", "SF_LOGGING_EMAIL_TEXT": "Enter the e-mail address to use when reporting errors during the Salesforce updating process", "SF_MAPPINGS_TEXT": "Map your Salesforce Benchmark Object's fields to columns in SEED. Use the Object Manager in Salesforce to retrieve the field names (not the field labels), ex: Cycle__c (not Cycle). These fields will only be sent to Salesforce when a property has the compliant label applied.", + "SF_PORTFOLIO_DESC": "This integration also allows you to connect your organization with a Salesforce instance, but assumes Salesforce is configured with Goal and Annual Report objects which will be updated with portfolio-level data (aggregated from multiple properties in SEED). ", "SF_SCHEDULED_UPDATE_TEXT": "If you would like to automatically update Salesforce on a daily basis, configure the fields below", "SF_SECURITY_TOKEN_TEXT": "Security token set in Salesforce", "SF_SEED_BENCHMARK_ID_FIELDNAME_TEXT": "Fieldname of the SEED field that is used to uniquely identify Benchmark records. Example: 'Salesforce Benchmark ID'", @@ -1206,6 +1218,7 @@ "SUGGEST_UPDATE_GEOCODE_COLS": "Update the geocoding column settings in your organization's column settings page.", "Salesforce": "Salesforce", "Salesforce Account Object Record Type": "Salesforce Account Object Record Type", + "Salesforce Connection": "Salesforce Connection", "Salesforce Connection Parameters": "Salesforce Connection Parameters", "Salesforce Contact Object Record Type": "Salesforce Contact Object Record Type", "Salesforce Field Mappings": "Salesforce Field Mappings", @@ -1213,6 +1226,7 @@ "Salesforce Instance URL": "Salesforce URL", "Salesforce Integration": "Salesforce Integration", "Salesforce Unique Benchmark ID Fieldname": "Salesforce Unique Benchmark ID Fieldname", + "Salesforce connection successful": "Salesforce connection successful", "Save": "Save", "Save Changes": "Save Changes", "Save Concatenation": "Save Concatenation", diff --git a/public/i18n/es.json b/public/i18n/es.json index acb04242..359adca9 100644 --- a/public/i18n/es.json +++ b/public/i18n/es.json @@ -169,6 +169,7 @@ "Back to Mapping": "Volver a Cartografía", "Baseline Cycle": "Ciclo de referencia", "Begin Update": "Iniciar actualización", + "Benchmark Configuration": "Configuración de referencia", "Benchmarking": "Evaluación comparativa", "Block Number": "Número de bloque", "Body": "Cuerpo", @@ -243,6 +244,8 @@ "Classification Type": "Tipo de clasificación", "Clear Filters": "Limpiar filtros", "Clear Labels": "Etiquetas transparentes", + "Client ID": "ID de cliente", + "Client Secret": "Secreto del cliente", "Close": "Cerrar", "Close Preview": "Cerrar Vista previa", "Collapse Tabs": "Contraer pestañas", @@ -287,7 +290,10 @@ "Confirm Save Mappings?": "¿Confirmar guardar asignaciones?", "Confirm delete": "Confirmar eliminación", "Confirm new password": "Confirmar nueva contraseña", + "Connected to Salesforce": "Conectado a Salesforce", "Connection": "Conexión", + "Connection Status": "Estado de la conexión", + "Connection error": "Error de conexión", "Contact": "Póngase en contacto con", "Contact Account Name Column": "Columna Nombre de la cuenta de contacto", "Contact Benchmark Field": "Póngase en contacto con Benchmark Field", @@ -498,6 +504,8 @@ "Enable Public Endpoints": "Habilitar puntos finales públicos", "Enable Public GeoJSON": "Activar GeoJSON público", "Enable Salesforce Integration": "Activar la integración de Salesforce", + "Enable Salesforce Integration (Individual Properties)": "Habilitar la integración con Salesforce (propiedades individuales)", + "Enable Salesforce Integration (Portfolio of Properties)": "Habilitar la integración con Salesforce (Cartera de propiedades)", "Energy": "Energía", "Energy Alerts": "Alertas de energía", "Energy Capacity (kWh)": "Capacidad energética (kWh)", @@ -755,6 +763,7 @@ "Log in": "Conectarse", "Log in to SEED Platform": "Iniciar sesión en la Plataforma SEED", "Logging Email": "Correo electrónico de registro", + "Login": "Acceso", "Logout": "Cierre de sesión", "Longitude": "Longitud", "MAPPING YOUR DATA TO SEED": "ASIGNAR SUS DATOS A LAS SEMILLAS", @@ -902,6 +911,7 @@ "Not Compliant": "No conforme", "Not Null": "No nulo", "Not all inventory items were successfully deleted": "No se han eliminado correctamente todos los artículos del inventario", + "Not connected to Salesforce": "No está conectado a Salesforce", "Not seeing your column?": "¿No ves tu columna?", "Note:": "Nota:", "Note: Meters are labeled with the following format: \"Type - Source - Source ID\"": "Nota: Los contadores se etiquetan con el siguiente formato: \"Tipo - Fuente - ID de fuente\"", @@ -1152,6 +1162,7 @@ "SF_ACCOUNT_RECORD_TYPE_TEXT": "Si su instancia de Salesforce tiene varios tipos de cuenta, proporcione el ID de tipo de registro del tipo de cuenta que se utilizará cuando las cuentas se creen automáticamente desde SEED", "SF_BENCHMARK_CONTACT_FIELDNAME_TEXT": "Si su registro de referencia de Salesforce almacena una relación de contacto de Salesforce, indique aquí el nombre del campo de Salesforce, por ejemplo: Contact_Name__c", "SF_BENCHMARK_ID_FIELDNAME_TEXT": "Nombre de campo del campo de Salesforce que se utiliza para identificar de forma exclusiva los registros de Benchmark. Ejemplo: 'Salesforce_Benchmark_ID__c'", + "SF_BUILDING_DESC": "Esta integración permite conectar su organización con una instancia de Salesforce para optimizar la gestión de datos y la automatización de flujos de trabajo. Esta integración presupone que Salesforce está configurado con objetos de Propiedad y Referencia, que se actualizarán con los datos individuales de las propiedades desde SEED.", "SF_COMPLIANCE_LABEL_TEXT": "Etiqueta utilizada para designar que una propiedad SEED es conforme. Ejemplo: \"Cumple\"", "SF_CONFIGURATION_TEXT": "Configure algunos parámetros necesarios para la transferencia de datos a Salesforce", "SF_CONNECTION_TEXT": "Introduzca los detalles de su instancia de Salesforce y asegúrese de que la conexión se realiza correctamente", @@ -1175,6 +1186,7 @@ "SF_LABELS_FIELDNAME_TEXT": "Si su registro de referencia de Salesforce almacena una cadena de todas las etiquetas de SEED aplicadas, introduzca aquí el nombre del campo de Salesforce, por ejemplo: SEED_Labels__c", "SF_LOGGING_EMAIL_TEXT": "Introduzca la dirección de correo electrónico que se utilizará para notificar errores durante el proceso de actualización de Salesforce", "SF_MAPPINGS_TEXT": "Asigne los campos de su objeto de referencia de Salesforce a columnas en SEED. Utilice el Administrador de Objetos en Salesforce para recuperar los nombres de campo (no las etiquetas de campo), ej: Cycle__c (no Cycle). Estos campos sólo se enviarán a Salesforce cuando una propiedad tenga aplicada la etiqueta correspondiente.", + "SF_PORTFOLIO_DESC": "Esta integración también le permite conectar su organización con una instancia de Salesforce, pero presupone que Salesforce está configurado con objetos de Objetivo e Informe Anual que se actualizarán con datos a nivel de cartera (agregados a partir de múltiples propiedades en SEED). ", "SF_SCHEDULED_UPDATE_TEXT": "Si desea actualizar automáticamente Salesforce a diario, configure los siguientes campos", "SF_SECURITY_TOKEN_TEXT": "Token de seguridad establecido en Salesforce", "SF_SEED_BENCHMARK_ID_FIELDNAME_TEXT": "Nombre de campo del campo SEED que se utiliza para identificar de forma exclusiva los registros de Benchmark. Ejemplo: 'Salesforce Benchmark ID'", @@ -1206,6 +1218,7 @@ "SUGGEST_UPDATE_GEOCODE_COLS": "Actualice la configuración de la columna de geocodificación en la página de configuración de columnas de su organización.", "Salesforce": "Salesforce", "Salesforce Account Object Record Type": "Tipo de registro de objeto de cuenta de Salesforce", + "Salesforce Connection": "Conexión con Salesforce", "Salesforce Connection Parameters": "Parámetros de conexión de Salesforce", "Salesforce Contact Object Record Type": "Tipo de registro de objeto de contacto de Salesforce", "Salesforce Field Mappings": "Asignaciones de campos de Salesforce", @@ -1213,6 +1226,7 @@ "Salesforce Instance URL": "URL de Salesforce", "Salesforce Integration": "Integración con Salesforce", "Salesforce Unique Benchmark ID Fieldname": "ID único de referencia de Salesforce Nombre de campo", + "Salesforce connection successful": "Conexión con Salesforce exitosa", "Save": "Guardar", "Save Changes": "Guardar cambios", "Save Concatenation": "Guardar concatenación", diff --git a/public/i18n/fr_CA.json b/public/i18n/fr_CA.json index f22b8005..6f17bb6f 100644 --- a/public/i18n/fr_CA.json +++ b/public/i18n/fr_CA.json @@ -169,6 +169,7 @@ "Back to Mapping": "Retournez à les mappages", "Baseline Cycle": "Cycle de référence", "Begin Update": "Démarrer la mise à jour", + "Benchmark Configuration": "Configuration de référence", "Benchmarking": "Analyse comparative", "Block Number": "Numéro de bloc", "Body": "Corps", @@ -243,6 +244,8 @@ "Classification Type": "Type de classification", "Clear Filters": "Effacer les filtres", "Clear Labels": "Effacer les étiquettes", + "Client ID": "ID client", + "Client Secret": "Secret du client", "Close": "Fermer", "Close Preview": "Fermer l'aperçu", "Collapse Tabs": "Réduire les onglets", @@ -287,7 +290,10 @@ "Confirm Save Mappings?": "Confirmer enregistrer les mappages?", "Confirm delete": "Confirmer la supprimation", "Confirm new password": "Confirmer le nouveau mot de passe", + "Connected to Salesforce": "Connecté à Salesforce", "Connection": "Connexion", + "Connection Status": "État de la connexion", + "Connection error": "Erreur de connexion", "Contact": "Contact", "Contact Account Name Column": "Colonne Nom du compte de contact", "Contact Benchmark Field": "Nom de champ Contact pour l'object Benchmark", @@ -498,6 +504,8 @@ "Enable Public Endpoints": "Activer les points de terminaison publics", "Enable Public GeoJSON": "Activer GeoJSON public", "Enable Salesforce Integration": "Activer la fonctionnalité Salesforce", + "Enable Salesforce Integration (Individual Properties)": "Activer l'intégration Salesforce (propriétés individuelles)", + "Enable Salesforce Integration (Portfolio of Properties)": "Activer l'intégration Salesforce (Portefeuille de propriétés)", "Energy": "Énergie", "Energy Alerts": "Alertes énergétiques", "Energy Capacity (kWh)": "Capacité énergétique (kWh)", @@ -755,6 +763,7 @@ "Log in": "S'identifier", "Log in to SEED Platform": "Connectez-vous à SEED Platform", "Logging Email": "Adresse courriel pour reportage", + "Login": "Se connecter", "Logout": "Déconnexion", "Longitude": "Longitude", "MAPPING YOUR DATA TO SEED": "MAPPAGE DE VOS DONNÉES À SEED", @@ -902,6 +911,7 @@ "Not Compliant": "Non conforme", "Not Null": "Pas nul", "Not all inventory items were successfully deleted": "Tous les éléments de l'inventaire n'ont pas été supprimés", + "Not connected to Salesforce": "Non connecté à Salesforce", "Not seeing your column?": "Vous ne voyez pas votre colonne?", "Note:": "Remarque:", "Note: Meters are labeled with the following format: \"Type - Source - Source ID\"": "Remarque: Les compteurs sont étiquetés au format suivant: \"la catégorie - Source - Source ID\"", @@ -1152,6 +1162,7 @@ "SF_ACCOUNT_RECORD_TYPE_TEXT": "Si votre Salesforce a plusieurs types de Compte, fournissez le 'Record Type ID' à utiliser lorsqu'un compte est automatiquement créé par SEED", "SF_BENCHMARK_CONTACT_FIELDNAME_TEXT": "Si votre objet Benchmark a un champ pour capturer le Contact, inscrivez le nom du champ dans l'application Salesforce, ex: Contact_Name__c", "SF_BENCHMARK_ID_FIELDNAME_TEXT": "Nom du champ dans l'application Salesforce utilisé pour identifier uniquement un object 'Benchmark'. Example: 'Salesforce_Benchmark_ID__c'", + "SF_BUILDING_DESC": "Cette intégration vous permet de connecter votre organisation à une instance Salesforce pour une gestion des données optimisée et une automatisation des flux de travail. Elle suppose que Salesforce est configuré avec les objets Propriété et Benchmark, qui seront mis à jour avec les données de chaque propriété issues de SEED.", "SF_COMPLIANCE_LABEL_TEXT": "Étiquette qui signale qu'une propriété est en conformité. Example: 'Comforme'", "SF_CONFIGURATION_TEXT": "Configurez les paramètres necessaires pour transferez les données à Salesforce", "SF_CONNECTION_TEXT": "Inscrivez vos détails pour la connexion Salesforce", @@ -1175,6 +1186,7 @@ "SF_LABELS_FIELDNAME_TEXT": "Si votre objet Benchmark a un champ pour capturer tous les étiquettes SEED, inscrivez le nom du champ dans l'application Salesforce, ex: SEED_Labels__c", "SF_LOGGING_EMAIL_TEXT": "Inscrivez l'adresse courriel à utiliser pour le reportage des errors rencontrée pendant la mise à jour Salesforce", "SF_MAPPINGS_TEXT": "Associez les champs de données de votre objet Benchmark dans l'application Salesforce avec le nom du champ dans l'application SEED. Vous pouvez utiliser le gestionnaire d'object dans Salesforce pour trouver les noms de champs (et non les étiquettes de champs)", + "SF_PORTFOLIO_DESC": "Cette intégration vous permet également de connecter votre organisation à une instance Salesforce, mais suppose que Salesforce est configuré avec des objets Objectif et Rapport annuel qui seront mis à jour avec des données au niveau du portefeuille (agrégées à partir de plusieurs propriétés dans SEED). ", "SF_SCHEDULED_UPDATE_TEXT": "Si vous désirez faire une mise à jour automatique de l'application Salesforce quotidiennement, configurez les champs ci-dessous", "SF_SECURITY_TOKEN_TEXT": "Jeton d'authentification Salesforce", "SF_SEED_BENCHMARK_ID_FIELDNAME_TEXT": "Nom du champ dans SEED utilisé pour identifier uniquement un object 'Benchmark'. Example: 'Salesforce Benchmark ID'", @@ -1206,6 +1218,7 @@ "SUGGEST_UPDATE_GEOCODE_COLS": "Mettez à jour les paramètres de colonne de géocodage dans la page des paramètres de colonne de votre organisation.", "Salesforce": "Salesforce", "Salesforce Account Object Record Type": "Type d'enregistrement pour Compte Salesforce", + "Salesforce Connection": "Connexion Salesforce", "Salesforce Connection Parameters": "Paramètres pour la Connexion Salesforce", "Salesforce Contact Object Record Type": "Type d'enregistrement pour Contact Salesforce", "Salesforce Field Mappings": "Attribution de champs Salesforce", @@ -1213,6 +1226,7 @@ "Salesforce Instance URL": "Salesforce URL", "Salesforce Integration": "Intégration de Salesforce", "Salesforce Unique Benchmark ID Fieldname": "Unique nom de champ pour Benchmark ID Salesforce", + "Salesforce connection successful": "Connexion à Salesforce réussie", "Save": "Enregistrer", "Save Changes": "Enregistrer Les Modifications", "Save Concatenation": "Enregistrer la concaténation", diff --git a/src/@seed/api/organization/organization.types.ts b/src/@seed/api/organization/organization.types.ts index c4192fbc..e45a638c 100644 --- a/src/@seed/api/organization/organization.types.ts +++ b/src/@seed/api/organization/organization.types.ts @@ -23,6 +23,7 @@ export type BriefOrganization = { user_role: UserRole; display_decimal_places: number; salesforce_enabled: boolean; + bb_salesforce_enabled: boolean; access_level_names: string[]; audit_template_conditional_import: boolean; property_display_field: string; diff --git a/src/@seed/api/salesforce/bb-salesforce.service.ts b/src/@seed/api/salesforce/bb-salesforce.service.ts new file mode 100644 index 00000000..05c1b0ea --- /dev/null +++ b/src/@seed/api/salesforce/bb-salesforce.service.ts @@ -0,0 +1,132 @@ +import type { HttpErrorResponse } from '@angular/common/http' +import { HttpClient } from '@angular/common/http' +import { inject, Injectable } from '@angular/core' +import type { Observable } from 'rxjs' +import { catchError, map, ReplaySubject } from 'rxjs' +import { ErrorService } from '@seed/services' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import { UserService } from '../user' +import type { + BbSalesforceConfig, + BbSalesforceConfigResponse, + BbSalesforceConfigsResponse, + BbSalesforceLoginUrlResponse, + BbSalesforceLogoutResponse, + BbSalesforceTokenResponse, + BbSalesforceVerifyTokenResponse, +} from './salesforce.types' + +@Injectable({ providedIn: 'root' }) +export class BbSalesforceService { + private _httpClient = inject(HttpClient) + private _errorService = inject(ErrorService) + private _snackBar = inject(SnackBarService) + private _userService = inject(UserService) + + private _config = new ReplaySubject(1) + + config$ = this._config.asObservable() + + constructor() { + this._userService.currentOrganizationId$.subscribe((organizationId) => { + this.getConfig(organizationId).subscribe() + }) + } + + getConfig(organizationId: number): Observable { + const url = `/api/v3/bb_salesforce/configs/?organization_id=${organizationId}` + return this._httpClient.get(url).pipe( + map((response) => { + if (response.bb_salesforce_configs.length === 0) { + this._config.next({} as BbSalesforceConfig) + } else { + this._config.next(response.bb_salesforce_configs[0]) + } + return response + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching BB Salesforce config') + }), + ) + } + + create(organizationId: number, config: Partial): Observable { + const url = `/api/v3/bb_salesforce/configs/?organization_id=${organizationId}` + return this._httpClient.post(url, { ...config }).pipe( + map((response) => { + this._config.next(response.bb_salesforce_config) + return response.bb_salesforce_config + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error creating BB Salesforce config') + }), + ) + } + + update(organizationId: number, config: BbSalesforceConfig): Observable { + const url = `/api/v3/bb_salesforce/configs/update_config/?organization_id=${organizationId}` + return this._httpClient.put(url, { ...config }).pipe( + map((response) => { + this._snackBar.success('BB Salesforce configuration updated') + this._config.next(response.bb_salesforce_config) + return response.bb_salesforce_config + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error updating BB Salesforce config') + }), + ) + } + + delete(organizationId: number, configId: number): Observable { + const url = `/api/v3/bb_salesforce/configs/${configId}/?organization_id=${organizationId}` + return this._httpClient.delete(url).pipe( + map(() => { + this._config.next({} as BbSalesforceConfig) + return {} + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error deleting BB Salesforce config') + }), + ) + } + + getLoginUrl(organizationId: number): Observable { + const url = `/api/v3/bb_salesforce/login_url/?organization_id=${organizationId}` + return this._httpClient.get(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Cannot login to Salesforce. Double check the Salesforce login URL.') + }), + ) + } + + getToken(code: string, organizationId: number): Observable { + const url = `/api/v3/bb_salesforce/get_token/?code=${code}&organization_id=${organizationId}` + return this._httpClient.get(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error exchanging Salesforce authorization code') + }), + ) + } + + logout(organizationId: number): Observable { + const url = `/api/v3/bb_salesforce/logout/?organization_id=${organizationId}` + return this._httpClient.get(url).pipe( + map((response) => { + this._snackBar.success('Successfully logged out of Salesforce') + return response + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error logging out of Salesforce') + }), + ) + } + + verifyToken(organizationId: number): Observable { + const url = `/api/v3/bb_salesforce/verify_token/?organization_id=${organizationId}` + return this._httpClient.get(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error verifying Salesforce token') + }), + ) + } +} diff --git a/src/@seed/api/salesforce/index.ts b/src/@seed/api/salesforce/index.ts index 396aad80..74e6e7ba 100644 --- a/src/@seed/api/salesforce/index.ts +++ b/src/@seed/api/salesforce/index.ts @@ -1,2 +1,3 @@ +export * from './bb-salesforce.service' export * from './salesforce.service' export * from './salesforce.types' diff --git a/src/@seed/api/salesforce/salesforce.types.ts b/src/@seed/api/salesforce/salesforce.types.ts index d1e92ed9..4bf717d0 100644 --- a/src/@seed/api/salesforce/salesforce.types.ts +++ b/src/@seed/api/salesforce/salesforce.types.ts @@ -49,6 +49,43 @@ export type SalesforceMapping = { salesforce_fieldname: string; } +export type BbSalesforceConfig = { + id: number; + organization_id: number; + salesforce_url: string; + client_id: string; + client_secret: string; +} + +export type BbSalesforceConfigResponse = { + status: string; + bb_salesforce_config: BbSalesforceConfig; +} + +export type BbSalesforceConfigsResponse = { + status: string; + bb_salesforce_configs: BbSalesforceConfig[]; +} + +export type BbSalesforceLoginUrlResponse = { + status: string; + url?: string; + response?: string; +} + +export type BbSalesforceTokenResponse = { + status: string; + response?: string; +} + +export type BbSalesforceVerifyTokenResponse = { + valid: boolean; +} + +export type BbSalesforceLogoutResponse = { + status: string; +} + export type SalesforceMappingDeleteResponse = { status: string; message: string; diff --git a/src/app/modules/organizations/settings/salesforce/salesforce.component.html b/src/app/modules/organizations/settings/salesforce/salesforce.component.html index 7acb3d54..9eb8342f 100644 --- a/src/app/modules/organizations/settings/salesforce/salesforce.component.html +++ b/src/app/modules/organizations/settings/salesforce/salesforce.component.html @@ -7,11 +7,13 @@
-
Enable this if you would like to sync data between SEED and a Salesforce instance
+
+ {{ t('SF_BUILDING_DESC') }} +
{{ - t('Enable Salesforce Integration') + t('Enable Salesforce Integration (Individual Properties)') }}
@@ -21,7 +23,7 @@
{{ t('Salesforce Connection') }}
-
{{ t('Enter your Salesforce instance details and ensure your connection is successful') }}
+
{{ t('SF_CONNECTION_TEXT') }}
{{ t('Salesforce URL') }} @@ -64,16 +66,14 @@ > - {{ t('Security token set in Salesforce') }} + {{ t('SF_SECURITY_TOKEN_TEXT') }}
{{ t('Domain') }} - {{ - t("If your Salesforce instance is a sandbox, set this field to the value 'test'; otherwise leave blank.") - }} + {{ t('SF_DOMAIN_TEXT') }}
@@ -99,30 +99,30 @@
-
{{ t('Scheduled Daily Updates') }}
+
{{ t('Scheduled Daily Update') }}
- {{ t('If you would like to automatically update Salesforce on a daily basis, configure the fields below') }} + {{ t('SF_SCHEDULED_UPDATE_TEXT') }}
{{ t('Hour') }} - Enter the hour when the update should be run daily (0-23) Timezone: America/New_York + {{ t('SF_UPDATE_HOUR_TEXT') }}
{{ t('Minute') }} - Enter the minute after the hour when the update should be run daily (0-59) + {{ t('SF_UPDATE_MINUTE_TEXT') }}
{{ t('Logging Email') }} - Enter the e-mail address to use when reporting errors during the Salesforce updating process + {{ t('SF_LOGGING_EMAIL_TEXT') }} @if (salesforceForm.get('salesforceConfig.logging_email').hasError('email')) { Please enter a valid email address } @@ -148,7 +148,7 @@
{{ t('Configuration') }}
-
{{ t('Configure a few parameters needed for data transfer to Salesforce') }}
+
{{ t('SF_CONFIGURATION_TEXT') }}
{{ t('Indication Label') }} @@ -157,10 +157,7 @@ {{ l.name }} } - Label used to designate that a SEED property should be updated in Salesforce. Example: 'Add to - Salesforce' + {{ t('SF_INDICATION_LABEL_TEXT') }}
@@ -170,8 +167,7 @@ - Check this checkbox to automatically remove the Indication Label from properties that were successfully updated in - Salesforce in order to prevent future automatic updates. + {{ t('SF_DELETE_LABEL_AFTER_SYNC_TEXT') }}
@@ -182,9 +178,7 @@ {{ l.name }} } - Label used to designate that a SEED property has a violation. Example: 'Violation - Insufficient Data' + {{ t('SF_VIOLATION_LABEL_TEXT') }}
@@ -195,7 +189,7 @@ {{ l.name }} } - Label used to designate that a SEED property is in compliance. Example: 'Complied' + {{ t('SF_COMPLIANCE_LABEL_TEXT') }}
{{ t('Salesforce Unique Benchmark ID Fieldname') }} - The API field name in the Salesforce Benchmark object that stores the unique identifier. Ex: - Unique_Benchmark_ID__c + {{ t('SF_BENCHMARK_ID_FIELDNAME_TEXT') }}
@@ -229,31 +220,28 @@ {{ c.display_name }} } - Select the SEED property column that contains the unique identifier matching the Salesforce Benchmark ID - field + {{ t('SF_SEED_BENCHMARK_ID_FIELDNAME_TEXT') }}
{{ t('Cycle Name Benchmark Field') }} - The API field name in the Salesforce Benchmark object that stores the cycle name. Ex: Cycle__c + {{ t('SF_CYCLE_FIELDNAME_TEXT') }}
{{ t('Status Label Benchmark Field') }} - The API field name in the Salesforce Benchmark object that stores the status label. Ex: Status__c + {{ t('SF_STATUS_FIELDNAME_TEXT') }}
{{ t('All Labels Benchmark Field') }} - The API field name in the Salesforce Benchmark object that stores all labels. Ex: Labels__c + {{ t('SF_LABELS_FIELDNAME_TEXT') }}
{{ t('Contacts and Accounts') }}
- {{ - t( - 'Configure the contact information used for benchmark status notifications in Salesforce. When configured, this functionality will create or update contact records in Salesforce and link them to the Benchmark object' - ) - }} + {{ t('SF_CONTACTS_AND_ACCOUNTS_TEXT') }}
{{ t('Salesforce Account Object Record Type') }} - If your Salesforce instance has multiple account types, provide the Record Type ID of the type of account to use - when accounts are automatically created from SEED + {{ t('SF_ACCOUNT_RECORD_TYPE_TEXT') }}
{{ t('Salesforce Contact Object Record Type') }} - If your Salesforce instance has multiple contact types, provide the Record Type ID of the type to use when contacts - are automatically created from SEED + {{ t('SF_CONTACT_RECORD_TYPE_TEXT') }}
{{ t('Main Contact Settings') }}
- {{ - t( - 'The following fields will be used to retrieve or create a main contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' - ) - }} + {{ t('SF_CONTACT_HEADER_TEXT') }}
@@ -310,21 +284,14 @@ {{ c.display_name }} } - Select the SEED field that holds the account name for the contact record to be created in Salesforce. Ex: - 'Organization' + {{ t('SF_ACCOUNT_NAME_FIELDNAME_TEXT') }}
{{ t('Default Contact Account Name') }} - Provide a default account name for Salesforce to use when there is no valid data in the Contact Account Name Column - specified above. Leave this field blank to report an error and abort sync instead when the Contact Account Name - Column is blank. + {{ t('SF_DEFAULT_ACCOUNT_NAME_TEXT') }}
@@ -335,7 +302,7 @@ {{ c.display_name }} } - Select the SEED field that holds the contact name for the benchmark record. Ex: 'On Behalf Of' + {{ t('SF_CONTACT_NAME_FIELDNAME_TEXT') }}
@@ -346,26 +313,19 @@ {{ c.display_name }} } - Select the SEED field that holds the contact email for the benchmark record. Ex: 'Email' + {{ t('SF_CONTACT_EMAIL_FIELDNAME_TEXT') }}
{{ t('Contact Benchmark Field') }} - If your Salesforce Benchmark Record stores a Salesforce Contact relation, provide the Salesforce field name here, - ex: Contact_Name__c + {{ t('SF_BENCHMARK_CONTACT_FIELDNAME_TEXT') }}
{{ t('Data Administrator Contact Settings') }}
- {{ - t( - 'The following fields will be used to retrieve or create a property data administrator contact to associate with the Benchmark object in Salesforce. Leave blank if your instance does not use this functionality.' - ) - }} + {{ t('SF_DATA_ADMIN_HEADER_TEXT') }}
@@ -375,21 +335,14 @@ {{ c.display_name }} } - Select the SEED field that holds the account name for the data administrator contact record to be created in - Salesforce. Ex: 'Organization' + {{ t('SF_DATA_ADMIN_ACCOUNT_NAME_FIELDNAME_TEXT') }}
{{ t('Default Data Administrator Account Name') }} - Provide a default account name for Salesforce to use when there is no valid data in the Data Administrator Account - Name Column specified above. Leave this field blank to report an error and abort sync instead when the Data - Administrator Account Name Column is blank + {{ t('SF_DEFAULT_DATA_ADMIN_ACCOUNT_NAME_TEXT') }}
@@ -400,10 +353,7 @@ {{ c.display_name }} } - Select the SEED field that holds the property data administrator's name for the benchmark record. Ex: 'Property - Data Administrator' + {{ t('SF_DATA_ADMIN_NAME_FIELDNAME_TEXT') }}
@@ -414,20 +364,14 @@ {{ c.display_name }} } - Select the SEED field that holds the property data administrator's email for the benchmark record. Ex: 'Property - Data Administrator - Email' + {{ t('SF_DATA_ADMIN_EMAIL_FIELDNAME_TEXT') }}
{{ t('Data Administrator Contact Field') }} - - If your Salesforce Benchmark Record stores a Salesforce Contact relation for a Property Data Administrator, provide - the Salesforce field name here, ex: Property_Data_Administrator__c - + {{ t('SF_DATA_ADMIN_CONTACT_FIELDNAME_TEXT') }}
{{ t('Save Changes') }} + + + + +
+ {{ t('SF_PORTFOLIO_DESC') }} +
+ +
+ {{ + t('Enable Salesforce Integration (Portfolio of Properties)') + }} +
+ + @if (bbSalesforceForm.controls.bb_salesforce_enabled.value) { +
+
+
+ +
{{ t('Salesforce Connection') }}
+
+
+ {{ t('Enter your Salesforce OAuth credentials to connect portfolio data') }} +
+
+ + {{ t('Salesforce URL') }} + + {{ t('The URL of your Salesforce instance') }} + +
+
+ + {{ t('Client ID') }} + + {{ t('The Client ID from your Salesforce Connected App') }} + +
+
+ + {{ t('Client Secret') }} + + + {{ t('The Client Secret from your Salesforce Connected App') }} + +
+ +
+ +
{{ t('Connection Status') }}
+
+
+ @if (isLoggedIntoBbSalesforce) { +
+ + {{ t('Connected to Salesforce') }} +
+ + } @else { +
+ + {{ t('Not connected to Salesforce') }} +
+ + } +
+
+
+ + } + +
+ +
- {{ - t( - "Map your Salesforce Benchmark Object's fields to columns in SEED. Use the Object Manager in Salesforce to retrieve the field names (not the field labels), ex: Cycle__c (not Cycle). These fields will only be sent to Salesforce when a property has the compliant label applied." - ) - }} + {{ t('SF_MAPPINGS_TEXT') }}
diff --git a/src/app/modules/organizations/settings/salesforce/salesforce.component.ts b/src/app/modules/organizations/settings/salesforce/salesforce.component.ts index 465dad4b..edaf17fa 100644 --- a/src/app/modules/organizations/settings/salesforce/salesforce.component.ts +++ b/src/app/modules/organizations/settings/salesforce/salesforce.component.ts @@ -4,8 +4,8 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { MatDialog } from '@angular/material/dialog' import { MatTableDataSource } from '@angular/material/table' import { Subject, takeUntil, tap } from 'rxjs' -import type { Column, Label, Organization, SalesforceConfig, SalesforceMapping } from '@seed/api' -import { ColumnService, LabelService, OrganizationService, SalesforceService } from '@seed/api' +import type { BbSalesforceConfig, Column, Label, Organization, SalesforceConfig, SalesforceMapping } from '@seed/api' +import { BbSalesforceService, ColumnService, LabelService, OrganizationService, SalesforceService } from '@seed/api' import { PageComponent } from '@seed/components' import { SharedImports } from '@seed/directives' import { MaterialImports } from '@seed/materials' @@ -20,18 +20,22 @@ import { DeleteModalComponent, FormModalComponent } from './modal' export class SalesforceComponent implements OnDestroy, OnInit { private _organizationService = inject(OrganizationService) private _salesforceService = inject(SalesforceService) + private _bbSalesforceService = inject(BbSalesforceService) private _labelService = inject(LabelService) private _columnService = inject(ColumnService) private readonly _unsubscribeAll$ = new Subject() private _dialog = inject(MatDialog) passwordHidden = true tokenHidden = true + bbClientSecretHidden = true testConnectionStatus: 'idle' | 'success' | 'error' = 'idle' testConnectionMessage = '' + isLoggedIntoBbSalesforce = false labels: Label[] columns: Column[] organization: Organization salesforceConfig: SalesforceConfig + bbSalesforceConfig: BbSalesforceConfig salesforceMappings: SalesforceMapping[] salesforceMappingsDataSource = new MatTableDataSource([]) salesforceMappingColumns = ['salesforce_fieldname', 'column', 'actions'] @@ -71,12 +75,25 @@ export class SalesforceComponent implements OnDestroy, OnInit { update_at_minute: new FormControl(0, [Validators.min(0), Validators.max(59)]), }), }) + bbSalesforceForm = new FormGroup({ + bb_salesforce_enabled: new FormControl(false), + bbSalesforceConfig: new FormGroup({ + salesforce_url: new FormControl(''), + client_id: new FormControl(''), + client_secret: new FormControl(''), + }), + }) ngOnInit(): void { this._organizationService.currentOrganization$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((organization) => { this.organization = organization this.salesforceForm.get('salesforce_enabled').setValue(this.organization.salesforce_enabled) this._setFormEnabledState(this.organization.salesforce_enabled) + this.bbSalesforceForm.get('bb_salesforce_enabled').setValue(this.organization.bb_salesforce_enabled) + this._setBbFormEnabledState(this.organization.bb_salesforce_enabled) + if (this.organization.bb_salesforce_enabled) { + this._checkBbLoginStatus() + } }) this._salesforceService.config$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((config) => { this.salesforceConfig = config @@ -87,6 +104,15 @@ export class SalesforceComponent implements OnDestroy, OnInit { } } }) + this._bbSalesforceService.config$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((config) => { + this.bbSalesforceConfig = config + for (const field of Object.keys(config)) { + const key = `bbSalesforceConfig.${field}` + if (this.bbSalesforceForm.get(key)) { + this.bbSalesforceForm.get(key).patchValue(config[field]) + } + } + }) this._salesforceService.mappings$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((mappings) => { this.salesforceMappings = mappings this.salesforceMappingsDataSource.data = mappings @@ -233,6 +259,66 @@ export class SalesforceComponent implements OnDestroy, OnInit { } } + toggleBbForm(): void { + const enabled = this.bbSalesforceForm.get('bb_salesforce_enabled').value + this.organization.bb_salesforce_enabled = enabled + this._organizationService.updateSettings(this.organization).subscribe() + this._setBbFormEnabledState(enabled) + if (enabled) { + this._checkBbLoginStatus() + } + } + + toggleBbClientSecret(): void { + this.bbClientSecretHidden = !this.bbClientSecretHidden + } + + bbLogin(): void { + this._bbSalesforceService.getLoginUrl(this.organization.id).subscribe((data) => { + if (data.status === 'error') { + this.testConnectionMessage = data.response || 'Cannot login to Salesforce' + } else if (data.url) { + window.location.href = data.url + } + }) + } + + bbLogout(): void { + this._bbSalesforceService.logout(this.organization.id).subscribe((data) => { + if (data.status === 'success') { + this.isLoggedIntoBbSalesforce = false + } + }) + } + + submitBb(): void { + this.organization.bb_salesforce_enabled = this.bbSalesforceForm.get('bb_salesforce_enabled').value + this._organizationService.updateSettings(this.organization).subscribe() + const configValues = this.bbSalesforceForm.controls.bbSalesforceConfig.value + if (this.bbSalesforceConfig?.id) { + this._bbSalesforceService.update(this.organization.id, { ...this.bbSalesforceConfig, ...configValues }).subscribe((config) => { + this.bbSalesforceConfig = config + }) + } else { + this._bbSalesforceService + .create(this.organization.id, { ...configValues, organization_id: this.organization.id }) + .subscribe((config) => { + this.bbSalesforceConfig = config + }) + } + } + + private _checkBbLoginStatus(): void { + this._bbSalesforceService.verifyToken(this.organization.id).subscribe({ + next: (response) => { + this.isLoggedIntoBbSalesforce = response.valid + }, + error: () => { + this.isLoggedIntoBbSalesforce = false + }, + }) + } + private _setFormEnabledState(enabled: boolean): void { const fg = this.salesforceForm.get('salesforceConfig') as FormGroup for (const field of Object.keys(fg.controls)) { @@ -243,4 +329,15 @@ export class SalesforceComponent implements OnDestroy, OnInit { } } } + + private _setBbFormEnabledState(enabled: boolean): void { + const fg = this.bbSalesforceForm.get('bbSalesforceConfig') as FormGroup + for (const field of Object.keys(fg.controls)) { + if (enabled) { + this.bbSalesforceForm.get(`bbSalesforceConfig.${field}`).enable() + } else { + this.bbSalesforceForm.get(`bbSalesforceConfig.${field}`).disable() + } + } + } }