fix: implemented list group view in lead & deal

This commit is contained in:
Shariq Ansari 2024-05-31 18:12:26 +05:30
parent c86e78267d
commit ed47d87315
6 changed files with 359 additions and 256 deletions

@ -1 +1 @@
Subproject commit 1394a12b6de105649c8ca5beeead62a38ef1b18e Subproject commit 90c75affe85b2a1abfce68d61c0c5043f093a8a0

View File

@ -29,18 +29,74 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows id="list-rows"> <ListRows :rows="rows" v-slot="{ idx, column, item }">
<ListRow <div v-if="column.key === '_assign'" class="flex items-center">
class="mx-5" <MultipleAvatar
v-for="row in rows" :avatars="item"
:key="row.name" size="sm"
v-slot="{ idx, column, item }" @click="
:row="row" (event) =>
> emit('applyFilter', {
<div v-if="column.key === '_assign'" class="flex items-center"> event,
<MultipleAvatar idx,
:avatars="item" column,
size="sm" item,
firstColumn: columns[0],
})
"
/>
</div>
<ListRowItem v-else :item="item">
<template #prefix>
<div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" />
</div>
<div v-else-if="column.key === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'deal_owner'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
:label="item.full_name"
size="sm"
/>
</div>
<div v-else-if="column.key === 'mobile_no'">
<PhoneIcon class="h-4 w-4" />
</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>
</template>
<template #default="{ label }">
<div
v-if="
[
'modified',
'creation',
'first_response_time',
'first_responded_on',
'response_by',
].includes(column.key)
"
class="truncate text-base"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -51,60 +107,21 @@
firstColumn: columns[0], firstColumn: columns[0],
}) })
" "
/> >
</div> <Tooltip :text="item.label">
<ListRowItem v-else :item="item"> <div>{{ item.timeAgo }}</div>
<template #prefix> </Tooltip>
<div v-if="column.key === 'status'"> </div>
<IndicatorIcon :class="item.color" /> <div
</div> v-else-if="column.key === 'sla_status'"
<div v-else-if="column.key === 'organization'"> class="truncate text-base"
<Avatar >
v-if="item.label" <Badge
class="flex items-center" v-if="item.value"
:image="item.logo" :variant="'subtle'"
:label="item.label" :theme="item.color"
size="sm" size="md"
/> :label="item.value"
</div>
<div v-else-if="column.key === 'deal_owner'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
:label="item.full_name"
size="sm"
/>
</div>
<div v-else-if="column.key === 'mobile_no'">
<PhoneIcon class="h-4 w-4" />
</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>
</template>
<template #default="{ label }">
<div
v-if="
[
'modified',
'creation',
'first_response_time',
'first_responded_on',
'response_by',
].includes(column.key)
"
class="truncate text-base"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -115,60 +132,34 @@
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 :modelValue="item"
v-else-if="column.key === 'sla_status'" :disabled="true"
class="truncate text-base" class="text-gray-900"
> />
<Badge </div>
v-if="item.value" <div
:variant="'subtle'" v-else
:theme="item.color" class="truncate text-base"
size="md" @click="
:label="item.value" (event) =>
@click=" emit('applyFilter', {
(event) => event,
emit('applyFilter', { idx,
event, column,
idx, item,
column, firstColumn: columns[0],
item, })
firstColumn: columns[0], "
}) >
" {{ label }}
/> </div>
</div> </template>
<div v-else-if="column.type === 'Check'"> </ListRowItem>
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-gray-900"
/>
</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 }">
@ -199,13 +190,12 @@ import MultipleAvatar from '@/components/MultipleAvatar.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.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,
ListRowItem, ListRowItem,
ListSelectBanner, ListSelectBanner,
ListFooter, ListFooter,

View File

@ -29,18 +29,71 @@
</Button> </Button>
</ListHeaderItem> </ListHeaderItem>
</ListHeader> </ListHeader>
<ListRows id="list-rows"> <ListRows :rows="rows" v-slot="{ idx, column, item }">
<ListRow <div v-if="column.key === '_assign'" class="flex items-center">
class="mx-5" <MultipleAvatar
v-for="row in rows" :avatars="item"
:key="row.name" size="sm"
v-slot="{ idx, column, item }" @click="
:row="row" (event) =>
> emit('applyFilter', {
<div v-if="column.key === '_assign'" class="flex items-center"> event,
<MultipleAvatar idx,
:avatars="item" column,
size="sm" item,
firstColumn: columns[0],
})
"
/>
</div>
<ListRowItem v-else :item="item">
<template #prefix>
<div v-if="column.key === 'status'">
<IndicatorIcon :class="item.color" />
</div>
<div v-else-if="column.key === 'lead_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 === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'lead_owner'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
:label="item.full_name"
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',
'first_response_time',
'first_responded_on',
'response_by',
].includes(column.key)
"
class="truncate text-base"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -51,56 +104,37 @@
firstColumn: columns[0], firstColumn: columns[0],
}) })
" "
/> >
</div> <Tooltip :text="item.label">
<ListRowItem v-else :item="item"> <div>{{ item.timeAgo }}</div>
<template #prefix> </Tooltip>
<div v-if="column.key === 'status'"> </div>
<IndicatorIcon :class="item.color" /> <div v-else-if="column.key === '_liked_by'">
</div> <Button
<div v-else-if="column.key === 'lead_name'"> v-if="column.key == '_liked_by'"
<Avatar variant="ghosted"
v-if="item.label" :class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
class="flex items-center" @click.stop.prevent="
:image="item.image" () =>
:label="item.image_label" emit('likeDoc', {
size="sm" name: row.name,
/> liked: isLiked(item),
</div> })
<div v-else-if="column.key === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
<div v-else-if="column.key === 'lead_owner'">
<Avatar
v-if="item.full_name"
class="flex items-center"
:image="item.user_image"
:label="item.full_name"
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',
'first_response_time',
'first_responded_on',
'response_by',
].includes(column.key)
" "
class="truncate text-base" >
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else-if="column.key === 'sla_status'"
class="truncate text-base"
>
<Badge
v-if="item.value"
:variant="'subtle'"
:theme="item.color"
size="md"
:label="item.value"
@click=" @click="
(event) => (event) =>
emit('applyFilter', { emit('applyFilter', {
@ -111,73 +145,34 @@
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 === '_liked_by'"> :modelValue="item"
<Button :disabled="true"
v-if="column.key == '_liked_by'" class="text-gray-900"
variant="ghosted" />
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'" </div>
@click.stop.prevent=" <div
() => v-else
emit('likeDoc', { name: row.name, liked: isLiked(item) }) class="truncate text-base"
" @click="
> (event) =>
<HeartIcon class="h-4 w-4" /> emit('applyFilter', {
</Button> event,
</div> idx,
<div column,
v-else-if="column.key === 'sla_status'" item,
class="truncate text-base" firstColumn: columns[0],
> })
<Badge "
v-if="item.value" >
:variant="'subtle'" {{ label }}
:theme="item.color" </div>
size="md" </template>
:label="item.value" </ListRowItem>
@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-gray-900"
/>
</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 }">
@ -208,13 +203,12 @@ import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.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

@ -0,0 +1,64 @@
<template>
<div class="mx-5 mt-2 h-full overflow-y-auto" v-if="showGroupedRows">
<div v-for="group in reactivieRows" :key="group.group">
<ListGroupHeader :group="group">
<div
class="my-2 flex items-center gap-2 text-base font-medium text-gray-800"
>
<div>{{ __('Status') }} -</div>
<div class="flex items-center gap-1">
<IndicatorIcon :class="group.color" />
<div>{{ group.group }}</div>
</div>
</div>
</ListGroupHeader>
<ListGroupRows :group="group" id="list-rows">
<ListRow
v-for="row in group.rows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<slot v-bind="{ idx, column, item }" />
</ListRow>
</ListGroupRows>
</div>
</div>
<ListRows class="mx-5" v-else id="list-rows">
<ListRow
v-for="row in reactivieRows"
:key="row.name"
v-slot="{ idx, column, item }"
:row="row"
>
<slot v-bind="{ idx, column, item }" />
</ListRow>
</ListRows>
</template>
<script setup>
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { ListRows, ListRow, ListGroupHeader, ListGroupRows } from 'frappe-ui'
import { ref, computed, watch } from 'vue'
const props = defineProps({
rows: {
type: Array,
required: true,
},
})
const reactivieRows = ref(props.rows)
watch(
() => props.rows,
(val) => (reactivieRows.value = val)
)
let showGroupedRows = computed(() => {
return props.rows.every(
(row) => row.group && row.rows && Array.isArray(row.rows)
)
})
</script>

View File

@ -77,6 +77,7 @@ import {
formatTime, formatTime,
} from '@/utils' } from '@/utils'
import { Breadcrumbs } from 'frappe-ui' import { Breadcrumbs } from 'frappe-ui'
import { useRoute } from 'vue-router'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }] const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }]
@ -85,6 +86,8 @@ const { getUser } = usersStore()
const { getOrganization } = organizationsStore() const { getOrganization } = organizationsStore()
const { getDealStatus } = statusesStore() const { getDealStatus } = statusesStore()
const route = useRoute()
const dealsListView = ref(null) const dealsListView = ref(null)
const showDealModal = ref(false) const showDealModal = ref(false)
@ -98,7 +101,7 @@ const viewControls = ref(null)
// Rows // Rows
const rows = computed(() => { const rows = computed(() => {
if (!deals.value?.data?.data) return [] if (!deals.value?.data?.data) return []
return deals.value.data.data.map((deal) => { let listRows = deals.value.data.data.map((deal) => {
let _rows = {} let _rows = {}
deals.value.data.rows.forEach((row) => { deals.value.data.rows.forEach((row) => {
_rows[row] = deal[row] _rows[row] = deal[row]
@ -174,5 +177,31 @@ const rows = computed(() => {
}) })
return _rows return _rows
}) })
if (route.params.viewType === 'group_by') {
return getGroupedByRows(listRows)
}
return listRows
}) })
function getGroupedByRows(listRows) {
let groupedRows = []
listRows.forEach((row) => {
if (!groupedRows.some((group) => group.group === row.status.label)) {
groupedRows.push({
group: row.status.label,
color: getDealStatus(row.status.label)?.iconColorClass,
collapsed: false,
rows: [],
})
}
groupedRows.filter((group) => {
if (group.group === row.status.label) {
group.rows.push(row)
}
})
})
return groupedRows || listRows
}
</script> </script>

View File

@ -78,7 +78,7 @@ import {
createToast, createToast,
} from '@/utils' } from '@/utils'
import { createResource, Breadcrumbs } from 'frappe-ui' import { createResource, Breadcrumbs } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }] const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }]
@ -88,6 +88,7 @@ const { getOrganization } = organizationsStore()
const { getLeadStatus } = statusesStore() const { getLeadStatus } = statusesStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const leadsListView = ref(null) const leadsListView = ref(null)
const showLeadModal = ref(false) const showLeadModal = ref(false)
@ -102,7 +103,7 @@ const viewControls = ref(null)
// Rows // Rows
const rows = computed(() => { const rows = computed(() => {
if (!leads.value?.data?.data) return [] if (!leads.value?.data?.data) return []
return leads.value?.data.data.map((lead) => { let listRows = leads.value?.data.data.map((lead) => {
let _rows = {} let _rows = {}
leads.value?.data.rows.forEach((row) => { leads.value?.data.rows.forEach((row) => {
_rows[row] = lead[row] _rows[row] = lead[row]
@ -182,8 +183,33 @@ const rows = computed(() => {
}) })
return _rows return _rows
}) })
if (route.params.viewType === 'group_by') {
return getGroupedByRows(listRows)
}
return listRows
}) })
function getGroupedByRows(listRows) {
let groupedRows = []
listRows.forEach((row) => {
if (!groupedRows.some((group) => group.group === row.status.label)) {
groupedRows.push({
group: row.status.label,
color: getLeadStatus(row.status.label)?.iconColorClass,
collapsed: false,
rows: [],
})
}
groupedRows.filter((group) => {
if (group.group === row.status.label) {
group.rows.push(row)
}
})
})
return groupedRows || listRows
}
let newLead = reactive({ let newLead = reactive({
salutation: '', salutation: '',
first_name: '', first_name: '',