1
0
forked from test/crm

Merge pull request #18 from shariquerik/replace-with-frappeui-listview

This commit is contained in:
Shariq Ansari 2023-10-19 16:47:01 +05:30 committed by GitHub
commit 412565fd1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 268 additions and 370 deletions

@ -1 +1 @@
Subproject commit 167ff453cb525674f976d53b126900b80b9a80a2
Subproject commit 38eb500cb47d6cfc7124817e9c2acdcd560045f1

View File

@ -16,7 +16,7 @@
"@vueuse/integrations": "^10.3.0",
"autoprefixer": "^10.4.14",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.6",
"frappe-ui": "^0.1.12",
"pinia": "^2.0.33",
"postcss": "^8.4.5",
"socket.io-client": "^4.7.2",

View File

@ -1,61 +0,0 @@
<template>
<Tooltip
:text="tooltipText"
class="flex items-center space-x-2"
:class="align == 'text-right' ? 'justify-end' : ''"
>
<slot name="prefix"></slot>
<slot>
<div class="text-base truncate">
{{ label }}
</div>
</slot>
</Tooltip>
</template>
<script setup>
import {
dateFormat,
timeAgo,
dateTooltipFormat,
htmlToText,
formatNumberIntoCurrency,
} from '@/utils'
import { Tooltip } from 'frappe-ui'
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'text',
},
align: {
type: String,
default: 'left',
},
value: {
type: [String, Number, Object],
default: '',
},
})
const tooltipText = computed(() => {
if (props.type === 'html') return ''
if (props.type === 'pretty_date') {
return dateFormat(props.value, dateTooltipFormat)
}
return props.value?.toString()
})
const label = computed(() => {
if (props.type === 'pretty_date') {
return timeAgo(props.value)
}
if (props.type === 'html') {
return htmlToText(props.value?.toString())
}
if (props.type === 'currency') {
return formatNumberIntoCurrency(props.value)
}
return props.value?.toString()
})
</script>

View File

@ -1,205 +0,0 @@
<template>
<div id="content" class="flex flex-col w-full overflow-x-auto flex-1">
<div class="flex flex-col overflow-y-hidden w-max min-w-full">
<div
id="list-header"
class="flex space-x-4 items-center mx-5 mb-2 p-2 rounded bg-gray-100"
>
<Checkbox
class="duration-300 cursor-pointer"
:modelValue="allRowsSelected"
@click.stop="toggleAllRows"
/>
<div
v-for="column in columns"
:key="column"
class="text-base text-gray-600"
:class="[column.size, column.align]"
>
{{ column.label }}
</div>
</div>
<div id="list-rows" class="h-full overflow-y-auto">
<router-link
v-for="(row, i) in rows"
:key="row[rowKey]"
:to="$router.currentRoute.value.path + '/' + row[rowKey]"
class="flex flex-col mx-5 cursor-pointer transition-all duration-300 ease-in-out"
>
<div
class="flex space-x-4 items-center px-2 py-2.5 rounded"
:class="
selections.has(row[rowKey])
? 'bg-gray-100 hover:bg-gray-200'
: 'hover:bg-gray-50'
"
>
<Checkbox
:modelValue="selections.has(row[rowKey])"
@click.stop="toggleRow(row[rowKey])"
class="duration-300 cursor-pointer"
/>
<div
v-for="column in columns"
:key="column.key"
:class="[column.size, column.align]"
>
<ListRowItem
:value="getValue(row[column.key]).label"
:type="column.type"
:align="column.align"
>
<template #prefix>
<div v-if="column.type === 'indicator'">
<IndicatorIcon :class="getValue(row[column.key]).color" />
</div>
<div v-else-if="column.type === 'avatar'">
<Avatar
v-if="getValue(row[column.key]).label"
class="flex items-center"
:image="getValue(row[column.key]).image"
:label="getValue(row[column.key]).image_label"
size="sm"
/>
</div>
<div v-else-if="column.type === 'logo'">
<Avatar
v-if="getValue(row[column.key]).label"
class="flex items-center"
:image="getValue(row[column.key]).logo"
:label="getValue(row[column.key]).image_label"
size="sm"
/>
</div>
<div v-else-if="column.type === 'icon'">
<FeatherIcon
:name="getValue(row[column.key]).icon"
class="h-3 w-3"
/>
</div>
<div v-else-if="column.type === 'phone'">
<PhoneIcon class="h-4 w-4" />
</div>
</template>
<div v-if="column.type === 'badge'">
<Badge
:variant="'subtle'"
:theme="row[column.key].color"
size="md"
:label="row[column.key].label"
/>
</div>
</ListRowItem>
</div>
</div>
<div
v-if="i < rows.length - 1"
class="mx-2 h-px border-t border-gray-200"
/>
</router-link>
</div>
<transition
enter-active-class="duration-300 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="opacity-100"
leave-active-class="duration-300 ease-in"
leave-from-class="opacity-100"
leave-to-class="transform opacity-0"
>
<div
v-if="selections.size"
class="fixed inset-x-0 bottom-6 mx-auto w-max text-base"
>
<div
class="w-[596px] flex items-center space-x-3 rounded-lg bg-white px-4 py-2 shadow-2xl"
>
<div
class="flex flex-1 items-center space-x-3 border-r border-gray-300 text-gray-900"
>
<Checkbox
:modelValue="true"
:disabled="true"
class="text-gray-900"
/>
<div>{{ selectedText }}</div>
</div>
<div class="flex items-center space-x-1">
<Button
class="text-gray-700"
:disabled="allRowsSelected"
:class="allRowsSelected ? 'cursor-not-allowed' : ''"
variant="ghost"
@click="toggleAllRows(true)"
>
Select all
</Button>
<Button icon="x" variant="ghost" @click="toggleAllRows(false)" />
</div>
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup>
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import ListRowItem from '@/components/ListRowItem.vue'
import { Checkbox, Avatar, Badge, FeatherIcon } from 'frappe-ui'
import { reactive, computed } from 'vue'
const props = defineProps({
list: {
type: Object,
required: true,
},
columns: {
type: Array,
default: [],
},
rows: {
type: Array,
default: [],
},
rowKey: {
type: String,
required: true,
},
})
function getValue(value) {
if (value && typeof value === 'object') {
value.label = value.full_name || value.label
value.image = value.image || value.user_image || value.logo
value.image_label = value.image_label || value.label
return value
}
return { label: value }
}
let selections = reactive(new Set())
let selectedText = computed(() => {
let title =
selections.size === 1 ? props.list.singular_label : props.list.plural_label
return `${selections.size} ${title} selected`
})
const allRowsSelected = computed(() => {
if (!props.rows.length) return false
return selections.size === props.rows.length
})
function toggleRow(row) {
if (!selections.delete(row)) {
selections.add(row)
}
}
function toggleAllRows(select) {
if (!select || allRowsSelected.value) {
selections.clear()
return
}
props.rows.forEach((row) => selections.add(row[props.rowKey]))
}
</script>

View File

@ -17,30 +17,90 @@
<Button icon="more-horizontal" />
</div>
</div>
<ListView :list="list" :columns="columns" :rows="rows" row-key="name" />
<ListView
:columns="columns"
:rows="rows"
:options="{
getRowRoute: (row) => ({
name: 'Call Log',
params: { callLogId: row.name },
}),
}"
row-key="name"
>
<ListHeader class="mx-5" />
<ListRows>
<ListRow
class="mx-5"
v-for="row in rows"
:key="row.name"
v-slot="{ 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>
<div v-if="column.key === 'creation'" class="truncate text-base">
{{ item.timeAgo }}
</div>
<div v-else-if="column.key === 'status'" class="truncate text-base">
<Badge
:variant="'subtle'"
:theme="item.color"
size="md"
:label="item.label"
/>
</div>
</ListRowItem>
</ListRow>
</ListRows>
<ListSelectBanner />
</ListView>
</template>
<script setup>
import ListView from '@/components/ListView.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import SortIcon from '@/components/Icons/SortIcon.vue'
import FilterIcon from '@/components/Icons/FilterIcon.vue'
import { secondsToDuration } from '@/utils'
import {
secondsToDuration,
dateFormat,
dateTooltipFormat,
timeAgo,
} from '@/utils'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
import { Button, createListResource, Breadcrumbs } from 'frappe-ui'
import {
Avatar,
Badge,
createListResource,
Breadcrumbs,
ListView,
ListHeader,
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
FeatherIcon,
} from 'frappe-ui'
import { computed } from 'vue'
const { getUser } = usersStore()
const { getContact } = contactsStore()
const list = {
title: 'Call Logs',
plural_label: 'Call Logs',
singular_label: 'Call Log',
}
const breadcrumbs = [{ label: list.title, route: { name: 'Call Logs' } }]
const breadcrumbs = [{ label: 'Call Logs', route: { name: 'Call Logs' } }]
const callLogs = createListResource({
type: 'list',
@ -69,50 +129,42 @@ const columns = [
{
label: 'From',
key: 'caller',
type: 'avatar',
size: 'w-32',
width: '9rem',
},
{
label: 'To',
key: 'receiver',
type: 'avatar',
size: 'w-32',
width: '9rem',
},
{
label: 'Type',
key: 'type',
type: 'icon',
size: 'w-32',
width: '9rem',
},
{
label: 'Status',
key: 'status',
type: 'badge',
size: 'w-32',
width: '9rem',
},
{
label: 'Duration',
key: 'duration',
type: 'icon',
size: 'w-20',
width: '6rem',
},
{
label: 'From (number)',
key: 'from',
type: 'data',
size: 'w-32',
width: '9rem',
},
{
label: 'To (number)',
key: 'to',
type: 'data',
size: 'w-32',
width: '9rem',
},
{
label: 'Created on',
key: 'creation',
type: 'pretty_date',
size: 'w-28',
width: '8rem',
},
]
@ -159,7 +211,10 @@ const rows = computed(() => {
label: callLog.status,
color: callLog.status === 'Completed' ? 'green' : 'gray',
},
creation: callLog.creation,
creation: {
label: dateFormat(callLog.creation, dateTooltipFormat),
timeAgo: timeAgo(callLog.creation),
},
}
})
})

View File

@ -36,46 +36,36 @@
<Button icon="more-horizontal" />
</div>
</div>
<ListView :list="list" :columns="columns" :rows="rows" row-key="name" />
<ListView class="px-5" v-if="rows" :columns="columns" :rows="rows" row-key="name" />
</template>
<script setup>
import ListView from '@/components/ListView.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import SortIcon from '@/components/Icons/SortIcon.vue'
import FilterIcon from '@/components/Icons/FilterIcon.vue'
import { FeatherIcon, Button, Dropdown, Breadcrumbs } from 'frappe-ui'
import { FeatherIcon, Button, Dropdown, Breadcrumbs, ListView } from 'frappe-ui'
import { ref, computed } from 'vue'
import { contactsStore } from '@/stores/contacts.js'
const { contacts } = contactsStore()
const list = {
title: 'Contacts',
plural_label: 'Contacts',
singular_label: 'Contact',
}
const breadcrumbs = [{ label: list.title, route: { name: 'Contacts' } }]
const breadcrumbs = [{ label: 'Contacts', route: { name: 'Contacts' } }]
const columns = [
{
label: 'Full name',
key: 'full_name',
type: 'avatar',
size: 'w-44',
width: '12rem',
},
{
label: 'Email',
key: 'email',
type: 'email',
size: 'w-44',
width: '12rem',
},
{
label: 'Phone',
key: 'mobile_no',
type: 'phone',
size: 'w-44',
width: '12rem',
},
]

View File

@ -33,11 +33,62 @@
<Button icon="more-horizontal" />
</div>
</div>
<ListView :list="list" :columns="columns" :rows="rows" row-key="name" />
<ListView
:columns="columns"
:rows="rows"
:options="{
getRowRoute: (row) => ({ name: 'Deal', params: { dealId: row.name } }),
}"
row-key="name"
>
<ListHeader class="mx-5" />
<ListRows>
<ListRow
class="mx-5"
v-for="row in rows"
:key="row.name"
v-slot="{ column, item }"
:row="row"
>
<ListRowItem :item="item">
<template #prefix>
<div v-if="column.key === 'deal_status'">
<IndicatorIcon :class="item.color" />
</div>
<div v-else-if="column.key === 'organization_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 === '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>
<div v-if="column.key === 'modified'" class="truncate text-base">
{{ item.timeAgo }}
</div>
</ListRowItem>
</ListRow>
</ListRows>
<ListSelectBanner />
</ListView>
<Dialog
v-model="showNewDialog"
:options="{
size: '3xl',
width: '3xl',
title: 'New Deal',
actions: [{ label: 'Save', variant: 'solid' }],
}"
@ -54,17 +105,25 @@
</template>
<script setup>
import ListView from '@/components/ListView.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import NewDeal from '@/components/NewDeal.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import { usersStore } from '@/stores/users'
import { useOrderBy } from '@/composables/orderby'
import { useFilter } from '@/composables/filter'
import { useDebounceFn } from '@vueuse/core'
import { dealStatuses } from '@/utils'
import {
dealStatuses,
dateFormat,
dateTooltipFormat,
timeAgo,
formatNumberIntoCurrency,
} from '@/utils'
import {
Avatar,
FeatherIcon,
Dialog,
Button,
@ -72,17 +131,17 @@ import {
createListResource,
createResource,
Breadcrumbs,
ListView,
ListHeader,
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, computed, reactive, watch } from 'vue'
const list = {
title: 'Deals',
plural_label: 'Deals',
singular_label: 'Deal',
}
const breadcrumbs = [{ label: list.title, route: { name: 'Deals' } }]
const breadcrumbs = [{ label: 'Deals', route: { name: 'Deals' } }]
const { getUser } = usersStore()
const { get: getOrderBy } = useOrderBy()
@ -144,44 +203,37 @@ const columns = [
{
label: 'Organization',
key: 'organization_name',
type: 'logo',
size: 'w-40',
width: '11rem',
},
{
label: 'Amount',
key: 'annual_revenue',
type: 'currency',
size: 'w-32',
width: '9rem',
},
{
label: 'Status',
key: 'deal_status',
type: 'indicator',
size: 'w-36',
width: '10rem',
},
{
label: 'Email',
key: 'email',
type: 'email',
size: 'w-44',
width: '12rem',
},
{
label: 'Mobile no',
key: 'mobile_no',
type: 'phone',
size: 'w-40',
width: '11rem',
},
{
label: 'Lead owner',
key: 'lead_owner',
type: 'avatar',
size: 'w-36',
width: '10rem',
},
{
label: 'Last modified',
key: 'modified',
type: 'pretty_date',
size: 'w-28',
width: '8rem',
},
]
@ -193,15 +245,21 @@ const rows = computed(() => {
label: lead.organization_name,
logo: lead.organization_logo,
},
annual_revenue: lead.annual_revenue,
annual_revenue: formatNumberIntoCurrency(lead.annual_revenue),
deal_status: {
label: lead.deal_status,
color: dealStatuses[lead.deal_status]?.color,
},
email: lead.email,
mobile_no: lead.mobile_no,
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
modified: lead.modified,
lead_owner: {
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
...(lead.lead_owner && getUser(lead.lead_owner)),
},
modified: {
label: dateFormat(lead.modified, dateTooltipFormat),
timeAgo: timeAgo(lead.modified),
},
}
})
})

View File

@ -32,7 +32,67 @@
<Button icon="more-horizontal" />
</div>
</div>
<ListView :list="list" :columns="columns" :rows="rows" row-key="name" />
<ListView
:columns="columns"
:rows="rows"
:options="{
getRowRoute: (row) => ({ name: 'Lead', params: { leadId: row.name } }),
}"
row-key="name"
>
<ListHeader class="mx-5" />
<ListRows>
<ListRow
class="mx-5"
v-for="row in rows"
:key="row.name"
v-slot="{ column, item }"
:row="row"
>
<ListRowItem :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_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 === '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>
<div v-if="column.key === 'modified'" class="truncate text-base">
{{ item.timeAgo }}
</div>
</ListRowItem>
</ListRow>
</ListRows>
<ListSelectBanner />
</ListView>
<Dialog
v-model="showNewDialog"
:options="{
@ -53,17 +113,19 @@
</template>
<script setup>
import ListView from '@/components/ListView.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import NewLead from '@/components/NewLead.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import { usersStore } from '@/stores/users'
import { useOrderBy } from '@/composables/orderby'
import { useFilter } from '@/composables/filter'
import { useDebounceFn } from '@vueuse/core'
import { leadStatuses } from '@/utils'
import { leadStatuses, dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
import {
Avatar,
FeatherIcon,
Dialog,
Button,
@ -71,17 +133,17 @@ import {
createListResource,
createResource,
Breadcrumbs,
ListView,
ListHeader,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, computed, reactive, watch } from 'vue'
const list = {
title: 'Leads',
plural_label: 'Leads',
singular_label: 'Lead',
}
const breadcrumbs = [{ label: list.title, route: { name: 'Leads' } }]
const breadcrumbs = [{ label: 'Leads', route: { name: 'Leads' } }]
const { getUser } = usersStore()
const { get: getOrderBy } = useOrderBy()
@ -149,44 +211,37 @@ const columns = [
{
label: 'Name',
key: 'lead_name',
type: 'avatar',
size: 'w-44',
width: '12rem',
},
{
label: 'Organization',
key: 'organization_name',
type: 'logo',
size: 'w-36',
width: '10rem',
},
{
label: 'Status',
key: 'status',
type: 'indicator',
size: 'w-28',
width: '8rem',
},
{
label: 'Email',
key: 'email',
type: 'email',
size: 'w-44',
width: '12rem',
},
{
label: 'Mobile no',
key: 'mobile_no',
type: 'phone',
size: 'w-40',
width: '11rem',
},
{
label: 'Lead owner',
key: 'lead_owner',
type: 'avatar',
size: 'w-36',
width: '10rem',
},
{
label: 'Last modified',
key: 'modified',
type: 'pretty_date',
size: 'w-28',
width: '8rem',
},
]
@ -209,8 +264,14 @@ const rows = computed(() => {
},
email: lead.email,
mobile_no: lead.mobile_no,
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
modified: lead.modified,
lead_owner: {
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
...(lead.lead_owner && getUser(lead.lead_owner)),
},
modified: {
label: dateFormat(lead.modified, dateTooltipFormat),
timeAgo: timeAgo(lead.modified),
},
}
})
})

View File

@ -943,10 +943,10 @@ fraction.js@^4.3.6:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d"
integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==
frappe-ui@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.6.tgz#92e92fd1cab582dde09cf278b311f177364f2877"
integrity sha512-QmTt+hRF4/u3GSdCVo5hpUgQQX+Rm9hOvNMd3c1xNfCpZJ17nGODk6O89cnbbcfXWvIcTj6mnU7r/lSxa+qq9A==
frappe-ui@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.12.tgz#f112931897f75f307f18a3227ab641669cdfdddb"
integrity sha512-gb4aiNdcyiOYhJ1QZw1o+P26HSIM6T/3Dq4tfIq8uwvV2l3mk+xIW51g9lHLIUaYri44wojCrRCuCm8apO8xQw==
dependencies:
"@headlessui/vue" "^1.7.14"
"@popperjs/core" "^2.11.2"