diff --git a/src/components/admin/managedSettings/activityLoggingSettingPage.js b/src/components/admin/managedSettings/activityLoggingSettingPage.js new file mode 100644 index 0000000..1010e05 --- /dev/null +++ b/src/components/admin/managedSettings/activityLoggingSettingPage.js @@ -0,0 +1,249 @@ +function getActivityLoggingSettingPageRender({ adminUserSettings, crmManifest, userPermissions = {} }) { + const page = { + id: 'activityLoggingSettingPage', + title: 'Activity logging', + type: 'page', + schema: { + type: 'object', + required: [], + properties: { + activityLoggingOptions: { + type: 'object', + title: 'Enable automatic activity logging for:', + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'array', + title: 'Selected options', + items: { + type: 'string', + enum: [ + 'autoLogAnsweredIncoming', + 'autoLogMissedIncoming', + 'autoLogOutgoing', + 'autoLogVoicemails', + 'autoLogSMS', + 'autoLogInboundFax', + 'autoLogOutboundFax' + + ], + enumNames: [ + 'Answered incoming calls', + 'Missed incoming calls', + 'Outgoing calls', + 'Voicemails', + 'SMS', + 'Inbound faxes', + 'Outbound faxes' + ] + }, + uniqueItems: true + } + } + }, + logSyncFrequencySection: { + type: 'object', + title: 'Call Log Sync Frequency', + properties: { + logSyncFrequency: { + type: 'object', + title: 'Frequency', + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'string', + title: 'Value', + enum: ['disabled', '10min', '30min', '1hour', '3hours', '1day'], + enumNames: ['Disabled', '10 min', '30 min', '1 hour', '3 hours', '1 day'] + } + } + } + } + }, + oneTimeLog: { + type: 'object', + title: 'One-time call logging', + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'boolean', + title: 'Enable one-time call logging functionality' + } + } + }, + autoOpenOptions: { + type: 'object', + title: 'Auto-open logging page after:', + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'array', + title: 'Selected options', + items: { + type: 'string', + enum: [ + 'popupLogPageAfterSMS', + 'popupLogPageAfterCall' + ], + enumNames: [ + 'SMS is sent', + 'Call ends' + ] + }, + uniqueItems: true + } + } + } + } + }, + uiSchema: { + activityLoggingOptions: { + "ui:collapsible": true, + value: { + "ui:widget": "checkboxes", + "ui:options": { + "inline": false + } + } + }, + logSyncFrequencySection: { + "ui:collapsible": true, + }, + oneTimeLog: { + "ui:collapsible": true, + }, + autoOpenOptions: { + "ui:collapsible": true, + value: { + "ui:widget": "checkboxes", + "ui:options": { + "inline": false + } + } + }, + submitButtonOptions: { + submitText: 'Save', + }, + }, + formData: { + activityLoggingOptions: { + customizable: adminUserSettings?.activityLoggingOptions?.customizable ?? true, + value: adminUserSettings?.activityLoggingOptions?.value ?? [] + }, + logSyncFrequencySection: { + logSyncFrequency: { + customizable: adminUserSettings?.logSyncFrequency?.customizable ?? true, + value: adminUserSettings?.logSyncFrequency?.value ?? '10min' + } + }, + oneTimeLog: { + customizable: adminUserSettings?.oneTimeLog?.customizable ?? true, + value: adminUserSettings?.oneTimeLog?.value ?? false + }, + autoOpenOptions: { + customizable: adminUserSettings?.autoOpenOptions?.customizable ?? true, + value: adminUserSettings?.autoOpenOptions?.value ?? [] + } + } + }; + + // Add custom settings with section "activityLogging" + if (crmManifest?.settings) { + for (const customSetting of crmManifest.settings) { + if (customSetting.section === 'activityLogging') { + + // Handle different types of custom settings + switch (customSetting.type) { + case 'option': + // Filter options based on permissions + const filteredOptions = customSetting.options ? customSetting.options.filter(opt => + !opt.requiredPermission || userPermissions[opt.requiredPermission] + ) : []; + + page.schema.properties[customSetting.id] = { + type: 'object', + title: customSetting.name, + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'array', + title: customSetting.name, + items: { + type: 'string', + enum: filteredOptions.map(opt => opt.id), + enumNames: filteredOptions.map(opt => opt.name) + }, + uniqueItems: true + } + } + }; + + page.uiSchema[customSetting.id] = { + "ui:collapsible": true, + value: { + "ui:widget": "checkboxes", + "ui:options": { + "inline": false + } + } + }; + + // Get current value from array-based setting + const currentSelectedOptions = adminUserSettings?.[customSetting.id]?.value ?? []; + let isCustomizable = adminUserSettings?.[customSetting.id]?.customizable ?? true; + + page.formData[customSetting.id] = { + customizable: isCustomizable, + value: currentSelectedOptions + }; + break; + + case 'boolean': + page.schema.properties[customSetting.id] = { + type: 'object', + title: customSetting.name, + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'boolean', + title: 'Value' + } + } + }; + + page.uiSchema[customSetting.id] = { + "ui:collapsible": true + }; + + page.formData[customSetting.id] = { + customizable: adminUserSettings?.[customSetting.id]?.customizable ?? true, + value: adminUserSettings?.[customSetting.id]?.value ?? customSetting.defaultValue ?? false + }; + break; + } + } + } + } + + return page; +} + +exports.getActivityLoggingSettingPageRender = getActivityLoggingSettingPageRender; \ No newline at end of file diff --git a/src/components/admin/managedSettings/callAndSMSLoggingSettingPage.js b/src/components/admin/managedSettings/callAndSMSLoggingSettingPage.js deleted file mode 100644 index 4accf19..0000000 --- a/src/components/admin/managedSettings/callAndSMSLoggingSettingPage.js +++ /dev/null @@ -1,198 +0,0 @@ -function getCallAndSMSLoggingSettingPageRender({ adminUserSettings }) { - return { - id: 'callAndSMSLoggingSettingPage', - title: 'Call and SMS logging', - type: 'page', - schema: { - type: 'object', - required: [], - properties: { - autoLogCall: { - type: 'object', - title: 'Log phone calls automatically', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - autoLogSMS: { - type: 'object', - title: 'Log SMS conversations automatically', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - autoLogInboundFax: { - type: 'object', - title: 'Log inbound faxes automatically', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - autoLogOutboundFax: { - type: 'object', - title: 'Log outbound faxes automatically', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - enableRetroCallLogSync: { - type: 'object', - title: 'Disable retroactive call log sync', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - oneTimeLog: { - type: 'object', - title: 'Enable one-time call logging', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - popupLogPageAfterCall: { - type: 'object', - title: '(Manual log) Open call logging page after call', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - }, - popupLogPageAfterSMS: { - type: 'object', - title: '(Manual log) Open SMS logging page after message', - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'boolean', - title: 'Value' - } - } - } - } - }, - uiSchema: { - autoLogCall: { - "ui:collapsible": true, - }, - autoLogSMS: { - "ui:collapsible": true, - }, - autoLogInboundFax: { - "ui:collapsible": true, - }, - autoLogOutboundFax: { - "ui:collapsible": true, - }, - enableRetroCallLogSync: { - "ui:collapsible": true, - }, - oneTimeLog: { - "ui:collapsible": true, - }, - popupLogPageAfterCall: { - "ui:collapsible": true, - }, - popupLogPageAfterSMS: { - "ui:collapsible": true, - }, - submitButtonOptions: { - submitText: 'Save', - } - }, - formData: { - autoLogCall: - { - customizable: adminUserSettings?.autoLogCall?.customizable ?? true, - value: adminUserSettings?.autoLogCall?.value ?? false - }, - autoLogSMS: - { - customizable: adminUserSettings?.autoLogSMS?.customizable ?? true, - value: adminUserSettings?.autoLogSMS?.value ?? false - }, - autoLogInboundFax: - { - customizable: adminUserSettings?.autoLogInboundFax?.customizable ?? true, - value: adminUserSettings?.autoLogInboundFax?.value ?? false - }, - autoLogOutboundFax: - { - customizable: adminUserSettings?.autoLogOutboundFax?.customizable ?? true, - value: adminUserSettings?.autoLogOutboundFax?.value ?? false - }, - enableRetroCallLogSync: - { - customizable: adminUserSettings?.enableRetroCallLogSync?.customizable ?? true, - value: adminUserSettings?.enableRetroCallLogSync?.value ?? true - }, - oneTimeLog: - { - customizable: adminUserSettings?.oneTimeLog?.customizable ?? true, - value: adminUserSettings?.oneTimeLog?.value ?? false - }, - popupLogPageAfterCall: - { - customizable: adminUserSettings?.popupLogPageAfterCall?.customizable ?? true, - value: adminUserSettings?.popupLogPageAfterCall?.value ?? false - }, - popupLogPageAfterSMS: - { - customizable: adminUserSettings?.popupLogPageAfterSMS?.customizable ?? true, - value: adminUserSettings?.popupLogPageAfterSMS?.value ?? false - } - } - } -} - -exports.getCallAndSMSLoggingSettingPageRender = getCallAndSMSLoggingSettingPageRender; \ No newline at end of file diff --git a/src/components/admin/managedSettings/customSettingsPage.js b/src/components/admin/managedSettings/customSettingsPage.js index 054d87b..275507a 100644 --- a/src/components/admin/managedSettings/customSettingsPage.js +++ b/src/components/admin/managedSettings/customSettingsPage.js @@ -22,62 +22,31 @@ function getCustomSettingsPageRender({ crmManifest, adminUserSettings, userSetti } } for (const section of crmManifest.settings) { - page.schema.properties[section.id] = { - type: 'string', - description: section.name - } - page.uiSchema[section.id] = { - "ui:field": "typography", - "ui:variant": "title2" - } - for (const setting of section.items) { - switch (setting.type) { - case 'warning': - page.schema.properties[setting.id] = { - type: 'string', - description: setting.value - }; - page.uiSchema[setting.id] = { - "ui:field": "admonition", - "ui:severity": "warning" - } - break; - case 'inputField': - case 'boolean': - page.schema.properties[setting.id] = { - type: 'object', - title: setting.name, - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: setting.type === 'inputField' ? 'string' : 'boolean', - title: setting.name - } + // Only process sections that don't belong to Activity Logging and have items + if (section.section !== 'activityLogging' && section.items) { + page.schema.properties[section.id] = { + type: 'string', + description: section.name + } + page.uiSchema[section.id] = { + "ui:field": "typography", + "ui:variant": "title2" + } + for (const setting of section.items) { + + switch (setting.type) { + case 'warning': + page.schema.properties[setting.id] = { + type: 'string', + description: setting.value + }; + page.uiSchema[setting.id] = { + "ui:field": "admonition", + "ui:severity": "warning" } - }; - page.formData[setting.id] = { - customizable: adminUserSettings?.[setting.id]?.customizable ?? true, - value: adminUserSettings?.[setting.id]?.value ?? setting.defaultValue - }; - page.uiSchema[setting.id] = { - "ui:collapsible": true, - } - break; - case 'option': - page.formData[setting.id] = { - customizable: adminUserSettings?.[setting.id]?.customizable ?? true, - value: adminUserSettings?.[setting.id]?.value ?? setting?.defaultValue - }; - if (setting.dynamicOptions) { - page.formData[setting.id].options = userSettings?.[setting.id]?.options ?? []; - } - page.uiSchema[setting.id] = { - "ui:collapsible": true, - } - if (setting.checkbox) { + break; + case 'inputField': + case 'boolean': page.schema.properties[setting.id] = { type: 'object', title: setting.name, @@ -87,48 +56,83 @@ function getCustomSettingsPageRender({ crmManifest, adminUserSettings, userSetti title: 'Customizable by user' }, value: { - type: 'array', - title: setting.name, - items: { - type: 'string', - enum: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => option.id) : setting.options.map(option => option.id), - enumNames: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => option.name) : setting.options.map(option => option.name) - }, - uniqueItems: true + type: setting.type === 'inputField' ? 'string' : 'boolean', + title: setting.name } } + }; + page.formData[setting.id] = { + customizable: adminUserSettings?.[setting.id]?.customizable ?? true, + value: adminUserSettings?.[setting.id]?.value ?? setting.defaultValue + }; + page.uiSchema[setting.id] = { + "ui:collapsible": true, } - page.uiSchema[setting.id].value = { - 'ui:widget': 'checkboxes', - 'ui:options': { - inline: true, - }, + break; + case 'option': + page.formData[setting.id] = { + customizable: adminUserSettings?.[setting.id]?.customizable ?? true, + value: adminUserSettings?.[setting.id]?.value ?? setting?.defaultValue }; - } - else { - page.schema.properties[setting.id] = { - type: 'object', - title: setting.name, - properties: { - customizable: { - type: 'boolean', - title: 'Customizable by user' - }, - value: { - type: 'string', - title: setting.name, - oneOf: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => ({ - const: option.id, - title: option.name - })) : setting.options.map(option => ({ - const: option.id, - title: option.name - })) + if (setting.dynamicOptions) { + page.formData[setting.id].options = userSettings?.[setting.id]?.options ?? []; + } + page.uiSchema[setting.id] = { + "ui:collapsible": true, + } + if (setting.checkbox) { + page.schema.properties[setting.id] = { + type: 'object', + title: setting.name, + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'array', + title: setting.name, + items: { + type: 'string', + enum: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => option.id) : setting.options.map(option => option.id), + enumNames: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => option.name) : setting.options.map(option => option.name) + }, + uniqueItems: true + } } } - }; - } - break; + page.uiSchema[setting.id].value = { + 'ui:widget': 'checkboxes', + 'ui:options': { + inline: true, + }, + }; + } + else { + page.schema.properties[setting.id] = { + type: 'object', + title: setting.name, + properties: { + customizable: { + type: 'boolean', + title: 'Customizable by user' + }, + value: { + type: 'string', + title: setting.name, + oneOf: setting.dynamicOptions ? userSettings?.[setting.id]?.options?.map(option => ({ + const: option.id, + title: option.name + })) : setting.options.map(option => ({ + const: option.id, + title: option.name + })) + } + } + }; + } + break; + } } } } diff --git a/src/components/admin/managedSettingsPage.js b/src/components/admin/managedSettingsPage.js index f831782..d4339ef 100644 --- a/src/components/admin/managedSettingsPage.js +++ b/src/components/admin/managedSettingsPage.js @@ -14,8 +14,8 @@ function getManagedSettingsPageRender({ crmManifest }) { title: "General" }, { - const: "callAndSMSLogging", - title: "Call and SMS logging" + const: "activityLogging", + title: "Activity logging" }, { const: "contactSetting", diff --git a/src/core/admin.js b/src/core/admin.js index 53479d1..3ccce3c 100644 --- a/src/core/admin.js +++ b/src/core/admin.js @@ -20,10 +20,30 @@ async function getAdminSettings({ serverUrl }) { async function uploadAdminSettings({ serverUrl, adminSettings }) { const rcAccessToken = getRcAccessToken(); const { rcUnifiedCrmExtJwt } = await chrome.storage.local.get('rcUnifiedCrmExtJwt'); + + let adminSettingsToUpload = { ...adminSettings }; + + // Convert callLogDetails array format to individual boolean settings for backend + if (adminSettingsToUpload.userSettings?.callLogDetails && Array.isArray(adminSettingsToUpload.userSettings.callLogDetails.value)) { + const callLogDetailsArray = adminSettingsToUpload.userSettings.callLogDetails.value; + const customizable = adminSettingsToUpload.userSettings.callLogDetails.customizable; + const callLogDetailOptions = ['addCallLogNote', 'addCallSessionId', 'addCallLogSubject', + 'addCallLogContactNumber', 'addCallLogDuration', 'addCallLogResult', + 'addCallLogRecording', 'addCallLogAiNote', 'addCallLogTranscript', 'addCallLogDateTime']; + + // Convert to individual boolean settings for backend + for (const option of callLogDetailOptions) { + adminSettingsToUpload.userSettings[option] = { + customizable: customizable, + value: callLogDetailsArray.includes(option) + }; + } + } + const uploadAdminSettingsResponse = await axios.post( `${serverUrl}/admin/settings?jwtToken=${rcUnifiedCrmExtJwt}&rcAccessToken=${rcAccessToken}`, { - adminSettings + adminSettings: adminSettingsToUpload }); } @@ -33,18 +53,40 @@ async function refreshAdminSettings() { const platform = manifest.platforms[platformInfo.platformName]; const rcAccessToken = getRcAccessToken(); let adminSettings; - // Admin tab render + + // First try to get from server const storedAdminSettings = await getAdminSettings({ serverUrl: manifest.serverUrl, rcAccessToken }); - await chrome.storage.local.set({ isAdmin: !!storedAdminSettings }); - if (storedAdminSettings) { + + // If server returns null, try to get from local storage + let finalAdminSettings = storedAdminSettings; + if (!storedAdminSettings) { + const { adminSettings: localAdminSettings } = await chrome.storage.local.get({ adminSettings: null }); + finalAdminSettings = localAdminSettings; + } + + // If both server and local storage are empty, initialize with proper structure + if (!finalAdminSettings) { + finalAdminSettings = { + userSettings: {} + }; + } + + // Ensure userSettings exists + if (!finalAdminSettings.userSettings) { + finalAdminSettings.userSettings = {}; + } + + await chrome.storage.local.set({ isAdmin: !!finalAdminSettings }); + + if (finalAdminSettings) { try { const adminPageRender = adminPage.getAdminPageRender({ platform }); document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ type: 'rc-adapter-register-customized-page', page: adminPageRender, }, '*'); - await chrome.storage.local.set({ adminSettings: storedAdminSettings }); - adminSettings = storedAdminSettings; + await chrome.storage.local.set({ adminSettings: finalAdminSettings }); + adminSettings = finalAdminSettings; } catch (e) { console.log('Cannot find admin settings', e); } @@ -52,7 +94,7 @@ async function refreshAdminSettings() { // Set user setting display name const { crmUserInfo } = await chrome.storage.local.get({ crmUserInfo: null }); - authCore.setAccountName(crmUserInfo?.name, !!storedAdminSettings); + authCore.setAccountName(crmUserInfo?.name, !!finalAdminSettings); return { adminSettings } } diff --git a/src/core/user.js b/src/core/user.js index 0051287..619efde 100644 --- a/src/core/user.js +++ b/src/core/user.js @@ -28,13 +28,32 @@ async function getUserSettingsOnline({ serverUrl }) { async function uploadUserSettings({ serverUrl, userSettings }) { const { rcUnifiedCrmExtJwt } = await chrome.storage.local.get('rcUnifiedCrmExtJwt'); const { selectedRegion } = await chrome.storage.local.get({ selectedRegion: 'US' }); - let userSettingsToUpload = userSettings; + let userSettingsToUpload = { ...userSettings }; + if (userSettingsToUpload.selectedRegion) { userSettingsToUpload.selectedRegion.value = selectedRegion; } else { userSettingsToUpload.selectedRegion = { value: selectedRegion }; } + + // Convert callLogDetails array format to individual boolean settings for backend + if (userSettingsToUpload.callLogDetails && Array.isArray(userSettingsToUpload.callLogDetails.value)) { + const callLogDetailsArray = userSettingsToUpload.callLogDetails.value; + const customizable = userSettingsToUpload.callLogDetails.customizable; + const callLogDetailOptions = ['addCallLogNote', 'addCallSessionId', 'addCallLogSubject', + 'addCallLogContactNumber', 'addCallLogDuration', 'addCallLogResult', + 'addCallLogRecording', 'addCallLogAiNote', 'addCallLogTranscript', 'addCallLogDateTime']; + + // Convert to individual boolean settings for backend + for (const option of callLogDetailOptions) { + userSettingsToUpload[option] = { + customizable: customizable, + value: callLogDetailsArray.includes(option) + }; + } + } + const uploadUserSettingsResponse = await axios.post( `${serverUrl}/user/settings?jwtToken=${rcUnifiedCrmExtJwt}`, { @@ -51,7 +70,58 @@ async function refreshUserSettings({ changedSettings, isAvoidForceChange = false } const rcAccessToken = getRcAccessToken(); const manifest = await getManifest(); - let userSettings = await getUserSettingsOnline({ serverUrl: manifest.serverUrl, rcAccessToken }); + + // Try to get user settings from server, with fallback to local storage + let userSettings; + try { + userSettings = await getUserSettingsOnline({ serverUrl: manifest.serverUrl, rcAccessToken }); + } catch (e) { + const { userSettings: localUserSettings } = await chrome.storage.local.get({ userSettings: {} }); + userSettings = localUserSettings; + } + + // If both server and local storage fail, initialize with empty object + if (!userSettings) { + userSettings = {}; + } + + // TEMP: to be deleted after this version 1.6.1 + // Backward compatibility - migrate old autoLogCall setting to new individual settings + if (userSettings.autoLogCall && userSettings.autoLogCall.value === true) { + // One-time translation: Set all three new settings to true + userSettings.autoLogAnsweredIncoming = { customizable: true, value: true }; + userSettings.autoLogMissedIncoming = { customizable: true, value: true }; + userSettings.autoLogOutgoing = { customizable: true, value: true }; + // Delete the old autoLogCall setting + delete userSettings.autoLogCall; + } + + // Migrate existing individual boolean call log details to array format (backward compatibility) + const callLogDetailOptions = ['addCallLogNote', 'addCallSessionId', 'addCallLogSubject', + 'addCallLogContactNumber', 'addCallLogDuration', 'addCallLogResult', + 'addCallLogRecording', 'addCallLogAiNote', 'addCallLogTranscript', 'addCallLogDateTime']; + + if (!userSettings.callLogDetails && callLogDetailOptions.some(option => userSettings[option])) { + // Migrate from individual boolean settings to array format + const enabledOptions = []; + let isCustomizable = true; + + for (const option of callLogDetailOptions) { + if (userSettings[option]?.value === true) { + enabledOptions.push(option); + } + // If any option is not customizable, make the whole array not customizable + if (userSettings[option]?.customizable === false) { + isCustomizable = false; + } + } + + userSettings.callLogDetails = { + customizable: isCustomizable, + value: enabledOptions + }; + } + if (changedSettings) { for (const k of Object.keys(changedSettings)) { if (userSettings[k] === undefined || !userSettings[k].value) { @@ -63,7 +133,17 @@ async function refreshUserSettings({ changedSettings, isAvoidForceChange = false } } await chrome.storage.local.set({ userSettings }); - userSettings = await uploadUserSettings({ serverUrl: manifest.serverUrl, userSettings }); + + // Try to upload user settings to server, but continue if it fails + try { + const uploadedSettings = await uploadUserSettings({ serverUrl: manifest.serverUrl, userSettings }); + if (uploadedSettings) { + userSettings = uploadedSettings; + } + } catch (e) { + console.log('Failed to upload user settings to server, using local settings:'); + // Continue with local settings if upload fails + } document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ type: 'rc-adapter-update-features-flags', chat: getShowChatTabSetting(userSettings).value, @@ -74,8 +154,10 @@ async function refreshUserSettings({ changedSettings, isAvoidForceChange = false recordings: getShowRecordingsTabSetting(userSettings).value, contacts: getShowContactsTabSetting(userSettings).value }, '*'); - const autoLogMessagesGroupTrigger = (userSettings?.autoLogSMS?.value ?? false) || (userSettings?.autoLogInboundFax?.value ?? false) || (userSettings?.autoLogOutboundFax?.value ?? false); - RCAdapter.setAutoLog({ call: userSettings.autoLogCall?.value ?? false, message: autoLogMessagesGroupTrigger }) + const activityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + const autoLogMessagesGroupTrigger = activityLoggingOptions.includes('autoLogSMS') || activityLoggingOptions.includes('autoLogInboundFax') || activityLoggingOptions.includes('autoLogOutboundFax') || activityLoggingOptions.includes('autoLogVoicemails'); + const autoLogCallsGroupTrigger = activityLoggingOptions.includes('autoLogAnsweredIncoming') || activityLoggingOptions.includes('autoLogMissedIncoming') || activityLoggingOptions.includes('autoLogOutgoing'); + RCAdapter.setAutoLog({ call: autoLogCallsGroupTrigger || (userSettings.autoLogCall?.value ?? false), message: autoLogMessagesGroupTrigger }) if (!isAvoidForceChange) { const showAiAssistantWidgetSetting = getShowAiAssistantWidgetSetting(userSettings); const autoStartAiAssistantSetting = getAutoStartAiAssistantSetting(userSettings); @@ -119,9 +201,19 @@ async function updateSSCLToken({ serverUrl, platform, token }) { } } -function getAutoLogCallSetting(userSettings, isAdmin) { + + +// Unified function to get activity logging settings +function getActivityLoggingSetting(userSettings, isAdmin = false) { + const activityLoggingValue = userSettings?.activityLoggingOptions?.value ?? []; + + // Check for server side logging conflicts for call-related settings + const callRelatedSettings = ['autoLogAnsweredIncoming', 'autoLogMissedIncoming', 'autoLogOutgoing']; const serverSideLoggingEnabled = userSettings?.serverSideLogging?.enable ?? false; - if (serverSideLoggingEnabled && (userSettings?.serverSideLogging?.loggingLevel === 'Account' || isAdmin)) { + const hasCallRelatedSettings = callRelatedSettings.some(setting => activityLoggingValue.includes(setting)); + if (hasCallRelatedSettings && + serverSideLoggingEnabled && + (userSettings?.serverSideLogging?.loggingLevel === 'Account' || isAdmin)) { return { value: false, readOnly: true, @@ -129,35 +221,36 @@ function getAutoLogCallSetting(userSettings, isAdmin) { warning: 'Unavailable while server side call logging enabled' } } - return { - value: userSettings?.autoLogCall?.value ?? false, - readOnly: userSettings?.autoLogCall?.customizable === undefined ? false : !userSettings?.autoLogCall?.customizable, - readOnlyReason: !userSettings?.autoLogCall?.customizable ? 'This setting is managed by admin' : '' - } -} -function getAutoLogSMSSetting(userSettings) { + // Standard activity logging logic + const isCustomizable = userSettings?.activityLoggingOptions?.customizable ?? true; + return { - value: userSettings?.autoLogSMS?.value ?? false, - readOnly: userSettings?.autoLogSMS?.customizable === undefined ? false : !userSettings?.autoLogSMS?.customizable, - readOnlyReason: !userSettings?.autoLogSMS?.customizable ? 'This setting is managed by admin' : '' + value: activityLoggingValue, + readOnly: !isCustomizable, + readOnlyReason: !isCustomizable ? 'This setting is managed by admin' : '' } } -function getAutoLogInboundFaxSetting(userSettings) { +function getLogSyncFrequencySetting(userSettings) { return { - value: userSettings?.autoLogInboundFax?.value ?? false, - readOnly: userSettings?.autoLogInboundFax?.customizable === undefined ? false : !userSettings?.autoLogInboundFax?.customizable, - readOnlyReason: !userSettings?.autoLogInboundFax?.customizable ? 'This setting is managed by admin' : '' + value: userSettings?.logSyncFrequency?.value ?? '10min', + readOnly: userSettings?.logSyncFrequency?.customizable === undefined ? false : !userSettings?.logSyncFrequency?.customizable, + readOnlyReason: !userSettings?.logSyncFrequency?.customizable ? 'This setting is managed by admin' : '' } } -function getAutoLogOutboundFaxSetting(userSettings) { - return { - value: userSettings?.autoLogOutboundFax?.value ?? false, - readOnly: userSettings?.autoLogOutboundFax?.customizable === undefined ? false : !userSettings?.autoLogOutboundFax?.customizable, - readOnlyReason: !userSettings?.autoLogOutboundFax?.customizable ? 'This setting is managed by admin' : '' - } +function getLogSyncFrequencyInMilliseconds(userSettings) { + const frequency = getLogSyncFrequencySetting(userSettings).value; + const frequencyMap = { + 'disabled': 0, + '10min': 10 * 60 * 1000, // 10 minutes + '30min': 30 * 60 * 1000, // 30 minutes + '1hour': 60 * 60 * 1000, // 1 hour + '3hours': 3 * 60 * 60 * 1000, // 3 hours + '1day': 24 * 60 * 60 * 1000 // 1 day + }; + return frequencyMap[frequency] || frequencyMap['10min']; } function getEnableRetroCallLogSync(userSettings) { @@ -177,18 +270,24 @@ function getOneTimeLogSetting(userSettings) { } function getCallPopSetting(userSettings) { + const autoOpenOptions = userSettings?.autoOpenOptions?.value ?? []; + const value = autoOpenOptions.includes('popupLogPageAfterCall'); + const isCustomizable = userSettings?.autoOpenOptions?.customizable ?? true; return { - value: userSettings?.popupLogPageAfterCall?.value ?? false, - readOnly: userSettings?.popupLogPageAfterCall?.customizable === undefined ? false : !userSettings?.popupLogPageAfterCall?.customizable, - readOnlyReason: !userSettings?.popupLogPageAfterCall?.customizable ? 'This setting is managed by admin' : '' + value: value, + readOnly: !isCustomizable, + readOnlyReason: !isCustomizable ? 'This setting is managed by admin' : '' } } function getSMSPopSetting(userSettings) { + const autoOpenOptions = userSettings?.autoOpenOptions?.value ?? []; + const value = autoOpenOptions.includes('popupLogPageAfterSMS'); + const isCustomizable = userSettings?.autoOpenOptions?.customizable ?? true; return { - value: userSettings?.popupLogPageAfterSMS?.value ?? false, - readOnly: userSettings?.popupLogPageAfterSMS?.customizable === undefined ? false : !userSettings?.popupLogPageAfterSMS?.customizable, - readOnlyReason: !userSettings?.popupLogPageAfterSMS?.customizable ? 'This setting is managed by admin' : '' + value: value, + readOnly: !isCustomizable, + readOnlyReason: !isCustomizable ? 'This setting is managed by admin' : '' } } @@ -352,16 +451,53 @@ function getCustomSetting(userSettings, id, defaultValue) { } } +function getCustomCallLogDetailsSetting(userSettings, id, defaultValue) { + if (userSettings === undefined) { + return { + value: null, + readOnly: false, + readOnlyReason: '' + }; + } + + // Check if we have the new array format + if (userSettings?.callLogDetails?.value) { + const callLogDetails = userSettings.callLogDetails.value; + const value = callLogDetails.includes(id); + const isCustomizable = userSettings.callLogDetails.customizable ?? true; + return { + value: value ?? defaultValue, + readOnly: !isCustomizable, + readOnlyReason: !isCustomizable ? 'This setting is managed by admin' : '', + } + } + + // Fallback to individual boolean settings (backward compatibility) + if (userSettings[id]) { + return { + value: userSettings[id].value ?? defaultValue, + readOnly: !userSettings[id].customizable, + readOnlyReason: !userSettings[id].customizable ? 'This setting is managed by admin' : '', + } + } + + // Default fallback + return { + value: defaultValue, + readOnly: false, + readOnlyReason: '' + }; +} + + exports.preloadUserSettingsFromAdmin = preloadUserSettingsFromAdmin; exports.getUserSettingsOnline = getUserSettingsOnline; exports.uploadUserSettings = uploadUserSettings; exports.refreshUserSettings = refreshUserSettings; exports.updateSSCLToken = updateSSCLToken; - -exports.getAutoLogCallSetting = getAutoLogCallSetting; -exports.getAutoLogSMSSetting = getAutoLogSMSSetting; -exports.getAutoLogInboundFaxSetting = getAutoLogInboundFaxSetting; -exports.getAutoLogOutboundFaxSetting = getAutoLogOutboundFaxSetting; +exports.getActivityLoggingSetting = getActivityLoggingSetting; +exports.getLogSyncFrequencySetting = getLogSyncFrequencySetting; +exports.getLogSyncFrequencyInMilliseconds = getLogSyncFrequencyInMilliseconds; exports.getEnableRetroCallLogSync = getEnableRetroCallLogSync; exports.getOneTimeLogSetting = getOneTimeLogSetting; exports.getCallPopSetting = getCallPopSetting; @@ -384,4 +520,5 @@ exports.getShowContactsTabSetting = getShowContactsTabSetting; exports.getClickToDialEmbedMode = getClickToDialEmbedMode; exports.getClickToDialUrls = getClickToDialUrls; exports.getNotificationLevelSetting = getNotificationLevelSetting; -exports.getCustomSetting = getCustomSetting; \ No newline at end of file +exports.getCustomSetting = getCustomSetting; +exports.getCustomCallLogDetailsSetting = getCustomCallLogDetailsSetting; \ No newline at end of file diff --git a/src/popup.js b/src/popup.js index 2b4203a..e68a5a5 100644 --- a/src/popup.js +++ b/src/popup.js @@ -19,7 +19,7 @@ import reportPage from './components/reportPage'; import adminPage from './components/admin/adminPage'; import managedSettingsPage from './components/admin/managedSettingsPage'; import generalSettingPage from './components/admin/generalSettingPage'; -import callAndSMSLoggingSettingPage from './components/admin/managedSettings/callAndSMSLoggingSettingPage'; +import activityLoggingSettingPage from './components/admin/managedSettings/activityLoggingSettingPage'; import customAdapterPage from './components/admin/customAdapterPage'; import serverSideLoggingPage from './components/admin/serverSideLoggingPage'; import contactSettingPage from './components/admin/managedSettings/contactSettingPage'; @@ -78,6 +78,83 @@ let platform = null; let hasOngoingCall = false; let lastUserSettingSyncDate = new Date(); +// Helper function to determine if a call should be auto-logged based on direction and result +function shouldAutoLogCall(call, userSettings) { + let shouldAutoLog = false; + + const activityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + + const callType = call.direction === 'Inbound' + ? (call.result === 'Answered' ? 'autoLogAnsweredIncoming' : 'autoLogMissedIncoming') + : 'autoLogOutgoing'; + + shouldAutoLog = activityLoggingOptions.includes(callType); + + // Fallback to legacy setting for backward compatibility + if (!shouldAutoLog) { + shouldAutoLog = userSettings?.autoLogCall?.value ?? false; + } + + return shouldAutoLog; +} + +// Helper function for presence update auto-logging (maps presence results to call results) +function shouldAutoLogCallFromPresence(call, userSettings) { + let shouldAutoLog = false; + + const activityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + + // Determine auto-log setting based on call direction and result + const callType = call.direction === 'Inbound' + ? (call.result === 'CallConnected' ? 'autoLogAnsweredIncoming' : 'autoLogMissedIncoming') + : 'autoLogOutgoing'; + + shouldAutoLog = activityLoggingOptions.includes(callType); + + // Fallback to legacy setting for backward compatibility + if (!shouldAutoLog) { + shouldAutoLog = userSettings?.autoLogCall?.value ?? false; + } + + return shouldAutoLog; +} + +async function restartSyncInterval() { + // Clear existing interval + const { retroAutoCallLogIntervalId } = await chrome.storage.local.get({ retroAutoCallLogIntervalId: null }); + if (retroAutoCallLogIntervalId) { + clearInterval(retroAutoCallLogIntervalId); + await chrome.storage.local.set({ retroAutoCallLogIntervalId: null }); + } + + // Check if auto logging is enabled + const activityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + const autoLogCallsGroupTrigger = activityLoggingOptions.includes('autoLogAnsweredIncoming') || + activityLoggingOptions.includes('autoLogMissedIncoming') || + activityLoggingOptions.includes('autoLogOutgoing'); + const isAutoLogEnabled = autoLogCallsGroupTrigger || (userSettings?.autoLogCall?.value ?? false); + + // Start interval if conditions are met + if (isAutoLogEnabled && crmAuthed) { + const syncIntervalMs = userCore.getLogSyncFrequencyInMilliseconds(userSettings); + + if (syncIntervalMs > 0) { + await chrome.storage.local.set({ retroAutoCallLogMaxAttempt: 10 }); + const newRetroAutoCallLogIntervalId = setInterval( + function () { + logService.retroAutoCallLog({ + manifest, + platformName, + platform + }); + }, syncIntervalMs); + await chrome.storage.local.set({ retroAutoCallLogIntervalId: newRetroAutoCallLogIntervalId }); + } + } +} + + + checkC2DCollision(); getCustomManifest(); @@ -255,6 +332,8 @@ window.addEventListener('message', async (e) => { setInterval(async function () { await triggerPendingRecordingCheck({ serverUrl: manifest.serverUrl }); }, 300000); + + } // Unique: Bullhorn if (platform.name === 'bullhorn' && crmAuthed) { @@ -397,6 +476,10 @@ window.addEventListener('message', async (e) => { type: 'rc-adapter-update-authorization-status', authorized: crmAuthed }, '*'); + + // Initialize sync interval + await restartSyncInterval(); + setInterval(function () { logService.forceCallLogMatcherCheck(); }, 600000) @@ -576,15 +659,14 @@ window.addEventListener('message', async (e) => { trackEditSettings({ changedItem: 'auto-call-log', status: data.autoLog }); if (!!data.autoLog && !!crmAuthed) { await chrome.storage.local.set({ retroAutoCallLogMaxAttempt: 10 }); - const retroAutoCallLogIntervalId = setInterval( - function () { - logService.retroAutoCallLog({ - manifest, - platformName, - platform - }) - }, 60000); - await chrome.storage.local.set({ retroAutoCallLogIntervalId }); + await restartSyncInterval(); + } else { + // Clear interval if auto logging is disabled + const { retroAutoCallLogIntervalId } = await chrome.storage.local.get({ retroAutoCallLogIntervalId: null }); + if (retroAutoCallLogIntervalId) { + clearInterval(retroAutoCallLogIntervalId); + await chrome.storage.local.remove('retroAutoCallLogIntervalId'); + } } break; case 'rc-messageLogger-auto-log-notify': @@ -985,15 +1067,20 @@ window.addEventListener('message', async (e) => { path: `/customized/${clickToDialEmbedPageRender.id}`, // page id }, '*'); break; - case 'callAndSMSLogging': - const callAndSMSLoggingSettingPageRender = callAndSMSLoggingSettingPage.getCallAndSMSLoggingSettingPageRender({ adminUserSettings: adminSettings?.userSettings }); + case 'activityLogging': + const { userPermissions } = await chrome.storage.local.get({ userPermissions: {} }); + const activityLoggingSettingPageRender = activityLoggingSettingPage.getActivityLoggingSettingPageRender({ + adminUserSettings: adminSettings?.userSettings, + crmManifest: platform, + userPermissions + }); document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ type: 'rc-adapter-register-customized-page', - page: callAndSMSLoggingSettingPageRender + page: activityLoggingSettingPageRender }); document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ type: 'rc-adapter-navigate-to', - path: `/customized/${callAndSMSLoggingSettingPageRender.id}`, // page id + path: `/customized/${activityLoggingSettingPageRender.id}`, // page id }, '*'); break; case 'serverSideLoggingSetting': @@ -1312,16 +1399,23 @@ window.addEventListener('message', async (e) => { }); // Translate: If no existing call log, create condition here to navigate to auto log - if (userCore.getAutoLogCallSetting(userSettings).value && data.body.triggerType === 'callLogSync' && !(existingCalls?.length > 0 && existingCalls[0]?.matched)) { - data.body.triggerType = 'createLog'; - isAutoLog = true; + if (data.body.triggerType === 'callLogSync' && !(existingCalls?.length > 0 && existingCalls[0]?.matched)) { + if (shouldAutoLogCall(data.body.call, userSettings)) { + data.body.triggerType = 'createLog'; + isAutoLog = true; + } } // Translate: Right after call, once presence update to Disconnect, auto log the call if (data.body.triggerType === 'presenceUpdate') { if (data.body.call.result === 'Disconnected' || data.body.call.result === 'CallConnected') { - data.body.triggerType = 'createLog'; - isAutoLog = true; + if (shouldAutoLogCallFromPresence(data.body.call, userSettings)) { + data.body.triggerType = 'createLog'; + isAutoLog = true; + } else { + responseMessage(data.requestId, { data: 'ok' }); + break; + } } else { responseMessage(data.requestId, { data: 'ok' }); @@ -1750,9 +1844,10 @@ window.addEventListener('message', async (e) => { responseMessage(data.requestId, { data: 'ok' }); break; } - const isAutoLogSMS = userSettings.autoLogSMS?.value ?? false; - const isAutoLogInboundFax = userSettings.autoLogInboundFax?.value ?? false; - const isAutoLogOutboundFax = userSettings.autoLogOutboundFax?.value ?? false; + const messageActivityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + const isAutoLogSMS = messageActivityLoggingOptions.includes('autoLogSMS'); + const isAutoLogInboundFax = messageActivityLoggingOptions.includes('autoLogInboundFax'); + const isAutoLogOutboundFax = messageActivityLoggingOptions.includes('autoLogOutboundFax'); const messageAutoPopup = userCore.getSMSPopSetting(userSettings).value; const messageLogPrefId = `rc-crm-conversation-pref-${data.body.conversation.conversationLogId}`; @@ -2040,17 +2135,30 @@ window.addEventListener('message', async (e) => { changedSettings[ii.id] = { value: ii.value }; } } else { + console.log('Setting at i level:', i.id, 'value:', i.value); changedSettings[i.id] = { value: i.value }; } } } else if (s.value !== undefined) { + console.log('Setting at s level:', s.id, 'value:', s.value); changedSettings[s.id] = { value: s.value }; } } userSettings = await userCore.refreshUserSettings({ changedSettings }); + + // Restart sync interval to respect any changes to sync frequency or activity logging settings + await restartSyncInterval(); + + // Refresh the service manifest to reflect user settings changes + const serviceManifest = await embeddableServices.getServiceManifest(); + document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ + type: 'rc-adapter-register-third-party-service', + service: serviceManifest + }, '*'); + if (data.body.setting.id === "developerMode") { showNotification({ level: 'success', message: `Developer mode is turned ${data.body.setting.value ? 'ON' : 'OFF'}.`, ttl: 5000 }); await chrome.storage.local.set({ developerMode: data.body.setting.value }); @@ -2065,7 +2173,7 @@ window.addEventListener('message', async (e) => { break; case '/custom-button-click': switch (data.body.button.id) { - case 'callAndSMSLoggingSettingPage': + case 'activityLoggingSettingPage': case 'contactSettingPage': case 'advancedFeaturesSettingPage': case 'customSettingsPage': @@ -2073,13 +2181,96 @@ window.addEventListener('message', async (e) => { case 'notificationLevelSettingPage': case 'clickToDialEmbedPage': window.postMessage({ type: 'rc-log-modal-loading-on' }, '*'); - const settingDataKeys = Object.keys(data.body.button.formData); - for (const k of settingDataKeys) { - adminSettings.userSettings[k] = data.body.button.formData[k]; + + // Handle nested form data structure properly + const formData = data.body.button.formData; + + // For activity logging page, handle both flat and nested structures + if (data.body.button.id === 'activityLoggingSettingPage') { + // Handle checkbox array structure + for (const settingKey of Object.keys(formData)) { + const setting = formData[settingKey]; + if (setting && typeof setting === 'object') { + if (settingKey === 'activityLoggingOptions' || settingKey === 'autoOpenOptions') { + // Save as array setting (consistent with user interface) + adminSettings.userSettings[settingKey] = { + customizable: setting.customizable ?? true, + value: setting.value || [] + }; + } else if (Array.isArray(setting.value) && setting.customizable !== undefined) { + // Handle custom activity logging settings (like call log details) + const selectedOptions = setting.value || []; + // Find the custom setting definition to get the options + let customSettingDef = null; + if (platform?.settings) { + for (const cs of platform.settings) { + if (cs.id === settingKey && cs.section === 'activityLogging') { + customSettingDef = cs; + break; + } + } + } + + if (customSettingDef && customSettingDef.options) { + // Save as array setting (consistent with user interface) + adminSettings.userSettings[settingKey] = { + customizable: setting.customizable ?? true, + value: selectedOptions + }; + } else { + console.warn(`No custom setting definition found for ${settingKey}`); + } + } else if (setting.customizable !== undefined || setting.value !== undefined) { + // This is a direct setting + adminSettings.userSettings[settingKey] = setting; + } else if (settingKey === 'logSyncFrequencySection' && setting.logSyncFrequency) { + // Handle the nested logSyncFrequency setting + adminSettings.userSettings.logSyncFrequency = setting.logSyncFrequency; + } + } + } + } else if (data.body.button.id === 'customSettingsPage') { + // Handle custom settings page - settings are stored directly by ID + for (const settingKey of Object.keys(formData)) { + const setting = formData[settingKey]; + if (setting && typeof setting === 'object' && (setting.customizable !== undefined || setting.value !== undefined)) { + adminSettings.userSettings[settingKey] = setting; + } + } + } else { + // For other pages, handle nested structure + for (const sectionKey of Object.keys(formData)) { + const section = formData[sectionKey]; + if (section && typeof section === 'object') { + // Process nested settings within sections + for (const settingKey of Object.keys(section)) { + const setting = section[settingKey]; + if (setting && typeof setting === 'object' && (setting.customizable !== undefined || setting.value !== undefined)) { + // This is an individual setting, save it directly + adminSettings.userSettings[settingKey] = setting; + } + } + } + } } await chrome.storage.local.set({ adminSettings }); await adminCore.uploadAdminSettings({ serverUrl: manifest.serverUrl, adminSettings }); - await userCore.refreshUserSettings({}); + + // Add delay to avoid race condition - wait for server to process admin settings + await new Promise(resolve => setTimeout(resolve, 1000)); + userSettings = await userCore.refreshUserSettings({ isAfterAdminChanges: true }); + // Refresh the service manifest to reflect admin changes in user settings + const serviceManifest = await embeddableServices.getServiceManifest(); + document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ + type: 'rc-adapter-register-third-party-service', + service: serviceManifest + }, '*'); + + // Restart sync interval if activity logging settings changed + if (data.body.button.id === 'activityLoggingSettingPage') { + await restartSyncInterval(); + } + showNotification({ level: 'success', message: `Settings saved.`, ttl: 3000 }); window.postMessage({ type: 'rc-log-modal-loading-off' }, '*'); document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ diff --git a/src/service/embeddableServices.js b/src/service/embeddableServices.js index 9accaf2..1970fa8 100644 --- a/src/service/embeddableServices.js +++ b/src/service/embeddableServices.js @@ -32,7 +32,7 @@ async function getServiceManifest() { authorized: crmAuthed, authorizedAccount: `${crmUserInfo?.name ?? ''} (Admin)`, info: `Developed by ${manifest?.author?.name ?? 'Unknown'}`, - + // Enable call log sync feature callLoggerPath: '/callLogger', callLogPageInputChangedEventPath: '/callLogger/inputChanged', @@ -43,9 +43,9 @@ async function getServiceManifest() { messagesLogPageInputChangedEventPath: '/messageLogger/inputChanged', messageLogEntityMatcherPath: '/messageLogger/match', messageLoggerAutoSettingLabel: 'Log SMS conversations automatically', - messageLoggerAutoSettingReadOnly: userCore.getAutoLogSMSSetting(userSettings).readOnly, - messageLoggerAutoSettingReadOnlyReason: userCore.getAutoLogSMSSetting(userSettings).readOnlyReason, - messageLoggerAutoSettingReadOnlyValue: userCore.getAutoLogSMSSetting(userSettings).value, + messageLoggerAutoSettingReadOnly: userCore.getActivityLoggingSetting(userSettings, isAdmin).readOnly, + messageLoggerAutoSettingReadOnlyReason: userCore.getActivityLoggingSetting(userSettings, isAdmin).readOnlyReason, + messageLoggerAutoSettingReadOnlyValue: userCore.getActivityLoggingSetting(userSettings, isAdmin).value, callLoggerAutoLogSettingHidden: true, messageLoggerAutoSettingHidden: true, @@ -53,81 +53,119 @@ async function getServiceManifest() { settingsPath: '/settings', settings: [ { - id: 'logging', - type: 'group', - name: 'Call and SMS logging', + id: 'activityLogging', + type: 'section', + name: 'Activity logging', items: [ { - id: 'autoLogCall', - type: 'boolean', - name: 'Log phone calls automatically', - description: 'Automatically log calls when they end in this app', - readOnly: userCore.getAutoLogCallSetting(userSettings, isAdmin).readOnly, - readOnlyReason: userCore.getAutoLogCallSetting(userSettings, isAdmin).warning ?? userCore.getAutoLogCallSetting(userSettings, isAdmin).readOnlyReason, - value: userCore.getAutoLogCallSetting(userSettings, isAdmin).value, - }, - { - id: 'autoLogSMS', - type: 'boolean', - name: 'Log SMS conversations automatically', - description: 'Automatically log SMS when they are sent or received in this app', - readOnly: userCore.getAutoLogSMSSetting(userSettings).readOnly, - readOnlyReason: userCore.getAutoLogSMSSetting(userSettings).readOnlyReason, - value: userCore.getAutoLogSMSSetting(userSettings).value, + id: 'activityLoggingOptions', + type: 'option', + name: ' Enable automatic activity logging for:', + multiple: true, + checkbox: true, + helper: `Select the types of communications you'd like automatically logged to your CRM.`, + options: [ + { + id: 'autoLogAnsweredIncoming', + name: 'Answered incoming calls' + }, + { + id: 'autoLogMissedIncoming', + name: 'Missed incoming calls' + }, + { + id: 'autoLogOutgoing', + name: 'Outgoing calls' + }, + { + id: 'autoLogVoicemails', + name: 'Voicemails' + }, + { + id: 'autoLogSMS', + name: 'SMS' + }, + { + id: 'autoLogInboundFax', + name: 'Inbound faxes' + }, + { + id: 'autoLogOutboundFax', + name: 'Outbound faxes' + } + + ], + value: userSettings?.activityLoggingOptions?.value ?? [], + readOnly: userCore.getActivityLoggingSetting(userSettings, isAdmin).readOnly, + readOnlyReason: userCore.getActivityLoggingSetting(userSettings, isAdmin).readOnlyReason }, { - id: 'autoLogInboundFax', - type: 'boolean', - name: 'Log inbound faxes automatically', - description: 'Automatically log inbound faxes when they are received in this app', - readOnly: userCore.getAutoLogInboundFaxSetting(userSettings).readOnly, - readOnlyReason: userCore.getAutoLogInboundFaxSetting(userSettings).readOnlyReason, - value: userCore.getAutoLogInboundFaxSetting(userSettings).value, + id: "logSyncFrequency", + type: "option", + name: 'Call Log Sync Frequency', + helper: `Specify how often you'd like to check for any unlogged calls.`, + options: [ + { + id: 'disabled', + name: 'Disabled' + }, + { + id: '10min', + name: '10 min' + }, + { + id: '30min', + name: '30 min' + }, + { + id: '1hour', + name: '1 hour' + }, + { + id: '3hours', + name: '3 hours' + }, + { + id: '1day', + name: '1 day' + } + ], + value: userCore.getLogSyncFrequencySetting(userSettings).value, + readOnly: userCore.getLogSyncFrequencySetting(userSettings).readOnly, + readOnlyReason: userCore.getLogSyncFrequencySetting(userSettings).readOnlyReason, }, { - id: 'autoLogOutboundFax', + id: 'oneTimeLog', type: 'boolean', - name: 'Log outbound faxes automatically', - description: 'Automatically log outbound faxes when they are sent in this app', - readOnly: userCore.getAutoLogOutboundFaxSetting(userSettings).readOnly, - readOnlyReason: userCore.getAutoLogOutboundFaxSetting(userSettings).readOnlyReason, - value: userCore.getAutoLogOutboundFaxSetting(userSettings).value, - }, - { - id: "enableRetroCallLogSync", - type: "boolean", - name: 'Retroactive call log sync', - description: 'Periodically scans for and logs any missed activity', - readOnly: userCore.getEnableRetroCallLogSync(userSettings).readOnly, - readOnlyReason: userCore.getEnableRetroCallLogSync(userSettings).readOnlyReason, - value: userCore.getEnableRetroCallLogSync(userSettings).value - }, - { - id: "oneTimeLog", - type: "boolean", name: 'One-time call logging', - description: 'Delays logging until full call details are available', + description: 'Delays logging until full call details are available.', + value: userCore.getOneTimeLogSetting(userSettings).value, readOnly: userCore.getOneTimeLogSetting(userSettings).readOnly, - readOnlyReason: userCore.getOneTimeLogSetting(userSettings).readOnlyReason, - value: userCore.getOneTimeLogSetting(userSettings).value - }, - { - id: "popupLogPageAfterCall", - type: "boolean", - name: '(Manual log) Open call logging page after call', - description: 'Automatically open the logging form after each call', - readOnly: userCore.getCallPopSetting(userSettings).readOnly, - readOnlyReason: userCore.getCallPopSetting(userSettings).readOnlyReason, - value: userCore.getCallPopSetting(userSettings).value + readOnlyReason: userCore.getOneTimeLogSetting(userSettings).readOnlyReason }, { - id: "popupLogPageAfterSMS", - type: "boolean", - name: '(Manual log) Open SMS logging page after message', - description: 'Automatically open the logging form after each message', - readOnly: userCore.getSMSPopSetting(userSettings).readOnly, - readOnlyReason: userCore.getSMSPopSetting(userSettings).readOnlyReason, - value: userCore.getSMSPopSetting(userSettings).value + id: 'autoOpenOptions', + type: 'option', + name: 'Auto-open logging page after:', + helper: 'Opens the logging page for manual entry after selected events.', + multiple: true, + checkbox: true, + options: [ + { + id: 'popupLogPageAfterSMS', + name: 'SMS is sent' + }, + { + id: 'popupLogPageAfterCall', + name: 'Call ends' + } + ], + value: [ + ...(userCore.getSMSPopSetting(userSettings).value ? ['popupLogPageAfterSMS'] : []), + ...(userCore.getCallPopSetting(userSettings).value ? ['popupLogPageAfterCall'] : []) + ], + readOnly: userCore.getSMSPopSetting(userSettings).readOnly || userCore.getCallPopSetting(userSettings).readOnly, + readOnlyReason: userCore.getSMSPopSetting(userSettings).readOnlyReason || userCore.getCallPopSetting(userSettings).readOnlyReason } ] }, @@ -374,7 +412,7 @@ async function getServiceManifest() { ], buttonEventPath: '/custom-button-click' } - + if (platform.useLicense) { const licenseStatusResponse = await authCore.getLicenseStatus({ serverUrl: manifest.serverUrl }); services.licenseStatus = `License: ${licenseStatusResponse.licenseStatus}`; @@ -430,76 +468,188 @@ async function getServiceManifest() { if (customSettings) { for (const cs of customSettings) { const items = []; - for (const item of cs.items) { - if (item.requiredPermission && !userPermissions[item.requiredPermission]) { - continue; - } - switch (item.type) { - case 'inputField': + + // Handle direct setting (cs itself is the setting) + if (cs.type && !cs.items) { + switch (cs.type) { + case 'option': + // Filter options based on permissions + const filteredOptions = cs.options ? cs.options.filter(opt => + !opt.requiredPermission || userPermissions[opt.requiredPermission] + ) : []; + + // For checkbox options, build value array from individual option settings + let finalValue; + let isReadOnly = false; + let readOnlyReason = ''; + + if (cs.multiple && cs.checkbox && cs.options) { + // Build value array from individual option values + finalValue = []; + for (const option of filteredOptions) { + const optionSetting = userCore.getCustomCallLogDetailsSetting(userSettings, option.id, false); + if (optionSetting.value) { + finalValue.push(option.id); + } + // If any option is read-only, mark the whole section as read-only + if (optionSetting.readOnly) { + isReadOnly = true; + readOnlyReason = optionSetting.readOnlyReason || 'This setting is managed by admin'; + } + } + } else { + // Single value setting + const currentValue = userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).value; + finalValue = currentValue !== undefined ? currentValue : + (cs.multiple ? (cs.defaultValue || []) : (cs.defaultValue || "")); + const settingInfo = userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue); + isReadOnly = settingInfo.readOnly; + readOnlyReason = settingInfo.readOnlyReason; + } + items.push({ - id: item.id, - type: 'string', - name: item.name, - description: item.description, - placeHolder: item.placeHolder ?? "", - value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value, - readOnly: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnly, - readOnlyReason: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnlyReason + id: cs.id, + type: "option", + name: cs.name, + helper: cs.helper, + options: filteredOptions, + multiple: cs.multiple ?? false, + checkbox: cs.checkbox ?? false, + required: cs.required ?? false, + value: finalValue, + readOnly: isReadOnly, + readOnlyReason: readOnlyReason }); break; case 'boolean': items.push({ - id: item.id, - type: item.type, - name: item.name, - description: item.description, - value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value, - readOnly: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnly, - readOnlyReason: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnlyReason + id: cs.id, + type: cs.type, + name: cs.name, + helper: cs.helper, + value: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).value, + readOnly: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).readOnly, + readOnlyReason: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).readOnlyReason }); break; - case 'warning': - items.push( - { + case 'inputField': + items.push({ + id: cs.id, + type: 'string', + name: cs.name, + helper: cs.helper, + placeHolder: cs.placeHolder ?? "", + value: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).value, + readOnly: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).readOnly, + readOnlyReason: userCore.getCustomSetting(userSettings, cs.id, cs.defaultValue).readOnlyReason + }); + break; + } + } + // Handle container with items (existing behavior) + else if (cs.items) { + for (const item of cs.items) { + if (item.requiredPermission && !userPermissions[item.requiredPermission]) { + continue; + } + switch (item.type) { + case 'inputField': + items.push({ id: item.id, + type: 'string', name: item.name, - type: 'admonition', - severity: 'warning', - value: item.value - } - ) - break; - case 'option': - items.push( - { + description: item.description, + placeHolder: item.placeHolder ?? "", + value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value, + readOnly: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnly, + readOnlyReason: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnlyReason + }); + break; + case 'boolean': + items.push({ id: item.id, - type: "option", + type: item.type, name: item.name, description: item.description, - options: item.dynamicOptions ? userCore.getCustomSetting(userSettings, item.id, item.defaultValue).options : item.options, - multiple: item.multiple ?? false, - checkbox: item.checkbox ?? false, - required: item.required ?? false, - value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value ?? (item.multiple ? [] : ""), + value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value, readOnly: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnly, readOnlyReason: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnlyReason - } - ) - break; + }); + break; + case 'warning': + items.push( + { + id: item.id, + name: item.name, + type: 'admonition', + severity: 'warning', + value: item.value + } + ) + break; + case 'option': + items.push( + { + id: item.id, + type: "option", + name: item.name, + description: item.description, + options: item.dynamicOptions ? userCore.getCustomSetting(userSettings, item.id, item.defaultValue).options : item.options, + multiple: item.multiple ?? false, + checkbox: item.checkbox ?? false, + required: item.required ?? false, + value: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).value ?? (item.multiple ? [] : ""), + readOnly: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnly, + readOnlyReason: userCore.getCustomSetting(userSettings, item.id, item.defaultValue).readOnlyReason + } + ) + break; + } } } - const group = { - id: cs.id, - type: cs.type, - name: cs.name, - items - }; - if (cs.group) { - group.groupId = cs.group; + // Handle custom settings with section property - add to existing sections + if (cs.section) { + const targetSection = services.settings.find(s => s.id === cs.section); + if (targetSection && targetSection.items) { + // For direct settings (cs.type && !cs.items), add the processed items directly + if (cs.type && !cs.items) { + // Add each processed item directly to the target section + targetSection.items.push(...items); + } else { + // For container settings, add as a subsection + const subsection = { + id: cs.id, + type: cs.type, + name: cs.name, + items + }; + targetSection.items.push(subsection); + } + } else { + // Fallback: add as top-level group if section not found + const group = { + id: cs.id, + type: cs.type, + name: cs.name, + items + }; + services.settings.splice(1, 0, group); + } + } else { + // Handle as regular group (existing behavior) + const group = { + id: cs.id, + type: cs.type, + name: cs.name, + items + }; + if (cs.group) { + group.groupId = cs.group; + } + services.settings.splice(1, 0, group); } - services.settings.splice(1, 0, group); } - }; + } if (platformName === 'clio' || platformName === 'insightly' || platformName === 'netsuite') { const numberFormatterComponent = [ { diff --git a/src/service/logService.js b/src/service/logService.js index 9b3ed39..17e63b1 100644 --- a/src/service/logService.js +++ b/src/service/logService.js @@ -5,6 +5,8 @@ import contactCore from '../core/contact'; import { showNotification, dismissNotification, isObjectEmpty, getRcAccessToken } from '../lib/util'; import { getLogConflictInfo } from '../lib/logUtil'; + + async function retroAutoCallLog({ manifest, platformName, @@ -13,10 +15,15 @@ async function retroAutoCallLog({ const { isAdmin } = await chrome.storage.local.get({ isAdmin: false }); const { userSettings } = await chrome.storage.local.get({ userSettings: {} }); const { rcAdditionalSubmission } = await chrome.storage.local.get({ rcAdditionalSubmission: {} }); - if (!userCore.getEnableRetroCallLogSync(userSettings).value) { + + const logSyncFrequency = userCore.getLogSyncFrequencySetting(userSettings).value; + + if (logSyncFrequency === 'disabled') { return; } + const { retroAutoCallLogMaxAttempt } = await chrome.storage.local.get({ retroAutoCallLogMaxAttempt: 10 }); + let retroLoggedCount = 0; if (retroAutoCallLogMaxAttempt > 0) { await chrome.storage.local.set({ retroAutoCallLogMaxAttempt: retroAutoCallLogMaxAttempt - 1 }); @@ -24,45 +31,76 @@ async function retroAutoCallLog({ let effectiveCount = 0; const itemsPerPage = 50; const pageNumber = 1; - const { calls, hasMore } = await RCAdapter.getUnloggedCalls(itemsPerPage, pageNumber) - const isAutoLog = userCore.getAutoLogCallSetting(userSettings, isAdmin).value; - const { retroAutoCallLogNotificationId } = await chrome.storage.local.get({ retroAutoCallLogNotificationId: null }) - if (isAutoLog) { + + let retroAutoCallLogNotificationId; + + try { + const { calls, hasMore } = await RCAdapter.getUnloggedCalls(itemsPerPage, pageNumber); + + // Check if any individual auto-logging setting is enabled + const activityLoggingOptions = userSettings?.activityLoggingOptions?.value ?? []; + const hasAnyAutoLogEnabled = activityLoggingOptions.includes('autoLogAnsweredIncoming') || + activityLoggingOptions.includes('autoLogMissedIncoming') || + activityLoggingOptions.includes('autoLogOutgoing') || + (userSettings?.autoLogCall?.value ?? false); + + if (!hasAnyAutoLogEnabled) { + return; + } + + const notificationData = await chrome.storage.local.get({ retroAutoCallLogNotificationId: null }); + retroAutoCallLogNotificationId = notificationData.retroAutoCallLogNotificationId; + if (!retroAutoCallLogNotificationId) { - const newRetroAutoCallLogNotificationId = await showNotification({ level: 'success', message: 'Attempting to sync historical call logs in the background...', ttl: 5000 }); + const newRetroAutoCallLogNotificationId = await showNotification({ + level: 'success', + message: 'Attempting to sync historical call logs in the background...', + ttl: 5000 + }); await chrome.storage.local.set({ retroAutoCallLogNotificationId: newRetroAutoCallLogNotificationId }); } + for (const c of calls) { if (effectiveCount >= effectiveTotal) { break; } + const contactPhoneNumber = c.direction === 'Inbound' ? c.from.phoneNumber : c.to.phoneNumber; - const { matched: callContactMatched, returnMessage: callLogContactMatchMessage, contactInfo: callMatchedContact } = await contactCore.getContact({ serverUrl: manifest.serverUrl, phoneNumber: contactPhoneNumber, platformName }); - if (!callContactMatched) { - continue; - } - const { hasConflict, autoSelectAdditionalSubmission } = await getLogConflictInfo({ - platform, - isAutoLog, - contactInfo: callMatchedContact, - logType: 'callLog', - direction: c.direction, - isVoicemail: false - }); - if (!hasConflict) { - const callLogSubject = c.direction === 'Inbound' ? - `Inbound Call from ${callMatchedContact[0]?.name ?? ''}` : - `Outbound Call to ${callMatchedContact[0]?.name ?? ''}`; - const note = await logCore.getCachedNote({ sessionId: c.sessionId }); - const exsitingLog = await logCore.getLog({ + + try { + const { matched: callContactMatched, returnMessage: callLogContactMatchMessage, contactInfo: callMatchedContact } = await contactCore.getContact({ serverUrl: manifest.serverUrl, - logType: 'Call', - sessionIds: c.sessionId, - requireDetails: false + phoneNumber: contactPhoneNumber, + platformName + }); + + if (!callContactMatched) { + continue; + } + + const { hasConflict, autoSelectAdditionalSubmission } = await getLogConflictInfo({ + platform, + isAutoLog: true, + contactInfo: callMatchedContact, + logType: 'callLog', + direction: c.direction, + isVoicemail: false }); - if (!!exsitingLog?.callLogs[0] && !exsitingLog.callLogs[0].matched) { - await logCore.addLog( - { + + if (!hasConflict) { + const callLogSubject = c.direction === 'Inbound' ? + `Inbound Call from ${callMatchedContact[0]?.name ?? ''}` : + `Outbound Call to ${callMatchedContact[0]?.name ?? ''}`; + const note = await logCore.getCachedNote({ sessionId: c.sessionId }); + const exsitingLog = await logCore.getLog({ + serverUrl: manifest.serverUrl, + logType: 'Call', + sessionIds: c.sessionId, + requireDetails: false + }); + + if (!!exsitingLog?.callLogs[0] && !exsitingLog.callLogs[0].matched) { + await logCore.addLog({ serverUrl: manifest.serverUrl, logType: 'Call', logInfo: c, @@ -77,33 +115,43 @@ async function retroAutoCallLog({ contactName: callMatchedContact[0]?.name, isShowNotification: false }); - if (!isObjectEmpty(autoSelectAdditionalSubmission) && !userCore.getOneTimeLogSetting(userSettings).value) { - await dispositionCore.upsertDisposition({ - serverUrl: manifest.serverUrl, - logType: 'Call', - sessionId: c.sessionId, - dispositions: { ...autoSelectAdditionalSubmission, note }, - rcAdditionalSubmission - }); + + if (!isObjectEmpty(autoSelectAdditionalSubmission) && !userCore.getOneTimeLogSetting(userSettings).value) { + await dispositionCore.upsertDisposition({ + serverUrl: manifest.serverUrl, + logType: 'Call', + sessionId: c.sessionId, + dispositions: { ...autoSelectAdditionalSubmission, note }, + rcAdditionalSubmission + }); + } + retroLoggedCount++; + effectiveCount++; + } + else { + // force call log matcher check + document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ + type: 'rc-adapter-trigger-call-logger-match', + sessionIds: [exsitingLog.callLogs[0].sessionId] + }, '*'); } - retroLoggedCount++; - effectiveCount++; - } - else { - // force call log matcher check - document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({ - type: 'rc-adapter-trigger-call-logger-match', - sessionIds: [exsitingLog.callLogs[0].sessionId] - }, '*'); } + } catch (error) { + console.error('Error processing call:', c.sessionId, error); } } + if (!hasMore) { const { retroAutoCallLogIntervalId } = await chrome.storage.local.get({ retroAutoCallLogIntervalId: null }); clearInterval(retroAutoCallLogIntervalId); dismissNotification({ notificationId: retroAutoCallLogNotificationId }); showNotification({ level: 'success', message: `Historical call syncing finished. ${retroLoggedCount} call(s) synced.`, ttl: 5000 }); } + } catch (error) { + const { retroAutoCallLogIntervalId } = await chrome.storage.local.get({ retroAutoCallLogIntervalId: null }); + clearInterval(retroAutoCallLogIntervalId); + dismissNotification({ notificationId: retroAutoCallLogNotificationId }); + showNotification({ level: 'error', message: 'Failed to fetch historical calls for sync.', ttl: 5000 }); } } else {