diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue
index 2ccadec62..abb7f22f0 100644
--- a/frontend/src/components/Modals/DealModal.vue
+++ b/frontend/src/components/Modals/DealModal.vue
@@ -38,6 +38,7 @@
v-if="filteredSections.length"
:tabs="filteredSections"
:data="deal"
+ doctype="CRM Deal"
/>
diff --git a/frontend/src/components/Modals/OrganizationModal.vue b/frontend/src/components/Modals/OrganizationModal.vue
index 585b9b78a..0ea5e1596 100644
--- a/frontend/src/components/Modals/OrganizationModal.vue
+++ b/frontend/src/components/Modals/OrganizationModal.vue
@@ -23,7 +23,11 @@
diff --git a/frontend/src/components/Modals/SidePanelModal.vue b/frontend/src/components/Modals/SidePanelModal.vue
index 6bd24b181..53a2a938b 100644
--- a/frontend/src/components/Modals/SidePanelModal.vue
+++ b/frontend/src/components/Modals/SidePanelModal.vue
@@ -151,7 +151,7 @@ function saveChanges() {
{
doctype: _doctype.value,
type: 'Side Panel',
- layout: JSON.stringify(_tabs),
+ layout: JSON.stringify(_tabs[0].sections),
},
).then(() => {
loading.value = false
diff --git a/frontend/src/components/MultipleAvatar.vue b/frontend/src/components/MultipleAvatar.vue
index aa871fe8e..fe88e10b1 100644
--- a/frontend/src/components/MultipleAvatar.vue
+++ b/frontend/src/components/MultipleAvatar.vue
@@ -48,5 +48,5 @@ const props = defineProps({
default: 'md',
},
})
-const reverseAvatars = computed(() => props.avatars.reverse())
+const reverseAvatars = computed(() => [...props.avatars].reverse())
diff --git a/frontend/src/components/Settings/SettingsPage.vue b/frontend/src/components/Settings/SettingsPage.vue
index e8d6c8762..1d6bfdc35 100644
--- a/frontend/src/components/Settings/SettingsPage.vue
+++ b/frontend/src/components/Settings/SettingsPage.vue
@@ -12,7 +12,12 @@
/>
-
+
diff --git a/frontend/src/components/SidePanelLayout.vue b/frontend/src/components/SidePanelLayout.vue
index e18d65fc2..788f7dd1b 100644
--- a/frontend/src/components/SidePanelLayout.vue
+++ b/frontend/src/components/SidePanelLayout.vue
@@ -168,6 +168,33 @@
@change="(data) => emit('update', field.name, data)"
/>
+
+
+
@@ -211,13 +212,9 @@ import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue'
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
import AddressModal from '@/components/Modals/AddressModal.vue'
-import {
- formatDate,
- timeAgo,
- formatNumberIntoCurrency,
- createToast,
-} from '@/utils'
+import { formatDate, timeAgo, createToast } from '@/utils'
import { getView } from '@/utils/view'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global.js'
import { usersStore } from '@/stores/users.js'
import { organizationsStore } from '@/stores/organizations.js'
@@ -601,6 +598,8 @@ async function updateField(fieldname, value) {
contact.reload()
}
+const { getFormattedCurrency } = getMeta('CRM Deal')
+
const columns = computed(() => dealColumns)
function getDealRowObject(deal) {
@@ -610,10 +609,7 @@ function getDealRowObject(deal) {
label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo,
},
- annual_revenue: formatNumberIntoCurrency(
- deal.annual_revenue,
- deal.currency,
- ),
+ annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: {
label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass,
@@ -640,6 +636,7 @@ const dealColumns = [
{
label: __('Amount'),
key: 'annual_revenue',
+ align: 'right',
width: '9rem',
},
{
diff --git a/frontend/src/pages/Contacts.vue b/frontend/src/pages/Contacts.vue
index dca88f309..fdd69dfc0 100644
--- a/frontend/src/pages/Contacts.vue
+++ b/frontend/src/pages/Contacts.vue
@@ -70,10 +70,12 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import ContactModal from '@/components/Modals/ContactModal.vue'
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
import ViewControls from '@/components/ViewControls.vue'
+import { getMeta } from '@/stores/meta'
import { organizationsStore } from '@/stores/organizations.js'
import { formatDate, timeAgo } from '@/utils'
import { ref, computed } from 'vue'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } = getMeta('Contact')
const { getOrganization } = organizationsStore()
const showContactModal = ref(false)
@@ -110,6 +112,18 @@ const rows = computed(() => {
_rows[row] = formatDate(contact[row], '', true, fieldType == 'Datetime')
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, contact)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, contact)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, contact)
+ }
+
if (row == 'full_name') {
_rows[row] = {
label: contact.full_name,
diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue
index ba464d018..9c2b3db9c 100644
--- a/frontend/src/pages/Deal.vue
+++ b/frontend/src/pages/Deal.vue
@@ -171,6 +171,7 @@
v-if="section.fields"
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
+ doctype="CRM Deal"
v-model="deal.data"
@update="updateField"
/>
diff --git a/frontend/src/pages/Deals.vue b/frontend/src/pages/Deals.vue
index 7a3162b8c..8afcdfe94 100644
--- a/frontend/src/pages/Deals.vue
+++ b/frontend/src/pages/Deals.vue
@@ -281,22 +281,19 @@ import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue'
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import ViewControls from '@/components/ViewControls.vue'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations'
import { statusesStore } from '@/stores/statuses'
import { callEnabled } from '@/composables/settings'
-import {
- formatDate,
- timeAgo,
- website,
- formatNumberIntoCurrency,
- formatTime,
-} from '@/utils'
+import { formatDate, timeAgo, website, formatTime } from '@/utils'
import { Tooltip, Avatar, Dropdown } from 'frappe-ui'
import { useRoute } from 'vue-router'
import { ref, reactive, computed, h } from 'vue'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('CRM Deal')
const { makeCall } = globalStore()
const { getUser } = usersStore()
const { getOrganization } = organizationsStore()
@@ -402,6 +399,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(deal[row], '', true, fieldType == 'Datetime')
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, deal)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, deal)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, deal)
+ }
+
if (row == 'organization') {
_rows[row] = {
label: deal.organization,
@@ -409,11 +418,6 @@ function parseRows(rows, columns = []) {
}
} else if (row === 'website') {
_rows[row] = website(deal.website)
- } else if (row == 'annual_revenue') {
- _rows[row] = formatNumberIntoCurrency(
- deal.annual_revenue,
- deal.currency,
- )
} else if (row == 'status') {
_rows[row] = {
label: deal.status,
diff --git a/frontend/src/pages/EmailTemplates.vue b/frontend/src/pages/EmailTemplates.vue
index 73e3ec5f5..4ab8e0dc5 100644
--- a/frontend/src/pages/EmailTemplates.vue
+++ b/frontend/src/pages/EmailTemplates.vue
@@ -75,9 +75,13 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import ViewControls from '@/components/ViewControls.vue'
import EmailTemplatesListView from '@/components/ListViews/EmailTemplatesListView.vue'
import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
+import { getMeta } from '@/stores/meta'
import { formatDate, timeAgo } from '@/utils'
import { computed, ref } from 'vue'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('Email Template')
+
const emailTemplatesListView = ref(null)
// emailTemplates data is loaded in the ViewControls component
@@ -115,6 +119,18 @@ const rows = computed(() => {
)
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, emailTemplate)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, emailTemplate)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, emailTemplate)
+ }
+
if (['modified', 'creation'].includes(row)) {
_rows[row] = {
label: formatDate(emailTemplate[row]),
diff --git a/frontend/src/pages/Leads.vue b/frontend/src/pages/Leads.vue
index 8be6fa7b5..c6884febc 100644
--- a/frontend/src/pages/Leads.vue
+++ b/frontend/src/pages/Leads.vue
@@ -303,6 +303,7 @@ import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue'
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import ViewControls from '@/components/ViewControls.vue'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses'
@@ -312,6 +313,8 @@ import { Avatar, Tooltip, Dropdown } from 'frappe-ui'
import { useRoute } from 'vue-router'
import { ref, computed, reactive, h } from 'vue'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('CRM Lead')
const { makeCall } = globalStore()
const { getUser } = usersStore()
const { getLeadStatus } = statusesStore()
@@ -416,6 +419,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(lead[row], '', true, fieldType == 'Datetime')
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, lead)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, lead)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, lead)
+ }
+
if (row == 'lead_name') {
_rows[row] = {
label: lead.lead_name,
diff --git a/frontend/src/pages/MobileContact.vue b/frontend/src/pages/MobileContact.vue
index c39d6bc32..ca08c833b 100644
--- a/frontend/src/pages/MobileContact.vue
+++ b/frontend/src/pages/MobileContact.vue
@@ -145,6 +145,7 @@
@@ -186,13 +187,9 @@ import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue'
import AddressModal from '@/components/Modals/AddressModal.vue'
-import {
- formatDate,
- timeAgo,
- formatNumberIntoCurrency,
- createToast,
-} from '@/utils'
+import { formatDate, timeAgo, createToast } from '@/utils'
import { getView } from '@/utils/view'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global.js'
import { usersStore } from '@/stores/users.js'
import { organizationsStore } from '@/stores/organizations.js'
@@ -581,6 +578,8 @@ async function updateField(fieldname, value) {
contact.reload()
}
+const { getFormattedCurrency } = getMeta('CRM Deal')
+
const columns = computed(() => dealColumns)
function getDealRowObject(deal) {
@@ -590,10 +589,7 @@ function getDealRowObject(deal) {
label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo,
},
- annual_revenue: formatNumberIntoCurrency(
- deal.annual_revenue,
- deal.currency,
- ),
+ annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: {
label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass,
@@ -620,6 +616,7 @@ const dealColumns = [
{
label: __('Amount'),
key: 'annual_revenue',
+ align: 'right',
width: '9rem',
},
{
diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue
index f2c784c26..8936921f7 100644
--- a/frontend/src/pages/MobileDeal.vue
+++ b/frontend/src/pages/MobileDeal.vue
@@ -102,6 +102,7 @@
v-if="section.fields"
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
+ doctype="CRM Deal"
v-model="deal.data"
@update="updateField"
/>
diff --git a/frontend/src/pages/MobileOrganization.vue b/frontend/src/pages/MobileOrganization.vue
index 58bd77d02..0a06f695f 100644
--- a/frontend/src/pages/MobileOrganization.vue
+++ b/frontend/src/pages/MobileOrganization.vue
@@ -125,9 +125,10 @@
>
@@ -176,16 +177,12 @@ import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view'
-import {
- formatDate,
- timeAgo,
- formatNumberIntoCurrency,
- createToast,
-} from '@/utils'
+import { formatDate, timeAgo, createToast } from '@/utils'
import {
Breadcrumbs,
Avatar,
@@ -441,6 +438,8 @@ const rows = computed(() => {
})
})
+const { getFormattedCurrency } = getMeta('CRM Deal')
+
const columns = computed(() => {
return tabIndex.value === 0 ? dealColumns : contactColumns
})
@@ -450,12 +449,9 @@ function getDealRowObject(deal) {
name: deal.name,
organization: {
label: deal.organization,
- logo: props.organization?.organization_logo,
+ logo: organization.doc?.organization_logo,
},
- annual_revenue: formatNumberIntoCurrency(
- deal.annual_revenue,
- deal.currency,
- ),
+ annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: {
label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass,
@@ -485,7 +481,7 @@ function getContactRowObject(contact) {
mobile_no: contact.mobile_no,
company_name: {
label: contact.company_name,
- logo: props.organization?.organization_logo,
+ logo: organization.doc?.organization_logo,
},
modified: {
label: formatDate(contact.modified),
@@ -503,6 +499,7 @@ const dealColumns = [
{
label: __('Amount'),
key: 'annual_revenue',
+ align: 'right',
width: '9rem',
},
{
diff --git a/frontend/src/pages/Organization.vue b/frontend/src/pages/Organization.vue
index d84fd232e..831a5b2a1 100644
--- a/frontend/src/pages/Organization.vue
+++ b/frontend/src/pages/Organization.vue
@@ -125,9 +125,10 @@
@@ -211,16 +212,12 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
+import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view'
-import {
- formatDate,
- timeAgo,
- formatNumberIntoCurrency,
- createToast,
-} from '@/utils'
+import { formatDate, timeAgo, createToast } from '@/utils'
import {
Tooltip,
Breadcrumbs,
@@ -476,6 +473,8 @@ const rows = computed(() => {
})
})
+const { getFormattedCurrency } = getMeta('CRM Deal')
+
const columns = computed(() => {
return tabIndex.value === 0 ? dealColumns : contactColumns
})
@@ -485,12 +484,9 @@ function getDealRowObject(deal) {
name: deal.name,
organization: {
label: deal.organization,
- logo: props.organization?.organization_logo,
+ logo: organization.doc?.organization_logo,
},
- annual_revenue: formatNumberIntoCurrency(
- deal.annual_revenue,
- deal.currency,
- ),
+ annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: {
label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass,
@@ -520,7 +516,7 @@ function getContactRowObject(contact) {
mobile_no: contact.mobile_no,
company_name: {
label: contact.company_name,
- logo: props.organization?.organization_logo,
+ logo: organization.doc?.organization_logo,
},
modified: {
label: formatDate(contact.modified),
@@ -538,6 +534,7 @@ const dealColumns = [
{
label: __('Amount'),
key: 'annual_revenue',
+ align: 'right',
width: '9rem',
},
{
diff --git a/frontend/src/pages/Organizations.vue b/frontend/src/pages/Organizations.vue
index 60a2204b6..4853d0966 100644
--- a/frontend/src/pages/Organizations.vue
+++ b/frontend/src/pages/Organizations.vue
@@ -69,9 +69,13 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
import ViewControls from '@/components/ViewControls.vue'
-import { formatDate, timeAgo, website, formatNumberIntoCurrency } from '@/utils'
+import { getMeta } from '@/stores/meta'
+import { formatDate, timeAgo, website } from '@/utils'
import { ref, computed } from 'vue'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('CRM Organization')
+
const organizationsListView = ref(null)
const showOrganizationModal = ref(false)
@@ -110,6 +114,18 @@ const rows = computed(() => {
)
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, organization)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, organization)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, organization)
+ }
+
if (row === 'organization_name') {
_rows[row] = {
label: organization.organization_name,
@@ -117,11 +133,6 @@ const rows = computed(() => {
}
} else if (row === 'website') {
_rows[row] = website(organization.website)
- } else if (row === 'annual_revenue') {
- _rows[row] = formatNumberIntoCurrency(
- organization.annual_revenue,
- organization.currency,
- )
} else if (['modified', 'creation'].includes(row)) {
_rows[row] = {
label: formatDate(organization[row]),
diff --git a/frontend/src/pages/Tasks.vue b/frontend/src/pages/Tasks.vue
index 0349c8768..69662d9e2 100644
--- a/frontend/src/pages/Tasks.vue
+++ b/frontend/src/pages/Tasks.vue
@@ -204,12 +204,15 @@ import ViewControls from '@/components/ViewControls.vue'
import TasksListView from '@/components/ListViews/TasksListView.vue'
import KanbanView from '@/components/Kanban/KanbanView.vue'
import TaskModal from '@/components/Modals/TaskModal.vue'
+import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users'
import { formatDate, timeAgo } from '@/utils'
import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('CRM Task')
const { getUser } = usersStore()
const router = useRouter()
@@ -271,6 +274,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(task[row], '', true, fieldType == 'Datetime')
}
+ if (fieldType && fieldType == 'Currency') {
+ _rows[row] = getFormattedCurrency(row, task)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ _rows[row] = getFormattedFloat(row, task)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ _rows[row] = getFormattedPercent(row, task)
+ }
+
if (['modified', 'creation'].includes(row)) {
_rows[row] = {
label: formatDate(task[row]),
diff --git a/frontend/src/router.js b/frontend/src/router.js
index a64bdd8ba..7ce0f1105 100644
--- a/frontend/src/router.js
+++ b/frontend/src/router.js
@@ -18,7 +18,6 @@ const routes = [
path: '/leads/view/:viewType?',
name: 'Leads',
component: () => import('@/pages/Leads.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
path: '/leads/:leadId',
@@ -31,7 +30,6 @@ const routes = [
path: '/deals/view/:viewType?',
name: 'Deals',
component: () => import('@/pages/Deals.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
path: '/deals/:dealId',
@@ -56,7 +54,6 @@ const routes = [
path: '/contacts/view/:viewType?',
name: 'Contacts',
component: () => import('@/pages/Contacts.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
path: '/contacts/:contactId',
@@ -69,7 +66,6 @@ const routes = [
path: '/organizations/view/:viewType?',
name: 'Organizations',
component: () => import('@/pages/Organizations.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
path: '/organizations/:organizationId',
@@ -82,14 +78,12 @@ const routes = [
path: '/call-logs/view/:viewType?',
name: 'Call Logs',
component: () => import('@/pages/CallLogs.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
alias: '/email-templates',
path: '/email-templates/view/:viewType?',
name: 'Email Templates',
component: () => import('@/pages/EmailTemplates.vue'),
- meta: { scrollPos: { top: 0, left: 0 } },
},
{
path: '/email-templates/:emailTemplateId',
@@ -108,29 +102,9 @@ const handleMobileView = (componentName) => {
return window.innerWidth < 768 ? `Mobile${componentName}` : componentName
}
-const scrollBehavior = (to, from, savedPosition) => {
- if (to.name === from.name) {
- to.meta?.scrollPos && (to.meta.scrollPos.top = 0)
- return { left: 0, top: 0 }
- }
- const scrollpos = to.meta?.scrollPos || { left: 0, top: 0 }
-
- if (scrollpos.top > 0) {
- setTimeout(() => {
- let el = document.querySelector('#list-rows')
- el.scrollTo({
- top: scrollpos.top,
- left: scrollpos.left,
- behavior: 'smooth',
- })
- }, 300)
- }
-}
-
let router = createRouter({
history: createWebHistory('/crm'),
routes,
- scrollBehavior,
})
router.beforeEach(async (to, from, next) => {
@@ -138,10 +112,6 @@ router.beforeEach(async (to, from, next) => {
isLoggedIn && (await userResource.promise)
- if (from.meta?.scrollPos) {
- from.meta.scrollPos.top = document.querySelector('#list-rows')?.scrollTop
- }
-
if (to.name === 'Home' && isLoggedIn) {
next({ name: 'Leads' })
} else if (!isLoggedIn) {
diff --git a/frontend/src/stores/meta.js b/frontend/src/stores/meta.js
new file mode 100644
index 000000000..af2350598
--- /dev/null
+++ b/frontend/src/stores/meta.js
@@ -0,0 +1,62 @@
+import { createResource } from 'frappe-ui'
+import { formatCurrency, formatNumber } from '@/utils/numberFormat.js'
+import { reactive } from 'vue'
+
+const doctypeMeta = reactive({})
+
+export function getMeta(doctype) {
+ const meta = createResource({
+ url: 'frappe.desk.form.load.getdoctype',
+ params: {
+ doctype: doctype,
+ with_parent: 1,
+ cached_timestamp: null,
+ },
+ cache: ['Meta', doctype],
+ onSuccess: (res) => {
+ let dtMetas = res.docs
+ for (let dtMeta of dtMetas) {
+ doctypeMeta[dtMeta.name] = dtMeta
+ }
+ },
+ })
+
+ if (!doctypeMeta[doctype]) {
+ meta.fetch()
+ }
+
+ function getFormattedPercent(fieldname, doc) {
+ let value = getFormattedFloat(fieldname, doc)
+ return value + '%'
+ }
+
+ function getFormattedFloat(fieldname, doc) {
+ let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
+ let precision = df?.precision || null
+ return formatNumber(doc[fieldname], '', precision)
+ }
+
+ function getFormattedCurrency(fieldname, doc) {
+ let currency = window.sysdefaults.currency || 'USD'
+ let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
+ let precision = df?.precision || null
+
+ if (df && df.options) {
+ if (df.options.indexOf(':') != -1) {
+ currency = currency
+ } else if (doc && doc[df.options]) {
+ currency = doc[df.options]
+ }
+ }
+
+ return formatCurrency(doc[fieldname], '', currency, precision)
+ }
+
+ return {
+ meta,
+ doctypeMeta,
+ getFormattedFloat,
+ getFormattedPercent,
+ getFormattedCurrency,
+ }
+}
diff --git a/frontend/src/utils/callLog.js b/frontend/src/utils/callLog.js
index ee006d1a0..ac10e74d6 100644
--- a/frontend/src/utils/callLog.js
+++ b/frontend/src/utils/callLog.js
@@ -1,7 +1,10 @@
import { secondsToDuration, formatDate, timeAgo } from '@/utils'
+import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
+const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
+ getMeta('CRM Call Log')
const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
@@ -58,6 +61,18 @@ export function getCallLogDetail(row, log, columns = []) {
return formatDate(log[row], '', true, fieldType == 'Datetime')
}
+ if (fieldType && fieldType == 'Currency') {
+ return getFormattedCurrency(row, log)
+ }
+
+ if (fieldType && fieldType == 'Float') {
+ return getFormattedFloat(row, log)
+ }
+
+ if (fieldType && fieldType == 'Percent') {
+ return getFormattedPercent(row, log)
+ }
+
return log[row]
}
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
index 744b55792..557e85880 100644
--- a/frontend/src/utils/index.js
+++ b/frontend/src/utils/index.js
@@ -126,17 +126,6 @@ export function secondsToDuration(seconds) {
return `${hours}h ${minutes}m ${_seconds}s`
}
-export function formatNumberIntoCurrency(value, currency = 'INR') {
- if (value) {
- return value.toLocaleString('en-IN', {
- maximumFractionDigits: 0,
- style: 'currency',
- currency: currency ? currency : 'INR',
- })
- }
- return ''
-}
-
export function startCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
diff --git a/frontend/src/utils/numberFormat.js b/frontend/src/utils/numberFormat.js
new file mode 100644
index 000000000..7ee2128fa
--- /dev/null
+++ b/frontend/src/utils/numberFormat.js
@@ -0,0 +1,286 @@
+import { get } from '@vueuse/core'
+
+const NUMBER_FORMAT_INFO = {
+ '#,###.##': { decimalStr: '.', groupSep: ',' },
+ '#.###,##': { decimalStr: ',', groupSep: '.' },
+ '# ###.##': { decimalStr: '.', groupSep: ' ' },
+ '# ###,##': { decimalStr: ',', groupSep: ' ' },
+ "#'###.##": { decimalStr: '.', groupSep: "'" },
+ '#, ###.##': { decimalStr: '.', groupSep: ', ' },
+ '#,##,###.##': { decimalStr: '.', groupSep: ',' },
+ '#,###.###': { decimalStr: '.', groupSep: ',' },
+ '#.###': { decimalStr: '', groupSep: '.' },
+ '#,###': { decimalStr: '', groupSep: ',' },
+}
+
+export function replaceAll(s, t1, t2) {
+ return s.split(t1).join(t2)
+}
+
+export function strip(s, chars) {
+ if (s) {
+ s = lstrip(s, chars)
+ s = rstrip(s, chars)
+ return s
+ }
+}
+
+export function lstrip(s, chars) {
+ if (!chars) chars = ['\n', '\t', ' ']
+ // strip left
+ let first_char = s.substr(0, 1)
+ while (chars.includes(first_char)) {
+ s = s.substr(1)
+ first_char = s.substr(0, 1)
+ }
+ return s
+}
+
+export function rstrip(s, chars) {
+ if (!chars) chars = ['\n', '\t', ' ']
+ let last_char = s.substr(s.length - 1)
+ while (chars.includes(last_char)) {
+ s = s.substr(0, s.length - 1)
+ last_char = s.substr(s.length - 1)
+ }
+ return s
+}
+
+export function cstr(s) {
+ if (s == null) return ''
+ return s + ''
+}
+
+export function cint(v, def) {
+ if (v === true) return 1
+ if (v === false) return 0
+ v = v + ''
+ if (v !== '0') v = lstrip(v, ['0'])
+ v = parseInt(v) // eslint-ignore-line
+ if (isNaN(v)) v = def === undefined ? 0 : def
+ return v
+}
+
+export function flt(v, decimals, numberFormat, roundingMethod) {
+ if (v == null || v == '') return 0
+
+ if (typeof v !== 'number') {
+ v = v + ''
+
+ // strip currency symbol if exists
+ if (v.indexOf(' ') != -1) {
+ // using slice(1).join(" ") because space could also be a group separator
+ var parts = v.split(' ')
+ v = isNaN(parseFloat(parts[0]))
+ ? parts.slice(parts.length - 1).join(' ')
+ : v
+ }
+
+ v = stripNumberGroups(v, numberFormat)
+
+ v = parseFloat(v)
+ if (isNaN(v)) v = 0
+ }
+
+ if (decimals != null) return roundNumber(v, decimals, roundingMethod)
+ return v
+}
+
+function stripNumberGroups(v, numberFormat) {
+ if (!numberFormat) numberFormat = getNumberFormat()
+ var info = getNumberFormatInfo(numberFormat)
+
+ // strip groups (,)
+ var groupRegex = new RegExp(
+ info.groupSep === '.' ? '\\.' : info.groupSep,
+ 'g',
+ )
+ v = v.replace(groupRegex, '')
+
+ // replace decimal separator with (.)
+ if (info.decimalStr !== '.' && info.decimalStr !== '') {
+ var decimal_regex = new RegExp(info.decimalStr, 'g')
+ v = v.replace(decimal_regex, '.')
+ }
+
+ return v
+}
+
+export function formatNumber(v, format, decimals) {
+ if (!format) {
+ format = getNumberFormat()
+ if (decimals == null)
+ decimals = cint(window.sysdefaults.float_precision || 3)
+ }
+
+ let info = getNumberFormatInfo(format)
+
+ // Fix the decimal first, toFixed will auto fill trailing zero.
+ if (decimals == null) decimals = info.precision
+
+ v = flt(v, decimals, format)
+
+ let isNegative = false
+ if (v < 0) isNegative = true
+ v = Math.abs(v)
+
+ v = v.toFixed(decimals)
+
+ let part = v.split('.')
+
+ // get group position and parts
+ let groupPosition = info.groupSep ? 3 : 0
+
+ if (groupPosition) {
+ let integer = part[0]
+ let str = ''
+ for (let i = integer.length; i >= 0; i--) {
+ let l = replaceAll(str, info.groupSep, '').length
+ if (format == '#,##,###.##' && str.indexOf(',') != -1) {
+ // INR
+ groupPosition = 2
+ l += 1
+ }
+
+ str += integer.charAt(i)
+
+ if (l && !((l + 1) % groupPosition) && i != 0) {
+ str += info.groupSep
+ }
+ }
+ part[0] = str.split('').reverse().join('')
+ }
+ if (part[0] + '' == '') {
+ part[0] = '0'
+ }
+
+ // join decimal
+ part[1] = part[1] && info.decimalStr ? info.decimalStr + part[1] : ''
+
+ // join
+ return (isNegative ? '-' : '') + part[0] + part[1]
+}
+
+export function formatCurrency(value, format, currency = 'USD', precision = 2) {
+ value = value == null || value === '' ? 0 : value
+
+ if (typeof precision != 'number') {
+ precision = cint(precision || window.sysdefaults.currency_precision || 2)
+ }
+
+ // If you change anything below, it's going to hurt a company in UAE, a bit.
+ if (precision > 2) {
+ let parts = cstr(value).split('.') // should be minimum 2, comes from the DB
+ let decimals = parts.length > 1 ? parts[1] : '' // parts.length == 2 ???
+
+ if (decimals.length < 3 || decimals.length < precision) {
+ const fraction = 100
+
+ if (decimals.length < cstr(fraction).length) {
+ precision = cstr(fraction).length - 1
+ }
+ }
+ }
+
+ format = getNumberFormat(format)
+ let symbol = getCurrencySymbol(currency)
+
+ if (symbol) {
+ return __(symbol) + ' ' + formatNumber(value, format, precision)
+ }
+
+ return formatNumber(value, format, precision)
+}
+
+function getNumberFormat(format = null) {
+ return format || window.sysdefaults.number_format || '#,###.##'
+}
+
+function getCurrencySymbol(currencyCode) {
+ try {
+ const formatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: currencyCode,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ })
+ // Extract the currency symbol from the formatted string
+ const parts = formatter.formatToParts(1)
+ const symbol = parts.find((part) => part.type === 'currency')
+ return symbol ? symbol.value : null
+ } catch (error) {
+ console.error(`Invalid currency code: ${currencyCode}`)
+ return null
+ }
+}
+
+function getNumberFormatInfo(format) {
+ let info = NUMBER_FORMAT_INFO[format]
+
+ if (!info) {
+ info = { decimalStr: '.', groupSep: ',' }
+ }
+
+ // get the precision from the number format
+ info.precision = format.split(info.decimalStr).slice(1)[0].length
+
+ return info
+}
+
+function roundNumber(num, precision, roundingMethod) {
+ roundingMethod =
+ roundingMethod ||
+ window.sysdefaults.rounding_method ||
+ "Banker's Rounding (legacy)"
+
+ let isNegative = num < 0 ? true : false
+
+ if (roundingMethod == "Banker's Rounding (legacy)") {
+ var d = cint(precision)
+ var m = Math.pow(10, d)
+ var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8) // Avoid rounding errors
+ var i = Math.floor(n),
+ f = n - i
+ var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n)
+ r = d ? r / m : r
+ return isNegative ? -r : r
+ } else if (roundingMethod == "Banker's Rounding") {
+ if (num == 0) return 0.0
+ precision = cint(precision)
+
+ let multiplier = Math.pow(10, precision)
+ num = Math.abs(num) * multiplier
+
+ let floorNum = Math.floor(num)
+ let decimalPart = num - floorNum
+
+ // For explanation of this method read python flt implementation notes.
+ let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0)
+
+ if (Math.abs(decimalPart - 0.5) < epsilon) {
+ num = floorNum % 2 == 0 ? floorNum : floorNum + 1
+ } else {
+ num = Math.round(num)
+ }
+ num = num / multiplier
+ return isNegative ? -num : num
+ } else if (roundingMethod == 'Commercial Rounding') {
+ if (num == 0) return 0.0
+
+ let digits = cint(precision)
+ let multiplier = Math.pow(10, digits)
+
+ num = num * multiplier
+
+ // For explanation of this method read python flt implementation notes.
+ let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0)
+ if (isNegative) {
+ epsilon = -1 * epsilon
+ }
+
+ num = Math.round(num + epsilon)
+ return num / multiplier
+ } else {
+ throw new Error(`Unknown rounding method ${roundingMethod}`)
+ }
+}