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',
'type': 'Currency',
'key': 'annual_revenue',
'align': 'right',
'width': '9rem',
},
{

View File

@ -32,7 +32,12 @@
<span>{{ __('Loading...') }}</span>
</div>
<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>
<DataFieldsModal
v-if="showDataFieldsModal"

View File

@ -49,7 +49,9 @@
</div>
</template>
</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
value=""
:options="fields"
@ -213,11 +215,13 @@ const fields = computed(() => {
})
function addColumn(c) {
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type) ? 'right' : 'left'
let _column = {
label: c.label,
type: c.type,
key: c.value,
width: '10rem',
align,
}
columns.value.push(_column)
rows.value.push(c.value)

View File

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

View File

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

View File

@ -36,96 +36,93 @@
</Button>
</ListHeaderItem>
</ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows">
<ListRow
v-for="row in rows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<ListRowItem :item="item">
<template #prefix>
<div v-if="column.key === 'full_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.image"
:label="item.image_label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'company_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'mobile_no'">
<PhoneIcon class="h-4 w-4" />
</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],
})
<ListRows
class="mx-3 sm:mx-5"
:rows="rows"
v-slot="{ idx, column, item }"
doctype="Contact"
>
<ListRowItem :item="item" :align="column.align">
<template #prefix>
<div v-if="column.key === 'full_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.image"
:label="item.image_label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'company_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'mobile_no'">
<PhoneIcon class="h-4 w-4" />
</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 === '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">
<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) })
"
>
<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>
<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>
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
@ -160,13 +157,12 @@
import HeartIcon from '@/components/Icons/HeartIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import {
Avatar,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
ListFooter,

View File

@ -36,7 +36,11 @@
</Button>
</ListHeaderItem>
</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">
<MultipleAvatar
:avatars="item"
@ -53,7 +57,7 @@
"
/>
</div>
<ListRowItem v-else :item="item">
<ListRowItem v-else :item="item" :align="column.align">
<template #prefix>
<div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" />

View File

@ -10,7 +10,10 @@
}"
row-key="name"
>
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
<ListHeader
class="sm:mx-5 mx-3"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem
v-for="column in columns"
:key="column.key"
@ -28,21 +31,41 @@
</Button>
</ListHeaderItem>
</ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows">
<ListRow
v-for="row in rows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<ListRowItem :item="item">
<!-- <template #prefix>
<ListRows
class="mx-3 sm:mx-5"
:rows="rows"
v-slot="{ idx, column, item }"
doctype="Email Template"
>
<ListRowItem :item="item" :align="column.align">
<!-- <template #prefix>
</template> -->
<template #default="{ label }">
<div
v-if="['modified', 'creation'].includes(column.key)"
class="truncate text-base"
<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.key === 'status'" class="truncate text-base">
<Badge
:variant="'subtle'"
:theme="item.color"
size="md"
:label="item.label"
@click="
(event) =>
emit('applyFilter', {
@ -53,69 +76,46 @@
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="
(event) =>
emit('applyFilter', {
event,
idx,
column,
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],
})
/>
</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) })
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRow>
<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>
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
@ -148,12 +148,11 @@
<script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import {
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
ListFooter,
@ -219,7 +218,7 @@ const listBulkActionsRef = ref(null)
defineExpose({
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
() => listBulkActionsRef.value?.customListActions,
),
})
</script>

View File

@ -36,7 +36,11 @@
</Button>
</ListHeaderItem>
</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">
<MultipleAvatar
:avatars="item"
@ -53,7 +57,7 @@
"
/>
</div>
<ListRowItem v-else :item="item">
<ListRowItem v-else :item="item" :align="column.align">
<template #prefix>
<div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" />

View File

@ -15,7 +15,7 @@
</div>
</div>
</ListGroupHeader>
<ListGroupRows :group="group" id="list-rows">
<ListGroupRows :group="group">
<ListRow
v-for="row in group.rows"
:key="row.name"
@ -27,7 +27,12 @@
</ListGroupRows>
</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
v-for="row in reactivieRows"
:key="row.name"
@ -40,27 +45,53 @@
</template>
<script setup>
import { useStorage } from '@vueuse/core'
import { ListRows, ListRow, ListGroupHeader, ListGroupRows } from 'frappe-ui'
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onBeforeUnmount, onMounted } from 'vue'
const props = defineProps({
rows: {
type: Array,
required: true,
},
doctype: {
type: String,
default: 'CRM Lead',
},
})
const reactivieRows = ref(props.rows)
watch(
() => props.rows,
(val) => (reactivieRows.value = val)
(val) => (reactivieRows.value = val),
)
let showGroupedRows = computed(() => {
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>

View File

@ -14,7 +14,10 @@
}"
row-key="name"
>
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
<ListHeader
class="sm:mx-5 mx-3"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem
v-for="column in columns"
:key="column.key"
@ -32,84 +35,81 @@
</Button>
</ListHeaderItem>
</ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows">
<ListRow
v-for="row in rows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<ListRowItem :item="item">
<template #prefix>
<div v-if="column.key === 'organization_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
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],
})
<ListRows
class="mx-3 sm:mx-5"
:rows="rows"
v-slot="{ idx, column, item }"
doctype="CRM Organization"
>
<ListRowItem :item="item" :align="column.align">
<template #prefix>
<div v-if="column.key === 'organization_name'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
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 === '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">
<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) })
"
>
<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>
<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>
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
@ -142,13 +142,12 @@
<script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import {
Avatar,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
ListFooter,
@ -216,7 +215,7 @@ const listBulkActionsRef = ref(null)
defineExpose({
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
() => listBulkActionsRef.value?.customListActions,
),
})
</script>

View File

@ -31,107 +31,102 @@
</Button>
</ListHeaderItem>
</ListHeader>
<ListRows class="mx-3 sm:mx-5" id="list-rows">
<ListRow
v-for="row in rows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<div v-if="column.key === 'due_date'">
<Tooltip
:text="item && formatDate(item, 'ddd, MMM D, YYYY | hh:mm a')"
>
<div class="flex items-center gap-2 truncate text-base">
<div><CalendarIcon /></div>
<div v-if="item" class="truncate">
{{ formatDate(item, 'D MMM, hh:mm a') }}
</div>
<ListRows
class="mx-3 sm:mx-5"
:rows="rows"
v-slot="{ idx, column, item }"
doctype="CRM Task"
>
<div v-if="column.key === 'due_date'">
<Tooltip :text="item && formatDate(item, 'ddd, MMM D, YYYY | hh:mm a')">
<div class="flex items-center gap-2 truncate text-base">
<div><CalendarIcon /></div>
<div v-if="item" class="truncate">
{{ formatDate(item, 'D MMM, hh:mm a') }}
</div>
</Tooltip>
</div>
<ListRowItem v-else :item="item">
<template #prefix>
<div v-if="column.key === 'status'">
<TaskStatusIcon :status="item" />
</div>
<div v-else-if="column.key === 'priority'">
<TaskPriorityIcon :priority="item" />
</div>
<div v-else-if="column.key === 'assigned_to'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
: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>
</Tooltip>
</div>
<ListRowItem v-else :item="item" :align="column.align">
<template #prefix>
<div v-if="column.key === 'status'">
<TaskStatusIcon :status="item" />
</div>
<div v-else-if="column.key === 'priority'">
<TaskPriorityIcon :priority="item" />
</div>
<div v-else-if="column.key === 'assigned_to'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
:label="item.full_name"
size="sm"
/>
<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],
})
</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'">
<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) })
"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRow>
<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>
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
@ -167,14 +162,13 @@ import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { formatDate } from '@/utils'
import {
Avatar,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
ListFooter,

View File

@ -23,7 +23,7 @@
</div>
</div>
<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" />
</div>
</div>
@ -54,7 +54,7 @@ import FieldLayout from '@/components/FieldLayout.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import { usersStore } from '@/stores/users'
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'
const props = defineProps({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,12 @@
/>
</h2>
<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" />
</div>
<div v-else class="flex flex-1 items-center justify-center">

View File

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

View File

@ -142,6 +142,7 @@
v-if="section.fields"
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
doctype="Contact"
v-model="contact.data"
@update="updateField"
/>
@ -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',
},
{

View File

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

View File

@ -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"
/>

View File

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

View File

@ -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]),

View File

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

View File

@ -145,6 +145,7 @@
<SidePanelLayout
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
doctype="Contact"
v-model="contact.data"
@update="updateField"
/>
@ -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',
},
{

View File

@ -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"
/>

View File

@ -125,9 +125,10 @@
>
<Section :label="section.label" :opened="section.opened">
<SidePanelLayout
v-model="organization.doc"
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
v-model="organization.doc"
doctype="CRM Organization"
@update="updateField"
/>
</Section>
@ -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',
},
{

View File

@ -125,9 +125,10 @@
</template>
<SidePanelLayout
v-if="section.fields"
v-model="organization.doc"
:fields="section.fields"
:isLastSection="i == fieldsLayout.data.length - 1"
v-model="organization.doc"
doctype="CRM Organization"
@update="updateField"
/>
</Section>
@ -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',
},
{

View File

@ -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]),

View File

@ -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]),

View File

@ -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) {

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 { 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]
}

View File

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

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