diff --git a/crm/api/activities.py b/crm/api/activities.py index ead84b1f..31d74580 100644 --- a/crm/api/activities.py +++ b/crm/api/activities.py @@ -125,6 +125,7 @@ def get_deal_activities(name): "bcc": communication.bcc, "attachments": get_attachments('Communication', communication.name), "read_by_recipient": communication.read_by_recipient, + "delivery_status": communication.delivery_status, }, "is_lead": False, } @@ -238,6 +239,7 @@ def get_lead_activities(name): "bcc": communication.bcc, "attachments": get_attachments('Communication', communication.name), "read_by_recipient": communication.read_by_recipient, + "delivery_status": communication.delivery_status, }, "is_lead": True, } diff --git a/crm/api/doc.py b/crm/api/doc.py index a5a7c51e..84a84458 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -685,6 +685,7 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False): "depends_on": field.depends_on, "mandatory_depends_on": field.mandatory_depends_on, "read_only_depends_on": field.read_only_depends_on, + "link_filters": field.get("link_filters"), }) return _fields diff --git a/frontend/src/components/Activities/EmailArea.vue b/frontend/src/components/Activities/EmailArea.vue index d42db18c..33e11f16 100644 --- a/frontend/src/components/Activities/EmailArea.vue +++ b/frontend/src/components/Activities/EmailArea.vue @@ -16,6 +16,12 @@ />
+
{{ __(timeAgo(activity.creation)) }} @@ -87,6 +93,7 @@ import AttachmentItem from '@/components/AttachmentItem.vue' import EmailContent from '@/components/Activities/EmailContent.vue' import { Badge, Tooltip } from 'frappe-ui' import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils' +import { computed } from 'vue' const props = defineProps({ activity: Object, @@ -140,4 +147,19 @@ function reply(email, reply_all = false) { .focus('start') .run() } + +const status = computed(() => { + let _status = props.activity?.data?.delivery_status + let indicator_color = 'red' + if (['Sent', 'Clicked'].includes(_status)) { + indicator_color = 'green' + } else if (['Sending', 'Scheduled'].includes(_status)) { + indicator_color = 'orange' + } else if (['Opened', 'Read'].includes(_status)) { + indicator_color = 'blue' + } else if (_status == 'Error') { + indicator_color = 'red' + } + return { label: _status, color: indicator_color } +}) diff --git a/frontend/src/components/Controls/Link.vue b/frontend/src/components/Controls/Link.vue index 30b0b282..3bcb3d7f 100644 --- a/frontend/src/components/Controls/Link.vue +++ b/frontend/src/components/Controls/Link.vue @@ -69,6 +69,10 @@ const props = defineProps({ type: String, required: true, }, + filters: { + type: Array, + default: () => [], + }, modelValue: { type: String, default: '', @@ -122,6 +126,7 @@ const options = createResource({ params: { txt: text.value, doctype: props.doctype, + filters: props.filters, }, transform: (data) => { let allData = data.map((option) => { @@ -152,6 +157,7 @@ function reload(val) { params: { txt: val, doctype: props.doctype, + filters: props.filters, }, }) options.reload() diff --git a/frontend/src/components/Fields.vue b/frontend/src/components/Fields.vue index c2bcd346..ac27882d 100644 --- a/frontend/src/components/Fields.vue +++ b/frontend/src/components/Fields.vue @@ -89,6 +89,7 @@ class="form-control flex-1" :value="data[field.name]" :doctype="field.options" + :filters="field.filters" @change="(v) => (data[field.name] = v)" :placeholder="__(field.placeholder || field.label)" :onCreate="field.create" @@ -110,6 +111,7 @@ class="form-control" :value="getUser(data[field.name]).full_name" :doctype="field.options" + :filters="field.filters" @change="(v) => (data[field.name] = v)" :placeholder="__(field.placeholder || field.label)" :hideMe="true" diff --git a/frontend/src/components/SectionFields.vue b/frontend/src/components/SectionFields.vue index 24b14f96..f2029aa2 100644 --- a/frontend/src/components/SectionFields.vue +++ b/frontend/src/components/SectionFields.vue @@ -65,6 +65,7 @@ class="form-control" :value="data[field.name] && getUser(data[field.name]).full_name" doctype="User" + :filters="field.filters" @change="(data) => emit('update', field.name, data)" :placeholder="'Select' + ' ' + field.label + '...'" :hideMe="true" @@ -88,6 +89,7 @@ class="form-control select-text" :value="data[field.name]" :doctype="field.doctype" + :filters="field.filters" :placeholder="field.placeholder" @change="(data) => emit('update', field.name, data)" :onCreate="field.create" @@ -144,6 +146,7 @@ const _fields = computed(() => { if (df?.depends_on) evaluate_depends_on(df.depends_on, field) all_fields.push({ ...field, + filters: df.link_filters && JSON.parse(df.link_filters), placeholder: field.placeholder || field.label, }) }) diff --git a/frontend/src/components/Settings/SettingsPage.vue b/frontend/src/components/Settings/SettingsPage.vue index e9a53bf0..90c49e70 100644 --- a/frontend/src/components/Settings/SettingsPage.vue +++ b/frontend/src/components/Settings/SettingsPage.vue @@ -122,6 +122,7 @@ const sections = computed(() => { } else { _sections[_sections.length - 1].fields.push({ ...field, + filters: field.link_filters && JSON.parse(field.link_filters), display_via_depends_on: evaluate_depends_on_value( field.depends_on, data.doc, diff --git a/frontend/src/composables/useActiveTabManager.js b/frontend/src/composables/useActiveTabManager.js new file mode 100644 index 00000000..5b9cd484 --- /dev/null +++ b/frontend/src/composables/useActiveTabManager.js @@ -0,0 +1,80 @@ +import { ref, watch } from 'vue' +import { useRoute } from 'vue-router' +import { useDebounceFn, useStorage } from '@vueuse/core' + +export function useActiveTabManager(tabs, storageKey) { + const activieTab = useStorage(storageKey, 'activity') + const route = useRoute() + + const preserveLastVisitedTab = useDebounceFn((tabName) => { + activieTab.value = tabName.toLowerCase() + }, 300) + + function setActiveTabInUrl(tabName) { + window.location.hash = '#' + tabName.toLowerCase() + } + + function getActiveTabFromUrl() { + return route.hash.replace('#', '') + } + + function findTabIndex(tabName) { + return tabs.value.findIndex( + (tabOptions) => tabOptions.name.toLowerCase() === tabName, + ) + } + + function getTabIndex(tabName) { + let index = findTabIndex(tabName) + return index !== -1 ? index : 0 // Default to the first tab if not found + } + + function getActiveTabFromLocalStorage() { + return activieTab.value + } + + function getActiveTab() { + let activeTab = getActiveTabFromUrl() + if (activeTab) { + let index = findTabIndex(activeTab) + if (index !== -1) { + preserveLastVisitedTab(activeTab) + return index + } + return 0 + } + + let lastVisitedTab = getActiveTabFromLocalStorage() + if (lastVisitedTab) { + setActiveTabInUrl(lastVisitedTab) + return getTabIndex(lastVisitedTab) + } + + return 0 // Default to the first tab if nothing is found + } + + const tabIndex = ref(getActiveTab()) + + watch(tabIndex, (tabIndexValue) => { + let currentTab = tabs.value[tabIndexValue].name + setActiveTabInUrl(currentTab) + preserveLastVisitedTab(currentTab) + }) + + watch( + () => route.hash, + (tabValue) => { + if (!tabValue) return + + let tabName = tabValue.replace('#', '') + let index = findTabIndex(tabName) + if (index === -1) index = 0 + + let currentTab = tabs.value[index].name + preserveLastVisitedTab(currentTab) + tabIndex.value = index + }, + ) + + return { tabIndex } +} diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index c9ef4fbb..9429cd6c 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -354,6 +354,8 @@ import { } from 'frappe-ui' import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue' import { useRoute, useRouter } from 'vue-router' +import { useActiveTabManager } from '@/composables/useActiveTabManager' + const { $dialog, $socket, makeCall } = globalStore() const { statusOptions, getDealStatus } = statusesStore() @@ -376,10 +378,13 @@ const deal = createResource({ params: { name: props.dealId }, cache: ['deal', props.dealId], onSuccess: async (data) => { - organization.update({ - params: { doctype: 'CRM Organization', name: data.organization }, - }) - organization.fetch() + if (data.organization) { + organization.update({ + params: { doctype: 'CRM Organization', name: data.organization }, + }) + organization.fetch() + } + let obj = { doc: data, $dialog, @@ -513,7 +518,6 @@ usePageMeta(() => { } }) -const tabIndex = ref(0) const tabs = computed(() => { let tabOptions = [ { @@ -556,6 +560,7 @@ const tabs = computed(() => { ] return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true)) }) +const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const fieldsLayout = createResource({ url: 'crm.api.doc.get_sidebar_fields', diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 25fedfd5..03e10670 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -330,6 +330,7 @@ import { } from 'frappe-ui' import { ref, computed, onMounted, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' +import { useActiveTabManager } from '@/composables/useActiveTabManager' const { $dialog, $socket, makeCall } = globalStore() const { getContactByName, contacts } = contactsStore() @@ -463,8 +464,6 @@ usePageMeta(() => { } }) -const tabIndex = ref(0) - const tabs = computed(() => { let tabOptions = [ { @@ -508,6 +507,8 @@ const tabs = computed(() => { return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true)) }) +const { tabIndex } = useActiveTabManager(tabs, 'lastLeadTab') + watch(tabs, (value) => { if (value && route.params.tabName) { let index = value.findIndex( diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 63b56743..08b8a54d 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -309,10 +309,13 @@ const deal = createResource({ params: { name: props.dealId }, cache: ['deal', props.dealId], onSuccess: async (data) => { - organization.update({ - params: { doctype: 'CRM Organization', name: data.organization }, - }) - organization.fetch() + if (data.organization) { + organization.update({ + params: { doctype: 'CRM Organization', name: data.organization }, + }) + organization.fetch() + } + let obj = { doc: data, $dialog,