refactor: changed split view or organization to listview

This commit is contained in:
Shariq Ansari 2023-11-16 20:14:06 +05:30
parent 1caaa89278
commit baf660be28
4 changed files with 332 additions and 147 deletions

View File

@ -0,0 +1,71 @@
<template>
<ListView
:columns="columns"
:rows="rows"
:options="{
getRowRoute: (row) => ({
name: 'Organization',
params: { organizationId: row.name },
}),
selectable: options.selectable,
}"
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 === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"
:image="item.logo"
:label="item.label"
size="sm"
/>
</div>
</template>
<div v-if="column.key === 'modified'" class="truncate text-base">
{{ item.timeAgo }}
</div>
</ListRowItem>
</ListRow>
</ListRows>
<ListSelectBanner />
</ListView>
</template>
<script setup>
import {
Avatar,
ListView,
ListHeader,
ListRows,
ListRow,
ListSelectBanner,
ListRowItem,
} from 'frappe-ui'
const props = defineProps({
rows: {
type: Array,
required: true,
},
columns: {
type: Array,
required: true,
},
options: {
type: Object,
default: () => ({
selectable: true,
}),
},
})
</script>

View File

@ -1,11 +1,16 @@
<template>
<div class="flex flex-1 flex-col overflow-hidden">
<div class="flex gap-6 p-5">
<FileUploader
@success="changeOrganizationImage"
:validateFile="validateFile"
>
<template #default="{ openFileSelector, error }">
<LayoutHeader v-if="organization">
<template #left-header>
<Breadcrumbs :items="breadcrumbs" />
</template>
</LayoutHeader>
<div v-if="organization" class="flex flex-1 flex-col overflow-hidden">
<FileUploader
@success="changeOrganizationImage"
:validateFile="validateFile"
>
<template #default="{ openFileSelector, error }">
<div class="flex items-center justify-start gap-6 p-5">
<div class="group relative h-24 w-24">
<Avatar
size="3xl"
@ -47,63 +52,90 @@
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
</div>
</component>
<ErrorMessage class="mt-2" :message="error" />
</div>
</template>
</FileUploader>
<div class="flex flex-col justify-center gap-2">
<div class="text-3xl font-semibold text-gray-900">
{{ organization.name }}
</div>
<div class="flex items-center gap-2 text-base text-gray-700">
<div v-if="organization.website" class="flex items-center gap-1.5">
<WebsiteIcon class="h-4 w-4" />
<span class="">{{ website(organization.website) }}</span>
</div>
<span
v-if="organization.industry && organization.website"
class="text-3xl leading-[0] text-gray-600"
>
&middot;
</span>
<div v-if="organization.industry" class="flex items-center gap-1.5">
<FeatherIcon name="briefcase" class="h-4 w-4" />
<span class="">{{ organization.industry }}</span>
</div>
<span
v-if="
(organization.website || organization.industry) &&
organization.annual_revenue
"
class="text-3xl leading-[0] text-gray-600"
>
&middot;
</span>
<div
v-if="organization.annual_revenue"
class="flex items-center gap-1.5"
>
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
<span class="">{{ organization.annual_revenue }}</span>
</div>
</div>
<div class="mt-1 flex gap-2">
<Button label="Edit" size="sm" @click="showOrganizationModal = true">
<template #prefix>
<EditIcon class="h-4 w-4" />
</template>
</Button>
<Button
label="Delete"
theme="red"
size="sm"
@click="deleteOrganization"
>
<template #prefix>
<FeatherIcon name="trash-2" class="h-4 w-4" />
</template>
</Button>
<!-- <Button label="Add lead" size="sm">
<div class="flex flex-col justify-center gap-0.5">
<div class="text-3xl font-semibold text-gray-900">
{{ organization.name }}
</div>
<div class="flex items-center gap-2 text-base text-gray-700">
<div
v-if="organization.website"
class="flex items-center gap-1.5"
>
<WebsiteIcon class="h-4 w-4" />
<span class="">{{ website(organization.website) }}</span>
</div>
<span
v-if="organization.industry && organization.website"
class="text-3xl leading-[0] text-gray-600"
>
&middot;
</span>
<div
v-if="organization.industry"
class="flex items-center gap-1.5"
>
<FeatherIcon name="briefcase" class="h-4 w-4" />
<span class="">{{ organization.industry }}</span>
</div>
<span
v-if="
(organization.website || organization.industry) &&
organization.annual_revenue
"
class="text-3xl leading-[0] text-gray-600"
>
&middot;
</span>
<div
v-if="organization.annual_revenue"
class="flex items-center gap-1.5"
>
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
<span class="">{{ organization.annual_revenue }}</span>
</div>
<span
v-if="
organization.website ||
organization.industry ||
organization.annual_revenue
"
class="text-3xl leading-[0] text-gray-600"
>
&middot;
</span>
<Button
v-if="
organization.website ||
organization.industry ||
organization.annual_revenue
"
variant="ghost"
label="More"
class="-ml-1 cursor-pointer hover:text-gray-900"
/>
</div>
<div class="mt-2 flex gap-1.5">
<Button
label="Edit"
size="sm"
@click="showOrganizationModal = true"
>
<template #prefix>
<EditIcon class="h-4 w-4" />
</template>
</Button>
<Button
label="Delete"
theme="red"
size="sm"
@click="deleteOrganization"
>
<template #prefix>
<FeatherIcon name="trash-2" class="h-4 w-4" />
</template>
</Button>
<!-- <Button label="Add lead" size="sm">
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
@ -113,9 +145,12 @@
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
</Button> -->
</div>
<ErrorMessage class="mt-2" :message="error" />
</div>
</div>
</div>
</div>
</template>
</FileUploader>
<Tabs v-model="tabIndex" :tabs="tabs">
<template #tab="{ tab, selected }">
<button
@ -180,6 +215,7 @@
<script setup>
import {
Breadcrumbs,
FeatherIcon,
Avatar,
FileUploader,
@ -190,14 +226,13 @@ import {
call,
createListResource,
} from 'frappe-ui'
import LayoutHeader from '@/components/LayoutHeader.vue'
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue'
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
@ -212,18 +247,32 @@ import {
formatNumberIntoCurrency,
} from '@/utils'
import { usersStore } from '@/stores/users'
import { h, computed, ref, watch, onMounted } from 'vue'
import { h, computed, ref } from 'vue'
const props = defineProps({
organization: {
type: Object,
organizationId: {
type: String,
required: true,
},
})
const { organizations } = organizationsStore()
const { organizations, getOrganization } = organizationsStore()
const showOrganizationModal = ref(false)
const organization = computed(() => getOrganization(props.organizationId))
const breadcrumbs = computed(() => {
let items = [{ label: 'Organizations', route: { name: 'Organizations' } }]
items.push({
label: props.organizationId,
route: {
name: 'Organization',
params: { organizationId: props.organizationId },
},
})
return items
})
function validateFile(file) {
let extn = file.name.split('.').pop().toLowerCase()
if (!['png', 'jpg', 'jpeg'].includes(extn)) {
@ -234,7 +283,7 @@ function validateFile(file) {
async function changeOrganizationImage(file) {
await call('frappe.client.set_value', {
doctype: 'CRM Organization',
name: props.organization.name,
name: props.organizationId,
fieldname: 'organization_logo',
value: file?.file_url || '',
})
@ -253,7 +302,7 @@ async function deleteOrganization() {
async onClick({ close }) {
await call('frappe.client.delete', {
doctype: 'CRM Organization',
name: props.organization.name,
name: props.organizationId,
})
organizations.reload()
close()
@ -291,7 +340,7 @@ const { getUser } = usersStore()
const leads = createListResource({
type: 'list',
doctype: 'CRM Lead',
cache: ['leads', props.organization.name],
cache: ['leads', props.organizationId],
fields: [
'name',
'first_name',
@ -305,7 +354,7 @@ const leads = createListResource({
'modified',
],
filters: {
organization: props.organization.name,
organization: props.organizationId,
converted: 0,
},
orderBy: 'modified desc',
@ -316,7 +365,7 @@ const leads = createListResource({
const deals = createListResource({
type: 'list',
doctype: 'CRM Deal',
cache: ['deals', props.organization.name],
cache: ['deals', props.organizationId],
fields: [
'name',
'organization',
@ -328,7 +377,7 @@ const deals = createListResource({
'modified',
],
filters: {
organization: props.organization.name,
organization: props.organizationId,
},
orderBy: 'modified desc',
pageLength: 20,
@ -338,7 +387,7 @@ const deals = createListResource({
const contacts = createListResource({
type: 'list',
doctype: 'Contact',
cache: ['contacts', props.organization.name],
cache: ['contacts', props.organizationId],
fields: [
'name',
'full_name',
@ -349,7 +398,7 @@ const contacts = createListResource({
'modified',
],
filters: {
company_name: props.organization.name,
company_name: props.organizationId,
},
orderBy: 'modified desc',
pageLength: 20,
@ -566,11 +615,4 @@ function reload(val) {
deals.reload()
contacts.reload()
}
watch(
() => props.organization.name,
(val) => val && reload(val)
)
onMounted(() => reload(props.organization.name))
</script>

View File

@ -10,56 +10,34 @@
@click="showOrganizationModal = true"
>
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
Create organization
</Button>
</template>
</LayoutHeader>
<div class="flex h-full overflow-hidden">
<div class="flex shrink-0 flex-col overflow-y-auto border-r">
<router-link
:to="{
name: 'Organization',
params: { organizationId: organization.name },
}"
v-for="(organization, i) in organizations.data"
:key="i"
:class="[
currentOrganization?.name === organization.name
? 'bg-gray-50 hover:bg-gray-100'
: 'hover:bg-gray-50',
]"
>
<div class="flex w-[352px] items-center gap-3 border-b px-5 py-4">
<Avatar
:image="organization.organization_logo"
:label="organization.name"
size="xl"
/>
<div class="flex flex-col items-start gap-1">
<span class="text-base font-medium text-gray-900">
{{ organization.name }}
</span>
<span class="text-sm text-gray-700">{{
website(organization.website)
}}</span>
</div>
</div>
</router-link>
<div class="flex items-center justify-between px-5 pb-4 pt-3">
<div class="flex items-center gap-2">
<Dropdown :options="viewsDropdownOptions">
<template #default="{ open }">
<Button :label="currentView.label">
<template #prefix>
<FeatherIcon :name="currentView.icon" class="h-4" />
</template>
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/>
</template>
</Button>
</template>
</Dropdown>
</div>
<router-view
v-if="currentOrganization"
:organization="currentOrganization"
/>
<div
v-else
class="grid h-full flex-1 place-items-center text-xl font-medium text-gray-500"
>
<div class="flex flex-col items-center justify-center space-y-2">
<OrganizationsIcon class="h-10 w-10" />
<div>No organization selected</div>
</div>
<div class="flex items-center gap-2">
<Filter doctype="CRM Organization" />
<SortBy doctype="CRM Organization" />
<Button icon="more-horizontal" />
</div>
</div>
<OrganizationsListView :rows="rows" :columns="columns" />
<OrganizationModal
v-model="showOrganizationModal"
v-model:reloadOrganizations="organizations"
@ -69,19 +47,26 @@
<script setup>
import LayoutHeader from '@/components/LayoutHeader.vue'
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
import { FeatherIcon, Breadcrumbs, Avatar } from 'frappe-ui'
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import { FeatherIcon, Breadcrumbs, Dropdown } from 'frappe-ui'
import { organizationsStore } from '@/stores/organizations.js'
import { ref, computed, onMounted } from 'vue'
import { dateFormat, dateTooltipFormat, timeAgo, formatNumberIntoCurrency } from '@/utils'
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
const { organizations } = organizationsStore()
const route = useRoute()
const showOrganizationModal = ref(false)
const currentOrganization = computed(() => {
return organizations.data.find(
(organization) => organization.name === route.params.organizationId
)
})
const breadcrumbs = computed(() => {
let items = [{ label: 'Organizations', route: { name: 'Organizations' } }]
if (!currentOrganization.value) return items
@ -94,13 +79,102 @@ const breadcrumbs = computed(() => {
})
return items
})
onMounted(() => {
const el = document.querySelector('.router-link-active')
if (el)
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
const currentView = ref({
label: 'List',
icon: 'list',
})
const viewsDropdownOptions = [
{
label: 'List',
icon: 'list',
onClick() {
currentView.value = {
label: 'List',
icon: 'list',
}
},
},
{
label: 'Table',
icon: 'grid',
onClick() {
currentView.value = {
label: 'Table',
icon: 'grid',
}
},
},
{
label: 'Calender',
icon: 'calendar',
onClick() {
currentView.value = {
label: 'Calender',
icon: 'calendar',
}
},
},
{
label: 'Board',
icon: 'columns',
onClick() {
currentView.value = {
label: 'Board',
icon: 'columns',
}
},
},
]
const rows = computed(() => {
return organizations.data.map((organization) => {
return {
name: organization.name,
organization: {
label: organization.organization_name,
logo: organization.organization_logo,
},
website: website(organization.website),
industry: organization.industry,
annual_revenue: formatNumberIntoCurrency(organization.annual_revenue),
modified: {
label: dateFormat(organization.modified, dateTooltipFormat),
timeAgo: timeAgo(organization.modified),
},
}
})
})
const columns = [
{
label: 'Organization',
key: 'organization',
width: '16rem',
},
{
label: 'Website',
key: 'website',
width: '14rem',
},
{
label: 'Industry',
key: 'industry',
width: '14rem',
},
{
label: 'Annual Revenue',
key: 'annual_revenue',
width: '14rem',
},
{
label: 'Last modified',
key: 'modified',
width: '8rem',
},
]
function website(url) {
return url && url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
}

View File

@ -49,14 +49,12 @@ const routes = [
path: '/organizations',
name: 'Organizations',
component: () => import('@/pages/Organizations.vue'),
children: [
{
path: '/organizations/:organizationId?',
name: 'Organization',
component: () => import('@/pages/Organization.vue'),
props: true,
},
],
},
{
path: '/organizations/:organizationId',
name: 'Organization',
component: () => import('@/pages/Organization.vue'),
props: true,
},
{
path: '/call-logs',