Merge pull request #488 from frappe/develop

This commit is contained in:
Shariq Ansari 2024-12-25 19:46:51 +05:30 committed by GitHub
commit 298aabb096
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1068 additions and 564 deletions

View File

@ -139,6 +139,7 @@ class CRMDeal(Document):
'label': 'Amount', 'label': 'Amount',
'type': 'Currency', 'type': 'Currency',
'key': 'annual_revenue', 'key': 'annual_revenue',
'align': 'right',
'width': '9rem', 'width': '9rem',
}, },
{ {

View File

@ -32,7 +32,12 @@
<span>{{ __('Loading...') }}</span> <span>{{ __('Loading...') }}</span>
</div> </div>
<div v-else> <div v-else>
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data.doc" /> <FieldLayout
v-if="tabs.data"
:tabs="tabs.data"
:data="data.doc"
:doctype="doctype"
/>
</div> </div>
<DataFieldsModal <DataFieldsModal
v-if="showDataFieldsModal" v-if="showDataFieldsModal"

View File

@ -49,7 +49,9 @@
</div> </div>
</template> </template>
</Draggable> </Draggable>
<div class="mt-1.5 flex flex-col gap-1 border-t border-outline-gray-modals pt-1.5"> <div
class="mt-1.5 flex flex-col gap-1 border-t border-outline-gray-modals pt-1.5"
>
<Autocomplete <Autocomplete
value="" value=""
:options="fields" :options="fields"
@ -213,11 +215,13 @@ const fields = computed(() => {
}) })
function addColumn(c) { function addColumn(c) {
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type) ? 'right' : 'left'
let _column = { let _column = {
label: c.label, label: c.label,
type: c.type, type: c.type,
key: c.value, key: c.value,
width: '10rem', width: '10rem',
align,
} }
columns.value.push(_column) columns.value.push(_column)
rows.value.push(c.value) rows.value.push(c.value)

View File

@ -190,6 +190,30 @@
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
v-model="data[field.name]" v-model="data[field.name]"
/> />
<FormControl
v-else-if="field.type === 'Percent'"
type="text"
:value="getFormattedPercent(field.name, data)"
:placeholder="getPlaceholder(field)"
:disabled="Boolean(field.read_only)"
@change="data[field.name] = flt($event.target.value)"
/>
<FormControl
v-else-if="field.type === 'Float'"
type="text"
:value="getFormattedFloat(field.name, data)"
:placeholder="getPlaceholder(field)"
:disabled="Boolean(field.read_only)"
@change="data[field.name] = flt($event.target.value)"
/>
<FormControl
v-else-if="field.type === 'Currency'"
type="text"
:value="getFormattedCurrency(field.name, data)"
:placeholder="getPlaceholder(field)"
:disabled="Boolean(field.read_only)"
@change="data[field.name] = flt($event.target.value)"
/>
<FormControl <FormControl
v-else v-else
type="text" type="text"
@ -213,22 +237,30 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { getMeta } from '../stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { getFormat } from '@/utils' import { getFormat } from '@/utils'
import { flt } from '@/utils/numberFormat.js'
import { Tabs, Tooltip, DatePicker, DateTimePicker } from 'frappe-ui' import { Tabs, Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const { getUser } = usersStore()
const props = defineProps({ const props = defineProps({
tabs: Array, tabs: Array,
data: Object, data: Object,
doctype: {
type: String,
default: 'CRM Lead',
},
modal: { modal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) })
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta(props.doctype)
const { getUser } = usersStore()
const hasTabs = computed(() => !props.tabs[0].no_tabs) const hasTabs = computed(() => !props.tabs[0].no_tabs)
const tabIndex = ref(0) const tabIndex = ref(0)

View File

@ -11,7 +11,10 @@
row-key="name" row-key="name"
v-bind="$attrs" v-bind="$attrs"
> >
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')"> <ListHeader
class="sm:mx-5 mx-3"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem <ListHeaderItem
v-for="column in columns" v-for="column in columns"
:key="column.key" :key="column.key"
@ -29,32 +32,52 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows"> <ListRows
<ListRow class="mx-3 sm:mx-5"
v-for="row in rows" :rows="rows"
:key="row.name" v-slot="{ idx, column, item }"
v-slot="{ idx, column, item }" doctype="CRM Call Log"
:row="row" >
> <ListRowItem :item="item" :align="column.align">
<ListRowItem :item="item"> <template #prefix>
<template #prefix> <div v-if="['caller', 'receiver'].includes(column.key)">
<div v-if="['caller', 'receiver'].includes(column.key)"> <Avatar
<Avatar v-if="item.label"
v-if="item.label" class="flex items-center"
class="flex items-center" :image="item.image"
:image="item.image" :label="item.label"
:label="item.label" size="sm"
size="sm" />
/> </div>
</div> <div v-else-if="['type', 'duration'].includes(column.key)">
<div v-else-if="['type', 'duration'].includes(column.key)"> <FeatherIcon :name="item.icon" class="h-3 w-3" />
<FeatherIcon :name="item.icon" class="h-3 w-3" /> </div>
</div> </template>
</template> <template #default="{ label }">
<template #default="{ label }"> <div
<div v-if="['modified', 'creation'].includes(column.key)"
v-if="['modified', 'creation'].includes(column.key)" class="truncate text-base"
class="truncate text-base" @click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div v-else-if="column.key === 'status'" class="truncate text-base">
<Badge
:variant="'subtle'"
:theme="item.color"
size="md"
:label="__(item.label)"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -65,69 +88,46 @@
firstColumn: columns[0], firstColumn: columns[0],
}) })
" "
> />
<Tooltip :text="item.label"> </div>
<div>{{ item.timeAgo }}</div> <div v-else-if="column.type === 'Check'">
</Tooltip> <FormControl
</div> type="checkbox"
<div v-else-if="column.key === 'status'" class="truncate text-base"> :modelValue="item"
<Badge :disabled="true"
:variant="'subtle'" class="text-ink-gray-9"
:theme="item.color" />
size="md" </div>
:label="__(item.label)" <div v-else-if="column.key === '_liked_by'">
@click=" <Button
(event) => v-if="column.key == '_liked_by'"
emit('applyFilter', { variant="ghosted"
event, :class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
idx, @click.stop.prevent="
column, () => emit('likeDoc', { name: row.name, liked: isLiked(item) })
item,
firstColumn: columns[0],
})
"
/>
</div>
<div v-else-if="column.type === 'Check'">
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-ink-gray-9"
/>
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
v-if="column.key == '_liked_by'"
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() =>
emit('likeDoc', { name: row.name, liked: isLiked(item) })
"
>
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
" "
> >
{{ label }} <HeartIcon class="h-4 w-4" />
</div> </Button>
</template> </div>
</ListRowItem> <div
</ListRow> v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
@ -161,13 +161,12 @@
<script setup> <script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue' import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner, ListSelectBanner,
ListRowItem, ListRowItem,
ListFooter, ListFooter,
@ -232,7 +231,7 @@ const listBulkActionsRef = ref(null)
defineExpose({ defineExpose({
customListActions: computed( customListActions: computed(
() => listBulkActionsRef.value?.customListActions () => listBulkActionsRef.value?.customListActions,
), ),
}) })
</script> </script>

View File

@ -36,96 +36,93 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows"> <ListRows
<ListRow class="mx-3 sm:mx-5"
v-for="row in rows" :rows="rows"
:key="row.name" v-slot="{ idx, column, item }"
v-slot="{ idx, column, item }" doctype="Contact"
:row="row" >
> <ListRowItem :item="item" :align="column.align">
<ListRowItem :item="item"> <template #prefix>
<template #prefix> <div v-if="column.key === 'full_name'">
<div v-if="column.key === 'full_name'"> <Avatar
<Avatar v-if="item.label"
v-if="item.label" class="flex items-center"
class="flex items-center" :image="item.image"
:image="item.image" :label="item.image_label"
:label="item.image_label" size="sm"
size="sm" />
/> </div>
</div> <div v-else-if="column.key === 'company_name'">
<div v-else-if="column.key === 'company_name'"> <Avatar
<Avatar v-if="item.label"
v-if="item.label" class="flex items-center"
class="flex items-center" :image="item.logo"
:image="item.logo" :label="item.label"
:label="item.label" size="sm"
size="sm" />
/> </div>
</div> <div v-else-if="column.key === 'mobile_no'">
<div v-else-if="column.key === 'mobile_no'"> <PhoneIcon class="h-4 w-4" />
<PhoneIcon class="h-4 w-4" /> </div>
</div> </template>
</template> <template #default="{ label }">
<template #default="{ label }"> <div
<div v-if="['modified', 'creation'].includes(column.key)"
v-if="['modified', 'creation'].includes(column.key)" class="truncate text-base"
class="truncate text-base" @click="
@click=" (event) =>
(event) => emit('applyFilter', {
emit('applyFilter', { event,
event, idx,
idx, column,
column, item,
item, firstColumn: columns[0],
firstColumn: columns[0], })
}) "
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div v-else-if="column.type === 'Check'">
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-ink-gray-9"
/>
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
v-if="column.key == '_liked_by'"
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() => emit('likeDoc', { name: row.name, liked: isLiked(item) })
" "
> >
<Tooltip :text="item.label"> <HeartIcon class="h-4 w-4" />
<div>{{ item.timeAgo }}</div> </Button>
</Tooltip> </div>
</div> <div
<div v-else-if="column.type === 'Check'"> v-else
<FormControl class="truncate text-base"
type="checkbox" @click="
:modelValue="item" (event) =>
:disabled="true" emit('applyFilter', {
class="text-ink-gray-9" event,
/> idx,
</div> column,
<div v-else-if="column.key === '_liked_by'"> item,
<Button firstColumn: columns[0],
v-if="column.key == '_liked_by'" })
variant="ghosted" "
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'" >
@click.stop.prevent=" {{ label }}
() => </div>
emit('likeDoc', { name: row.name, liked: isLiked(item) }) </template>
" </ListRowItem>
>
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRow>
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
@ -160,13 +157,12 @@
import HeartIcon from '@/components/Icons/HeartIcon.vue' import HeartIcon from '@/components/Icons/HeartIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner, ListSelectBanner,
ListRowItem, ListRowItem,
ListFooter, ListFooter,

View File

@ -36,7 +36,11 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows :rows="rows" v-slot="{ idx, column, item, row }"> <ListRows
:rows="rows"
v-slot="{ idx, column, item, row }"
doctype="CRM Deal"
>
<div v-if="column.key === '_assign'" class="flex items-center"> <div v-if="column.key === '_assign'" class="flex items-center">
<MultipleAvatar <MultipleAvatar
:avatars="item" :avatars="item"
@ -53,7 +57,7 @@
" "
/> />
</div> </div>
<ListRowItem v-else :item="item"> <ListRowItem v-else :item="item" :align="column.align">
<template #prefix> <template #prefix>
<div v-if="column.key === 'status'"> <div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" /> <IndicatorIcon :class="item.color" />

View File

@ -10,7 +10,10 @@
}" }"
row-key="name" row-key="name"
> >
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')"> <ListHeader
class="sm:mx-5 mx-3"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem <ListHeaderItem
v-for="column in columns" v-for="column in columns"
:key="column.key" :key="column.key"
@ -28,21 +31,41 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows"> <ListRows
<ListRow class="mx-3 sm:mx-5"
v-for="row in rows" :rows="rows"
:key="row.name" v-slot="{ idx, column, item }"
v-slot="{ idx, column, item }" doctype="Email Template"
:row="row" >
> <ListRowItem :item="item" :align="column.align">
<ListRowItem :item="item"> <!-- <template #prefix>
<!-- <template #prefix>
</template> --> </template> -->
<template #default="{ label }"> <template #default="{ label }">
<div <div
v-if="['modified', 'creation'].includes(column.key)" v-if="['modified', 'creation'].includes(column.key)"
class="truncate text-base" class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div v-else-if="column.key === 'status'" class="truncate text-base">
<Badge
:variant="'subtle'"
:theme="item.color"
size="md"
:label="item.label"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -53,69 +76,46 @@
firstColumn: columns[0], firstColumn: columns[0],
}) })
" "
> />
<Tooltip :text="item.label"> </div>
<div>{{ item.timeAgo }}</div> <div v-else-if="column.type === 'Check'">
</Tooltip> <FormControl
</div> type="checkbox"
<div v-else-if="column.key === 'status'" class="truncate text-base"> :modelValue="item"
<Badge :disabled="true"
:variant="'subtle'" class="text-ink-gray-9"
:theme="item.color" />
size="md" </div>
:label="item.label" <div v-else-if="column.key === '_liked_by'">
@click=" <Button
(event) => v-if="column.key == '_liked_by'"
emit('applyFilter', { variant="ghosted"
event, :class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
idx, @click.stop.prevent="
column, () => emit('likeDoc', { name: row.name, liked: isLiked(item) })
item,
firstColumn: columns[0],
})
"
/>
</div>
<div v-else-if="column.type === 'Check'">
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-ink-gray-9"
/>
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
v-if="column.key == '_liked_by'"
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() =>
emit('likeDoc', { name: row.name, liked: isLiked(item) })
"
>
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
" "
> >
{{ label }} <HeartIcon class="h-4 w-4" />
</div> </Button>
</template> </div>
</ListRowItem> <div
</ListRow> v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
@ -148,12 +148,11 @@
<script setup> <script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue' import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { import {
ListView, ListView,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner, ListSelectBanner,
ListRowItem, ListRowItem,
ListFooter, ListFooter,
@ -219,7 +218,7 @@ const listBulkActionsRef = ref(null)
defineExpose({ defineExpose({
customListActions: computed( customListActions: computed(
() => listBulkActionsRef.value?.customListActions () => listBulkActionsRef.value?.customListActions,
), ),
}) })
</script> </script>

View File

@ -36,7 +36,11 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows :rows="rows" v-slot="{ idx, column, item, row }"> <ListRows
:rows="rows"
v-slot="{ idx, column, item, row }"
doctype="CRM Lead"
>
<div v-if="column.key === '_assign'" class="flex items-center"> <div v-if="column.key === '_assign'" class="flex items-center">
<MultipleAvatar <MultipleAvatar
:avatars="item" :avatars="item"
@ -53,7 +57,7 @@
" "
/> />
</div> </div>
<ListRowItem v-else :item="item"> <ListRowItem v-else :item="item" :align="column.align">
<template #prefix> <template #prefix>
<div v-if="column.key === 'status'"> <div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" /> <IndicatorIcon :class="item.color" />

View File

@ -15,7 +15,7 @@
</div> </div>
</div> </div>
</ListGroupHeader> </ListGroupHeader>
<ListGroupRows :group="group" id="list-rows"> <ListGroupRows :group="group">
<ListRow <ListRow
v-for="row in group.rows" v-for="row in group.rows"
:key="row.name" :key="row.name"
@ -27,7 +27,12 @@
</ListGroupRows> </ListGroupRows>
</div> </div>
</div> </div>
<ListRows class="mx-3 sm:mx-5" v-else id="list-rows"> <ListRows
v-else
ref="scrollContainer"
class="mx-3 sm:mx-5"
@scroll="handleScroll"
>
<ListRow <ListRow
v-for="row in reactivieRows" v-for="row in reactivieRows"
:key="row.name" :key="row.name"
@ -40,27 +45,53 @@
</template> </template>
<script setup> <script setup>
import { useStorage } from '@vueuse/core'
import { ListRows, ListRow, ListGroupHeader, ListGroupRows } from 'frappe-ui' import { ListRows, ListRow, ListGroupHeader, ListGroupRows } from 'frappe-ui'
import { ref, computed, watch, onBeforeUnmount, onMounted } from 'vue'
import { ref, computed, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
type: Array, type: Array,
required: true, required: true,
}, },
doctype: {
type: String,
default: 'CRM Lead',
},
}) })
const reactivieRows = ref(props.rows) const reactivieRows = ref(props.rows)
watch( watch(
() => props.rows, () => props.rows,
(val) => (reactivieRows.value = val) (val) => (reactivieRows.value = val),
) )
let showGroupedRows = computed(() => { let showGroupedRows = computed(() => {
return props.rows.every( return props.rows.every(
(row) => row.group && row.rows && Array.isArray(row.rows) (row) => row.group && row.rows && Array.isArray(row.rows),
) )
}) })
const scrollPosition = useStorage(`scrollPosition${props.doctype}`, 0)
const scrollContainer = ref(null)
const handleScroll = () => {
if (scrollContainer.value) {
scrollPosition.value = scrollContainer.value.$el.scrollTop
}
}
onBeforeUnmount(() => {
if (scrollContainer.value) {
scrollContainer.value.$el.removeEventListener('scroll', handleScroll)
}
})
onMounted(() => {
if (scrollContainer.value) {
scrollContainer.value.$el.addEventListener('scroll', handleScroll)
scrollContainer.value.$el.scrollTop = scrollPosition.value
}
})
</script> </script>

View File

@ -14,7 +14,10 @@
}" }"
row-key="name" row-key="name"
> >
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')"> <ListHeader
class="sm:mx-5 mx-3"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem <ListHeaderItem
v-for="column in columns" v-for="column in columns"
:key="column.key" :key="column.key"
@ -32,84 +35,81 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows"> <ListRows
<ListRow class="mx-3 sm:mx-5"
v-for="row in rows" :rows="rows"
:key="row.name" v-slot="{ idx, column, item }"
v-slot="{ idx, column, item }" doctype="CRM Organization"
:row="row" >
> <ListRowItem :item="item" :align="column.align">
<ListRowItem :item="item"> <template #prefix>
<template #prefix> <div v-if="column.key === 'organization_name'">
<div v-if="column.key === 'organization_name'"> <Avatar
<Avatar v-if="item.label"
v-if="item.label" class="flex items-center"
class="flex items-center" :image="item.logo"
:image="item.logo" :label="item.label"
:label="item.label" size="sm"
size="sm" />
/> </div>
</div> </template>
</template> <template #default="{ label }">
<template #default="{ label }"> <div
<div v-if="['modified', 'creation'].includes(column.key)"
v-if="['modified', 'creation'].includes(column.key)" class="truncate text-base"
class="truncate text-base" @click="
@click=" (event) =>
(event) => emit('applyFilter', {
emit('applyFilter', { event,
event, idx,
idx, column,
column, item,
item, firstColumn: columns[0],
firstColumn: columns[0], })
}) "
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div v-else-if="column.type === 'Check'">
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-ink-gray-9"
/>
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
v-if="column.key == '_liked_by'"
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() => emit('likeDoc', { name: row.name, liked: isLiked(item) })
" "
> >
<Tooltip :text="item.label"> <HeartIcon class="h-4 w-4" />
<div>{{ item.timeAgo }}</div> </Button>
</Tooltip> </div>
</div> <div
<div v-else-if="column.type === 'Check'"> v-else
<FormControl class="truncate text-base"
type="checkbox" @click="
:modelValue="item" (event) =>
:disabled="true" emit('applyFilter', {
class="text-ink-gray-9" event,
/> idx,
</div> column,
<div v-else-if="column.key === '_liked_by'"> item,
<Button firstColumn: columns[0],
v-if="column.key == '_liked_by'" })
variant="ghosted" "
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'" >
@click.stop.prevent=" {{ label }}
() => </div>
emit('likeDoc', { name: row.name, liked: isLiked(item) }) </template>
" </ListRowItem>
>
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRow>
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
@ -142,13 +142,12 @@
<script setup> <script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue' import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner, ListSelectBanner,
ListRowItem, ListRowItem,
ListFooter, ListFooter,
@ -216,7 +215,7 @@ const listBulkActionsRef = ref(null)
defineExpose({ defineExpose({
customListActions: computed( customListActions: computed(
() => listBulkActionsRef.value?.customListActions () => listBulkActionsRef.value?.customListActions,
), ),
}) })
</script> </script>

View File

@ -31,107 +31,102 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows"> <ListRows
<ListRow class="mx-3 sm:mx-5"
v-for="row in rows" :rows="rows"
:key="row.name" v-slot="{ idx, column, item }"
v-slot="{ idx, column, item }" doctype="CRM Task"
:row="row" >
> <div v-if="column.key === 'due_date'">
<div v-if="column.key === 'due_date'"> <Tooltip :text="item && formatDate(item, 'ddd, MMM D, YYYY | hh:mm a')">
<Tooltip <div class="flex items-center gap-2 truncate text-base">
:text="item && formatDate(item, 'ddd, MMM D, YYYY | hh:mm a')" <div><CalendarIcon /></div>
> <div v-if="item" class="truncate">
<div class="flex items-center gap-2 truncate text-base"> {{ formatDate(item, 'D MMM, hh:mm a') }}
<div><CalendarIcon /></div>
<div v-if="item" class="truncate">
{{ formatDate(item, 'D MMM, hh:mm a') }}
</div>
</div> </div>
</Tooltip> </div>
</div> </Tooltip>
<ListRowItem v-else :item="item"> </div>
<template #prefix> <ListRowItem v-else :item="item" :align="column.align">
<div v-if="column.key === 'status'"> <template #prefix>
<TaskStatusIcon :status="item" /> <div v-if="column.key === 'status'">
</div> <TaskStatusIcon :status="item" />
<div v-else-if="column.key === 'priority'"> </div>
<TaskPriorityIcon :priority="item" /> <div v-else-if="column.key === 'priority'">
</div> <TaskPriorityIcon :priority="item" />
<div v-else-if="column.key === 'assigned_to'"> </div>
<Avatar <div v-else-if="column.key === 'assigned_to'">
v-if="item.full_name" <Avatar
class="flex items-center" v-if="item.full_name"
:image="item.user_image" class="flex items-center"
:label="item.full_name" :image="item.user_image"
size="sm" :label="item.full_name"
/> size="sm"
</div>
</template>
<template #default="{ label }">
<div
v-if="['modified', 'creation'].includes(column.key)"
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div
v-else-if="column.type === 'Text Editor'"
v-html="item"
class="truncate text-base h-4 [&>p]:truncate"
/> />
<div v-else-if="column.type === 'Check'"> </div>
<FormControl </template>
type="checkbox" <template #default="{ label }">
:modelValue="item" <div
:disabled="true" v-if="['modified', 'creation'].includes(column.key)"
class="text-ink-gray-9" class="truncate text-base"
/> @click="
</div> (event) =>
<div v-else-if="column.key === '_liked_by'"> emit('applyFilter', {
<Button event,
v-if="column.key == '_liked_by'" idx,
variant="ghosted" column,
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'" item,
@click.stop.prevent=" firstColumn: columns[0],
() => })
emit('likeDoc', { name: row.name, liked: isLiked(item) }) "
" >
> <Tooltip :text="item.label">
<HeartIcon class="h-4 w-4" /> <div>{{ item.timeAgo }}</div>
</Button> </Tooltip>
</div> </div>
<div <div
v-else v-else-if="column.type === 'Text Editor'"
class="truncate text-base" v-html="item"
@click=" class="truncate text-base h-4 [&>p]:truncate"
(event) => />
emit('applyFilter', { <div v-else-if="column.type === 'Check'">
event, <FormControl
idx, type="checkbox"
column, :modelValue="item"
item, :disabled="true"
firstColumn: columns[0], class="text-ink-gray-9"
}) />
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
v-if="column.key == '_liked_by'"
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() => emit('likeDoc', { name: row.name, liked: isLiked(item) })
" "
> >
{{ label }} <HeartIcon class="h-4 w-4" />
</div> </Button>
</template> </div>
</ListRowItem> <div
</ListRow> v-else
class="truncate text-base"
@click="
(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
@ -167,14 +162,13 @@ import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue' import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue' import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { formatDate } from '@/utils' import { formatDate } from '@/utils'
import { import {
Avatar, Avatar,
ListView, ListView,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner, ListSelectBanner,
ListRowItem, ListRowItem,
ListFooter, ListFooter,

View File

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div v-if="tabs.data"> <div v-if="tabs.data">
<FieldLayout :tabs="tabs.data" :data="_address" /> <FieldLayout :tabs="tabs.data" :data="_address" doctype="Address" />
<ErrorMessage class="mt-2" :message="error" /> <ErrorMessage class="mt-2" :message="error" />
</div> </div>
</div> </div>
@ -54,7 +54,7 @@ import FieldLayout from '@/components/FieldLayout.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { call, FeatherIcon, createResource, ErrorMessage } from 'frappe-ui' import { FeatherIcon, createResource, ErrorMessage } from 'frappe-ui'
import { ref, nextTick, watch, computed } from 'vue' import { ref, nextTick, watch, computed } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -23,7 +23,11 @@
</div> </div>
</div> </div>
<div v-if="filteredSections.length"> <div v-if="filteredSections.length">
<FieldLayout :tabs="filteredSections" :data="_contact" /> <FieldLayout
:tabs="filteredSections"
:data="_contact"
doctype="Contact"
/>
</div> </div>
</div> </div>
<div class="px-4 pb-7 pt-4 sm:px-6"> <div class="px-4 pb-7 pt-4 sm:px-6">

View File

@ -38,6 +38,7 @@
v-if="filteredSections.length" v-if="filteredSections.length"
:tabs="filteredSections" :tabs="filteredSections"
:data="deal" :data="deal"
doctype="CRM Deal"
/> />
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" /> <ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>

View File

@ -23,7 +23,11 @@
</div> </div>
</div> </div>
<div v-if="filteredSections.length"> <div v-if="filteredSections.length">
<FieldLayout :tabs="filteredSections" :data="_organization" /> <FieldLayout
:tabs="filteredSections"
:data="_organization"
doctype="CRM Organization"
/>
</div> </div>
</div> </div>
<div class="px-4 pb-7 pt-4 sm:px-6"> <div class="px-4 pb-7 pt-4 sm:px-6">

View File

@ -151,7 +151,7 @@ function saveChanges() {
{ {
doctype: _doctype.value, doctype: _doctype.value,
type: 'Side Panel', type: 'Side Panel',
layout: JSON.stringify(_tabs), layout: JSON.stringify(_tabs[0].sections),
}, },
).then(() => { ).then(() => {
loading.value = false loading.value = false

View File

@ -48,5 +48,5 @@ const props = defineProps({
default: 'md', default: 'md',
}, },
}) })
const reverseAvatars = computed(() => props.avatars.reverse()) const reverseAvatars = computed(() => [...props.avatars].reverse())
</script> </script>

View File

@ -12,7 +12,12 @@
/> />
</h2> </h2>
<div v-if="!data.get.loading" class="flex-1 overflow-y-auto"> <div v-if="!data.get.loading" class="flex-1 overflow-y-auto">
<FieldLayout v-if="data?.doc && tabs" :tabs="tabs" :data="data.doc" /> <FieldLayout
v-if="data?.doc && tabs"
:tabs="tabs"
:data="data.doc"
:doctype="doctype"
/>
<ErrorMessage class="mt-2" :message="error" /> <ErrorMessage class="mt-2" :message="error" />
</div> </div>
<div v-else class="flex flex-1 items-center justify-center"> <div v-else class="flex flex-1 items-center justify-center">

View File

@ -168,6 +168,33 @@
@change="(data) => emit('update', field.name, data)" @change="(data) => emit('update', field.name, data)"
/> />
</div> </div>
<FormControl
v-else-if="field.type === 'percent'"
class="form-control"
type="text"
:value="getFormattedPercent(field.name, data)"
:placeholder="field.placeholder"
:debounce="500"
@change.stop="emit('update', field.name, flt($event.target.value))"
/>
<FormControl
v-else-if="field.type === 'float'"
class="form-control"
type="text"
:value="getFormattedFloat(field.name, data)"
:placeholder="field.placeholder"
:debounce="500"
@change.stop="emit('update', field.name, flt($event.target.value))"
/>
<FormControl
v-else-if="field.type === 'currency'"
class="form-control"
type="text"
:value="getFormattedCurrency(field.name, data)"
:placeholder="field.placeholder"
:debounce="500"
@change.stop="emit('update', field.name, flt($event.target.value))"
/>
<FormControl <FormControl
v-else v-else
class="form-control" class="form-control"
@ -203,8 +230,10 @@ import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { getFormat } from '@/utils' import { getFormat } from '@/utils'
import { flt } from '@/utils/numberFormat.js'
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui' import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
import { computed } from 'vue' import { computed } from 'vue'
@ -212,12 +241,18 @@ const props = defineProps({
fields: { fields: {
type: Object, type: Object,
}, },
doctype: {
type: String,
default: 'CRM Lead',
},
isLastSection: { isLastSection: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) })
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta(props.doctype)
const { getUser } = usersStore() const { getUser } = usersStore()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])

View File

@ -142,6 +142,7 @@
v-if="section.fields" v-if="section.fields"
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
doctype="Contact"
v-model="contact.data" v-model="contact.data"
@update="updateField" @update="updateField"
/> />
@ -211,13 +212,9 @@ import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue' import DealsListView from '@/components/ListViews/DealsListView.vue'
import SidePanelModal from '@/components/Modals/SidePanelModal.vue' import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
import AddressModal from '@/components/Modals/AddressModal.vue' import AddressModal from '@/components/Modals/AddressModal.vue'
import { import { formatDate, timeAgo, createToast } from '@/utils'
formatDate,
timeAgo,
formatNumberIntoCurrency,
createToast,
} from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global.js' import { globalStore } from '@/stores/global.js'
import { usersStore } from '@/stores/users.js' import { usersStore } from '@/stores/users.js'
import { organizationsStore } from '@/stores/organizations.js' import { organizationsStore } from '@/stores/organizations.js'
@ -601,6 +598,8 @@ async function updateField(fieldname, value) {
contact.reload() contact.reload()
} }
const { getFormattedCurrency } = getMeta('CRM Deal')
const columns = computed(() => dealColumns) const columns = computed(() => dealColumns)
function getDealRowObject(deal) { function getDealRowObject(deal) {
@ -610,10 +609,7 @@ function getDealRowObject(deal) {
label: deal.organization, label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo, logo: getOrganization(deal.organization)?.organization_logo,
}, },
annual_revenue: formatNumberIntoCurrency( annual_revenue: getFormattedCurrency('annual_revenue', deal),
deal.annual_revenue,
deal.currency,
),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.iconColorClass,
@ -640,6 +636,7 @@ const dealColumns = [
{ {
label: __('Amount'), label: __('Amount'),
key: 'annual_revenue', key: 'annual_revenue',
align: 'right',
width: '9rem', width: '9rem',
}, },
{ {

View File

@ -70,10 +70,12 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import ContactModal from '@/components/Modals/ContactModal.vue' import ContactModal from '@/components/Modals/ContactModal.vue'
import ContactsListView from '@/components/ListViews/ContactsListView.vue' import ContactsListView from '@/components/ListViews/ContactsListView.vue'
import ViewControls from '@/components/ViewControls.vue' import ViewControls from '@/components/ViewControls.vue'
import { getMeta } from '@/stores/meta'
import { organizationsStore } from '@/stores/organizations.js' import { organizationsStore } from '@/stores/organizations.js'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo } from '@/utils'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } = getMeta('Contact')
const { getOrganization } = organizationsStore() const { getOrganization } = organizationsStore()
const showContactModal = ref(false) const showContactModal = ref(false)
@ -110,6 +112,18 @@ const rows = computed(() => {
_rows[row] = formatDate(contact[row], '', true, fieldType == 'Datetime') _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') { if (row == 'full_name') {
_rows[row] = { _rows[row] = {
label: contact.full_name, label: contact.full_name,

View File

@ -171,6 +171,7 @@
v-if="section.fields" v-if="section.fields"
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
doctype="CRM Deal"
v-model="deal.data" v-model="deal.data"
@update="updateField" @update="updateField"
/> />

View File

@ -281,22 +281,19 @@ import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue' import TaskModal from '@/components/Modals/TaskModal.vue'
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue' import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import ViewControls from '@/components/ViewControls.vue' import ViewControls from '@/components/ViewControls.vue'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations' import { organizationsStore } from '@/stores/organizations'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { callEnabled } from '@/composables/settings' import { callEnabled } from '@/composables/settings'
import { import { formatDate, timeAgo, website, formatTime } from '@/utils'
formatDate,
timeAgo,
website,
formatNumberIntoCurrency,
formatTime,
} from '@/utils'
import { Tooltip, Avatar, Dropdown } from 'frappe-ui' import { Tooltip, Avatar, Dropdown } from 'frappe-ui'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ref, reactive, computed, h } from 'vue' import { ref, reactive, computed, h } from 'vue'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Deal')
const { makeCall } = globalStore() const { makeCall } = globalStore()
const { getUser } = usersStore() const { getUser } = usersStore()
const { getOrganization } = organizationsStore() const { getOrganization } = organizationsStore()
@ -402,6 +399,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(deal[row], '', true, fieldType == 'Datetime') _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') { if (row == 'organization') {
_rows[row] = { _rows[row] = {
label: deal.organization, label: deal.organization,
@ -409,11 +418,6 @@ function parseRows(rows, columns = []) {
} }
} else if (row === 'website') { } else if (row === 'website') {
_rows[row] = website(deal.website) _rows[row] = website(deal.website)
} else if (row == 'annual_revenue') {
_rows[row] = formatNumberIntoCurrency(
deal.annual_revenue,
deal.currency,
)
} else if (row == 'status') { } else if (row == 'status') {
_rows[row] = { _rows[row] = {
label: deal.status, label: deal.status,

View File

@ -75,9 +75,13 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import ViewControls from '@/components/ViewControls.vue' import ViewControls from '@/components/ViewControls.vue'
import EmailTemplatesListView from '@/components/ListViews/EmailTemplatesListView.vue' import EmailTemplatesListView from '@/components/ListViews/EmailTemplatesListView.vue'
import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue' import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
import { getMeta } from '@/stores/meta'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo } from '@/utils'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('Email Template')
const emailTemplatesListView = ref(null) const emailTemplatesListView = ref(null)
// emailTemplates data is loaded in the ViewControls component // 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)) { if (['modified', 'creation'].includes(row)) {
_rows[row] = { _rows[row] = {
label: formatDate(emailTemplate[row]), label: formatDate(emailTemplate[row]),

View File

@ -303,6 +303,7 @@ import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue' import TaskModal from '@/components/Modals/TaskModal.vue'
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue' import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import ViewControls from '@/components/ViewControls.vue' import ViewControls from '@/components/ViewControls.vue'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
@ -312,6 +313,8 @@ import { Avatar, Tooltip, Dropdown } from 'frappe-ui'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ref, computed, reactive, h } from 'vue' import { ref, computed, reactive, h } from 'vue'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Lead')
const { makeCall } = globalStore() const { makeCall } = globalStore()
const { getUser } = usersStore() const { getUser } = usersStore()
const { getLeadStatus } = statusesStore() const { getLeadStatus } = statusesStore()
@ -416,6 +419,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(lead[row], '', true, fieldType == 'Datetime') _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') { if (row == 'lead_name') {
_rows[row] = { _rows[row] = {
label: lead.lead_name, label: lead.lead_name,

View File

@ -145,6 +145,7 @@
<SidePanelLayout <SidePanelLayout
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
doctype="Contact"
v-model="contact.data" v-model="contact.data"
@update="updateField" @update="updateField"
/> />
@ -186,13 +187,9 @@ import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue' import DealsListView from '@/components/ListViews/DealsListView.vue'
import AddressModal from '@/components/Modals/AddressModal.vue' import AddressModal from '@/components/Modals/AddressModal.vue'
import { import { formatDate, timeAgo, createToast } from '@/utils'
formatDate,
timeAgo,
formatNumberIntoCurrency,
createToast,
} from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global.js' import { globalStore } from '@/stores/global.js'
import { usersStore } from '@/stores/users.js' import { usersStore } from '@/stores/users.js'
import { organizationsStore } from '@/stores/organizations.js' import { organizationsStore } from '@/stores/organizations.js'
@ -581,6 +578,8 @@ async function updateField(fieldname, value) {
contact.reload() contact.reload()
} }
const { getFormattedCurrency } = getMeta('CRM Deal')
const columns = computed(() => dealColumns) const columns = computed(() => dealColumns)
function getDealRowObject(deal) { function getDealRowObject(deal) {
@ -590,10 +589,7 @@ function getDealRowObject(deal) {
label: deal.organization, label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo, logo: getOrganization(deal.organization)?.organization_logo,
}, },
annual_revenue: formatNumberIntoCurrency( annual_revenue: getFormattedCurrency('annual_revenue', deal),
deal.annual_revenue,
deal.currency,
),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.iconColorClass,
@ -620,6 +616,7 @@ const dealColumns = [
{ {
label: __('Amount'), label: __('Amount'),
key: 'annual_revenue', key: 'annual_revenue',
align: 'right',
width: '9rem', width: '9rem',
}, },
{ {

View File

@ -102,6 +102,7 @@
v-if="section.fields" v-if="section.fields"
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
doctype="CRM Deal"
v-model="deal.data" v-model="deal.data"
@update="updateField" @update="updateField"
/> />

View File

@ -125,9 +125,10 @@
> >
<Section :label="section.label" :opened="section.opened"> <Section :label="section.label" :opened="section.opened">
<SidePanelLayout <SidePanelLayout
v-model="organization.doc"
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
v-model="organization.doc" doctype="CRM Organization"
@update="updateField" @update="updateField"
/> />
</Section> </Section>
@ -176,16 +177,12 @@ import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue' import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue' import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { import { formatDate, timeAgo, createToast } from '@/utils'
formatDate,
timeAgo,
formatNumberIntoCurrency,
createToast,
} from '@/utils'
import { import {
Breadcrumbs, Breadcrumbs,
Avatar, Avatar,
@ -441,6 +438,8 @@ const rows = computed(() => {
}) })
}) })
const { getFormattedCurrency } = getMeta('CRM Deal')
const columns = computed(() => { const columns = computed(() => {
return tabIndex.value === 0 ? dealColumns : contactColumns return tabIndex.value === 0 ? dealColumns : contactColumns
}) })
@ -450,12 +449,9 @@ function getDealRowObject(deal) {
name: deal.name, name: deal.name,
organization: { organization: {
label: deal.organization, label: deal.organization,
logo: props.organization?.organization_logo, logo: organization.doc?.organization_logo,
}, },
annual_revenue: formatNumberIntoCurrency( annual_revenue: getFormattedCurrency('annual_revenue', deal),
deal.annual_revenue,
deal.currency,
),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.iconColorClass,
@ -485,7 +481,7 @@ function getContactRowObject(contact) {
mobile_no: contact.mobile_no, mobile_no: contact.mobile_no,
company_name: { company_name: {
label: contact.company_name, label: contact.company_name,
logo: props.organization?.organization_logo, logo: organization.doc?.organization_logo,
}, },
modified: { modified: {
label: formatDate(contact.modified), label: formatDate(contact.modified),
@ -503,6 +499,7 @@ const dealColumns = [
{ {
label: __('Amount'), label: __('Amount'),
key: 'annual_revenue', key: 'annual_revenue',
align: 'right',
width: '9rem', width: '9rem',
}, },
{ {

View File

@ -125,9 +125,10 @@
</template> </template>
<SidePanelLayout <SidePanelLayout
v-if="section.fields" v-if="section.fields"
v-model="organization.doc"
:fields="section.fields" :fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1" :isLastSection="i == fieldsLayout.data.length - 1"
v-model="organization.doc" doctype="CRM Organization"
@update="updateField" @update="updateField"
/> />
</Section> </Section>
@ -211,16 +212,12 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue' import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue' import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import { getMeta } from '@/stores/meta'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { import { formatDate, timeAgo, createToast } from '@/utils'
formatDate,
timeAgo,
formatNumberIntoCurrency,
createToast,
} from '@/utils'
import { import {
Tooltip, Tooltip,
Breadcrumbs, Breadcrumbs,
@ -476,6 +473,8 @@ const rows = computed(() => {
}) })
}) })
const { getFormattedCurrency } = getMeta('CRM Deal')
const columns = computed(() => { const columns = computed(() => {
return tabIndex.value === 0 ? dealColumns : contactColumns return tabIndex.value === 0 ? dealColumns : contactColumns
}) })
@ -485,12 +484,9 @@ function getDealRowObject(deal) {
name: deal.name, name: deal.name,
organization: { organization: {
label: deal.organization, label: deal.organization,
logo: props.organization?.organization_logo, logo: organization.doc?.organization_logo,
}, },
annual_revenue: formatNumberIntoCurrency( annual_revenue: getFormattedCurrency('annual_revenue', deal),
deal.annual_revenue,
deal.currency,
),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.iconColorClass,
@ -520,7 +516,7 @@ function getContactRowObject(contact) {
mobile_no: contact.mobile_no, mobile_no: contact.mobile_no,
company_name: { company_name: {
label: contact.company_name, label: contact.company_name,
logo: props.organization?.organization_logo, logo: organization.doc?.organization_logo,
}, },
modified: { modified: {
label: formatDate(contact.modified), label: formatDate(contact.modified),
@ -538,6 +534,7 @@ const dealColumns = [
{ {
label: __('Amount'), label: __('Amount'),
key: 'annual_revenue', key: 'annual_revenue',
align: 'right',
width: '9rem', width: '9rem',
}, },
{ {

View File

@ -69,9 +69,13 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import OrganizationModal from '@/components/Modals/OrganizationModal.vue' import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue' import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
import ViewControls from '@/components/ViewControls.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' import { ref, computed } from 'vue'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Organization')
const organizationsListView = ref(null) const organizationsListView = ref(null)
const showOrganizationModal = ref(false) 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') { if (row === 'organization_name') {
_rows[row] = { _rows[row] = {
label: organization.organization_name, label: organization.organization_name,
@ -117,11 +133,6 @@ const rows = computed(() => {
} }
} else if (row === 'website') { } else if (row === 'website') {
_rows[row] = website(organization.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)) { } else if (['modified', 'creation'].includes(row)) {
_rows[row] = { _rows[row] = {
label: formatDate(organization[row]), label: formatDate(organization[row]),

View File

@ -204,12 +204,15 @@ import ViewControls from '@/components/ViewControls.vue'
import TasksListView from '@/components/ListViews/TasksListView.vue' import TasksListView from '@/components/ListViews/TasksListView.vue'
import KanbanView from '@/components/Kanban/KanbanView.vue' import KanbanView from '@/components/Kanban/KanbanView.vue'
import TaskModal from '@/components/Modals/TaskModal.vue' import TaskModal from '@/components/Modals/TaskModal.vue'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo } from '@/utils'
import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui' import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Task')
const { getUser } = usersStore() const { getUser } = usersStore()
const router = useRouter() const router = useRouter()
@ -271,6 +274,18 @@ function parseRows(rows, columns = []) {
_rows[row] = formatDate(task[row], '', true, fieldType == 'Datetime') _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)) { if (['modified', 'creation'].includes(row)) {
_rows[row] = { _rows[row] = {
label: formatDate(task[row]), label: formatDate(task[row]),

View File

@ -18,7 +18,6 @@ const routes = [
path: '/leads/view/:viewType?', path: '/leads/view/:viewType?',
name: 'Leads', name: 'Leads',
component: () => import('@/pages/Leads.vue'), component: () => import('@/pages/Leads.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
path: '/leads/:leadId', path: '/leads/:leadId',
@ -31,7 +30,6 @@ const routes = [
path: '/deals/view/:viewType?', path: '/deals/view/:viewType?',
name: 'Deals', name: 'Deals',
component: () => import('@/pages/Deals.vue'), component: () => import('@/pages/Deals.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
path: '/deals/:dealId', path: '/deals/:dealId',
@ -56,7 +54,6 @@ const routes = [
path: '/contacts/view/:viewType?', path: '/contacts/view/:viewType?',
name: 'Contacts', name: 'Contacts',
component: () => import('@/pages/Contacts.vue'), component: () => import('@/pages/Contacts.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
path: '/contacts/:contactId', path: '/contacts/:contactId',
@ -69,7 +66,6 @@ const routes = [
path: '/organizations/view/:viewType?', path: '/organizations/view/:viewType?',
name: 'Organizations', name: 'Organizations',
component: () => import('@/pages/Organizations.vue'), component: () => import('@/pages/Organizations.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
path: '/organizations/:organizationId', path: '/organizations/:organizationId',
@ -82,14 +78,12 @@ const routes = [
path: '/call-logs/view/:viewType?', path: '/call-logs/view/:viewType?',
name: 'Call Logs', name: 'Call Logs',
component: () => import('@/pages/CallLogs.vue'), component: () => import('@/pages/CallLogs.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
alias: '/email-templates', alias: '/email-templates',
path: '/email-templates/view/:viewType?', path: '/email-templates/view/:viewType?',
name: 'Email Templates', name: 'Email Templates',
component: () => import('@/pages/EmailTemplates.vue'), component: () => import('@/pages/EmailTemplates.vue'),
meta: { scrollPos: { top: 0, left: 0 } },
}, },
{ {
path: '/email-templates/:emailTemplateId', path: '/email-templates/:emailTemplateId',
@ -108,29 +102,9 @@ const handleMobileView = (componentName) => {
return window.innerWidth < 768 ? `Mobile${componentName}` : 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({ let router = createRouter({
history: createWebHistory('/crm'), history: createWebHistory('/crm'),
routes, routes,
scrollBehavior,
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
@ -138,10 +112,6 @@ router.beforeEach(async (to, from, next) => {
isLoggedIn && (await userResource.promise) isLoggedIn && (await userResource.promise)
if (from.meta?.scrollPos) {
from.meta.scrollPos.top = document.querySelector('#list-rows')?.scrollTop
}
if (to.name === 'Home' && isLoggedIn) { if (to.name === 'Home' && isLoggedIn) {
next({ name: 'Leads' }) next({ name: 'Leads' })
} else if (!isLoggedIn) { } else if (!isLoggedIn) {

View File

@ -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,
}
}

View File

@ -1,7 +1,10 @@
import { secondsToDuration, formatDate, timeAgo } from '@/utils' import { secondsToDuration, formatDate, timeAgo } from '@/utils'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta('CRM Call Log')
const { getUser } = usersStore() const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore() const { getContact, getLeadContact } = contactsStore()
@ -58,6 +61,18 @@ export function getCallLogDetail(row, log, columns = []) {
return formatDate(log[row], '', true, fieldType == 'Datetime') 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] return log[row]
} }

View File

@ -126,17 +126,6 @@ export function secondsToDuration(seconds) {
return `${hours}h ${minutes}m ${_seconds}s` 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) { export function startCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1) return str.charAt(0).toUpperCase() + str.slice(1)
} }

View File

@ -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}`)
}
}