Merge pull request #16 from shariquerik/frappeui-tabs-component
This commit is contained in:
commit
35b0bf9a8b
@ -1 +1 @@
|
|||||||
Subproject commit 8762f85ef72c01c2dbf9fbe2bd7417911d347ac3
|
Subproject commit 167ff453cb525674f976d53b126900b80b9a80a2
|
||||||
8
frontend/.gitignore
vendored
8
frontend/.gitignore
vendored
@ -2,10 +2,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
*.pyc
|
|
||||||
*.egg-info
|
|
||||||
*.swp
|
|
||||||
tags
|
|
||||||
crm/public/frontend
|
|
||||||
__pycache__
|
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.5",
|
"frappe-ui": "^0.1.6",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex min-w-0 items-center text-ellipsis whitespace-nowrap">
|
|
||||||
<template v-for="(item, i) in items" :key="item.label">
|
|
||||||
<router-link
|
|
||||||
class="flex items-center rounded px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 text-gray-600 hover:text-gray-700 last:text-gray-900 last:hover:text-gray-900 transition-all duration-300 ease-in-out"
|
|
||||||
:to="item.route || ''"
|
|
||||||
>
|
|
||||||
<slot name="prefix" :item="item" />
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
</router-link>
|
|
||||||
<span
|
|
||||||
v-if="i != items.length - 1"
|
|
||||||
class="mx-0.5 text-base text-gray-500"
|
|
||||||
>
|
|
||||||
/
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -67,7 +67,7 @@
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { dealStatuses, statusDropdownOptions } from '@/utils'
|
import { dealStatuses, statusDropdownOptions, activeAgents } from '@/utils'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
Button,
|
Button,
|
||||||
@ -153,18 +153,4 @@ const allFields = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeAgents = computed(() => {
|
|
||||||
const nonAgents = ['Administrator', 'Guest']
|
|
||||||
return users.data
|
|
||||||
.filter((user) => !nonAgents.includes(user.name))
|
|
||||||
.sort((a, b) => a.full_name - b.full_name)
|
|
||||||
.map((user) => {
|
|
||||||
return {
|
|
||||||
label: user.full_name,
|
|
||||||
value: user.email,
|
|
||||||
...user,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div v-for="section in allFields" :key="section.section">
|
<div v-for="section in allFields" :key="section.section">
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div v-for="field in section.fields" :key="field.name">
|
<div v-for="field in section.fields" :key="field.name">
|
||||||
<div class="text-gray-600 text-sm mb-2">{{ field.label }}</div>
|
<div class="mb-2 text-sm text-gray-600">{{ field.label }}</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="field.type === 'select'"
|
v-if="field.type === 'select'"
|
||||||
type="select"
|
type="select"
|
||||||
@ -41,10 +41,12 @@
|
|||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<Button
|
<Button
|
||||||
:label="newLead[field.name]"
|
:label="newLead[field.name]"
|
||||||
class="justify-between w-full"
|
class="w-full justify-between"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<IndicatorIcon :class="leadStatuses[newLead[field.name]].color" />
|
<IndicatorIcon
|
||||||
|
:class="leadStatuses[newLead[field.name]].color"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #default>{{ newLead[field.name] }}</template>
|
<template #default>{{ newLead[field.name] }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@ -67,7 +69,7 @@
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { leadStatuses, statusDropdownOptions } from '@/utils'
|
import { leadStatuses, statusDropdownOptions, activeAgents } from '@/utils'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
Button,
|
Button,
|
||||||
@ -153,19 +155,4 @@ const allFields = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeAgents = computed(() => {
|
|
||||||
const nonAgents = ['Administrator', 'Guest']
|
|
||||||
return users.data
|
|
||||||
.filter((user) => !nonAgents.includes(user.name))
|
|
||||||
.sort((a, b) => a.full_name - b.full_name)
|
|
||||||
.map((user) => {
|
|
||||||
return {
|
|
||||||
label: user.full_name,
|
|
||||||
value: user.email,
|
|
||||||
...user,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="border-b"></div>
|
<div class="border-b"></div>
|
||||||
<div v-if="callLog.data" class="p-6 max-w-lg">
|
<div v-if="callLog.data" class="max-w-lg p-6">
|
||||||
<div class="pb-3 text-base font-medium">Call details</div>
|
<div class="pb-3 text-base font-medium">Call details</div>
|
||||||
<div class="flex flex-col gap-4 border rounded-lg p-4 mb-3 shadow-sm">
|
<div class="mb-3 flex flex-col gap-4 rounded-lg border p-4 shadow-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
@ -26,7 +26,7 @@
|
|||||||
? 'phone-incoming'
|
? 'phone-incoming'
|
||||||
: 'phone-outgoing'
|
: 'phone-outgoing'
|
||||||
"
|
"
|
||||||
class="w-4 h-4 text-gray-600"
|
class="h-4 w-4 text-gray-600"
|
||||||
/>
|
/>
|
||||||
<div class="font-medium">
|
<div class="font-medium">
|
||||||
{{ callLog.data.type == 'Incoming' ? 'Inbound' : 'Outbound' }} call
|
{{ callLog.data.type == 'Incoming' ? 'Inbound' : 'Outbound' }} call
|
||||||
@ -48,7 +48,7 @@
|
|||||||
:label="callLog.data.caller.label"
|
:label="callLog.data.caller.label"
|
||||||
size="xl"
|
size="xl"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-1 ml-1">
|
<div class="ml-1 flex flex-col gap-1">
|
||||||
<div class="text-base font-medium">
|
<div class="text-base font-medium">
|
||||||
{{ callLog.data.caller.label }}
|
{{ callLog.data.caller.label }}
|
||||||
</div>
|
</div>
|
||||||
@ -56,13 +56,13 @@
|
|||||||
{{ callLog.data.from }}
|
{{ callLog.data.from }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FeatherIcon name="arrow-right" class="w-5 h-5 text-gray-600 mx-2" />
|
<FeatherIcon name="arrow-right" class="mx-2 h-5 w-5 text-gray-600" />
|
||||||
<Avatar
|
<Avatar
|
||||||
:image="callLog.data.receiver.image"
|
:image="callLog.data.receiver.image"
|
||||||
:label="callLog.data.receiver.label"
|
:label="callLog.data.receiver.label"
|
||||||
size="xl"
|
size="xl"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-1 ml-1">
|
<div class="ml-1 flex flex-col gap-1">
|
||||||
<div class="text-base font-medium">
|
<div class="text-base font-medium">
|
||||||
{{ callLog.data.receiver.label }}
|
{{ callLog.data.receiver.label }}
|
||||||
</div>
|
</div>
|
||||||
@ -74,13 +74,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<DurationIcon class="w-4 h-4 text-gray-600" />
|
<DurationIcon class="h-4 w-4 text-gray-600" />
|
||||||
<div class="text-sm text-gray-600">Duration</div>
|
<div class="text-sm text-gray-600">Duration</div>
|
||||||
<div class="text-sm">{{ callLog.data.duration }}</div>
|
<div class="text-sm">{{ callLog.data.duration }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class="text-gray-600 text-sm"
|
class="text-sm text-gray-600"
|
||||||
:text="dateFormat(callLog.data.creation, dateTooltipFormat)"
|
:text="dateFormat(callLog.data.creation, dateTooltipFormat)"
|
||||||
>
|
>
|
||||||
{{ timeAgo(callLog.data.creation) }}
|
{{ timeAgo(callLog.data.creation) }}
|
||||||
@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<div v-if="callLog.data.recording_url" class="mt-6">
|
<div v-if="callLog.data.recording_url" class="mt-6">
|
||||||
<div class="mb-3 text-base font-medium">Call recording</div>
|
<div class="mb-3 text-base font-medium">Call recording</div>
|
||||||
<div class="flex items-center justify-between border rounded shadow-sm">
|
<div class="flex items-center justify-between rounded border shadow-sm">
|
||||||
<audio
|
<audio
|
||||||
class="audio-control"
|
class="audio-control"
|
||||||
controls
|
controls
|
||||||
@ -103,10 +103,10 @@
|
|||||||
<div v-if="callLog.data.note" class="mt-6">
|
<div v-if="callLog.data.note" class="mt-6">
|
||||||
<div class="mb-3 text-base font-medium">Call note</div>
|
<div class="mb-3 text-base font-medium">Call note</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 border rounded p-4 shadow-sm cursor-pointer h-56"
|
class="flex h-56 cursor-pointer flex-col gap-3 rounded border p-4 shadow-sm"
|
||||||
@click="showNoteModal = true"
|
@click="showNoteModal = true"
|
||||||
>
|
>
|
||||||
<div class="text-lg font-medium truncate">
|
<div class="truncate text-lg font-medium">
|
||||||
{{ callLog.data.note_doc.title }}
|
{{ callLog.data.note_doc.title }}
|
||||||
</div>
|
</div>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
@ -141,7 +141,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import DurationIcon from '@/components/Icons/DurationIcon.vue'
|
import DurationIcon from '@/components/Icons/DurationIcon.vue'
|
||||||
import NoteModal from '@/components/NoteModal.vue'
|
import NoteModal from '@/components/NoteModal.vue'
|
||||||
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
||||||
@ -153,6 +152,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
createResource,
|
createResource,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
@ -232,7 +232,10 @@ function createLead() {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => [
|
const breadcrumbs = computed(() => [
|
||||||
{ label: 'Call Logs', route: { name: 'Call Logs' } },
|
{ label: 'Call Logs', route: { name: 'Call Logs' } },
|
||||||
{ label: callLog.data?.caller.label },
|
{
|
||||||
|
label: callLog.data?.caller.label,
|
||||||
|
route: { name: 'Call Log', params: { callLogId: props.callLogId } },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: list.title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="flex justify-between items-center px-5 pt-3 pb-4">
|
<div class="flex items-center justify-between px-5 pb-4 pt-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button label="Sort">
|
<Button label="Sort">
|
||||||
<template #prefix><SortIcon class="h-4" /></template>
|
<template #prefix><SortIcon class="h-4" /></template>
|
||||||
@ -23,13 +23,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ListView from '@/components/ListView.vue'
|
import ListView from '@/components/ListView.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import SortIcon from '@/components/Icons/SortIcon.vue'
|
import SortIcon from '@/components/Icons/SortIcon.vue'
|
||||||
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
||||||
import { secondsToDuration } from '@/utils'
|
import { secondsToDuration } from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { Button, createListResource } from 'frappe-ui'
|
import { Button, createListResource, Breadcrumbs } from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
@ -41,6 +40,8 @@ const list = {
|
|||||||
singular_label: 'Call Log',
|
singular_label: 'Call Log',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = [{ label: list.title, route: { name: 'Call Logs' } }]
|
||||||
|
|
||||||
const callLogs = createListResource({
|
const callLogs = createListResource({
|
||||||
type: 'list',
|
type: 'list',
|
||||||
doctype: 'CRM Call Log',
|
doctype: 'CRM Call Log',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: list.title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button variant="solid" label="Create">
|
<Button variant="solid" label="Create">
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="flex justify-between items-center px-5 pt-3 pb-4">
|
<div class="flex items-center justify-between px-5 pb-4 pt-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
<Dropdown :options="viewsDropdownOptions">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -42,10 +42,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ListView from '@/components/ListView.vue'
|
import ListView from '@/components/ListView.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import SortIcon from '@/components/Icons/SortIcon.vue'
|
import SortIcon from '@/components/Icons/SortIcon.vue'
|
||||||
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
||||||
import { FeatherIcon, Button, Dropdown } from 'frappe-ui'
|
import { FeatherIcon, Button, Dropdown, Breadcrumbs } from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { contactsStore } from '@/stores/contacts.js'
|
import { contactsStore } from '@/stores/contacts.js'
|
||||||
|
|
||||||
@ -57,6 +56,8 @@ const list = {
|
|||||||
singular_label: 'Contact',
|
singular_label: 'Contact',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = [{ label: list.title, route: { name: 'Contacts' } }]
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
label: 'Full name',
|
label: 'Full name',
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
import { Breadcrumbs } from 'frappe-ui'
|
||||||
let title = 'Dashboard'
|
let title = 'Dashboard'
|
||||||
|
const breadcrumbs = [{ label: title, route: { name: 'Dashboard' } }]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -36,43 +36,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div v-if="deal.data" class="flex h-full overflow-hidden">
|
<div v-if="deal.data" class="flex h-full overflow-hidden">
|
||||||
<TabGroup as="div" class="flex flex-1 flex-col" @change="onTabChange">
|
<Tabs v-model="tabIndex" v-slot="{ tab }" :tabs="tabs">
|
||||||
<TabList class="relative flex items-center gap-6 border-b pl-5">
|
<Activities :title="tab.label" v-model:reload="reload" v-model="deal" />
|
||||||
<Tab
|
</Tabs>
|
||||||
ref="tabRef"
|
|
||||||
as="template"
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.label"
|
|
||||||
v-slot="{ selected }"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="-mb-[1px] flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 transition-all duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
|
||||||
:class="{ 'text-gray-900': selected }"
|
|
||||||
>
|
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
|
||||||
{{ tab.label }}
|
|
||||||
</button>
|
|
||||||
</Tab>
|
|
||||||
<div
|
|
||||||
ref="indicator"
|
|
||||||
class="absolute -bottom-[1px] h-[1px] w-[82px] bg-gray-900"
|
|
||||||
:style="{ left: `${indicatorLeftValue}px` }"
|
|
||||||
/>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels class="flex flex-1 overflow-hidden">
|
|
||||||
<TabPanel
|
|
||||||
class="flex flex-1 flex-col overflow-y-auto"
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.label"
|
|
||||||
>
|
|
||||||
<Activities
|
|
||||||
:title="tab.label"
|
|
||||||
v-model:reload="reload"
|
|
||||||
v-model="deal"
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</TabGroup>
|
|
||||||
<div class="flex w-[352px] flex-col justify-between border-l">
|
<div class="flex w-[352px] flex-col justify-between border-l">
|
||||||
<div
|
<div
|
||||||
class="flex h-[41px] items-center border-b px-5 py-2.5 text-lg font-semibold"
|
class="flex h-[41px] items-center border-b px-5 py-2.5 text-lg font-semibold"
|
||||||
@ -350,15 +316,13 @@ import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
|||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Toggler from '@/components/Toggler.vue'
|
import Toggler from '@/components/Toggler.vue'
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
|
||||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
|
||||||
import {
|
import {
|
||||||
dealStatuses,
|
dealStatuses,
|
||||||
statusDropdownOptions,
|
statusDropdownOptions,
|
||||||
openWebsite,
|
openWebsite,
|
||||||
createToast,
|
createToast,
|
||||||
|
activeAgents,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
@ -372,6 +336,8 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Tabs,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
@ -434,6 +400,7 @@ const breadcrumbs = computed(() => {
|
|||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tabIndex = ref(0)
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Activity',
|
label: 'Activity',
|
||||||
@ -469,21 +436,6 @@ function validateFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabRef = ref([])
|
|
||||||
const indicator = ref(null)
|
|
||||||
|
|
||||||
let indicatorLeft = ref(20)
|
|
||||||
const indicatorLeftValue = useTransition(indicatorLeft, {
|
|
||||||
duration: 250,
|
|
||||||
ease: TransitionPresets.easeOutCubic,
|
|
||||||
})
|
|
||||||
|
|
||||||
function onTabChange(index) {
|
|
||||||
const selectedTab = tabRef.value[index].el
|
|
||||||
indicator.value.style.width = `${selectedTab.offsetWidth}px`
|
|
||||||
indicatorLeft.value = selectedTab.offsetLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const detailSections = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -572,20 +524,6 @@ const detailSections = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeAgents = computed(() => {
|
|
||||||
const nonAgents = ['Administrator', 'Guest']
|
|
||||||
return users.data
|
|
||||||
.filter((user) => !nonAgents.includes(user.name))
|
|
||||||
.sort((a, b) => a.full_name - b.full_name)
|
|
||||||
.map((user) => {
|
|
||||||
return {
|
|
||||||
label: user.full_name,
|
|
||||||
value: user.email,
|
|
||||||
...user,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateAssignedAgent(email) {
|
function updateAssignedAgent(email) {
|
||||||
deal.data.lead_owner = email
|
deal.data.lead_owner = email
|
||||||
updateDeal('lead_owner', email)
|
updateDeal('lead_owner', email)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: list.title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button variant="solid" label="Create" @click="showNewDialog = true">
|
<Button variant="solid" label="Create" @click="showNewDialog = true">
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="flex justify-between items-center px-5 pt-3 pb-4">
|
<div class="flex items-center justify-between px-5 pb-4 pt-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
<Dropdown :options="viewsDropdownOptions">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -56,7 +56,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ListView from '@/components/ListView.vue'
|
import ListView from '@/components/ListView.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import NewDeal from '@/components/NewDeal.vue'
|
import NewDeal from '@/components/NewDeal.vue'
|
||||||
import SortBy from '@/components/SortBy.vue'
|
import SortBy from '@/components/SortBy.vue'
|
||||||
import Filter from '@/components/Filter.vue'
|
import Filter from '@/components/Filter.vue'
|
||||||
@ -72,6 +71,7 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
createListResource,
|
createListResource,
|
||||||
createResource,
|
createResource,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ref, computed, reactive, watch } from 'vue'
|
import { ref, computed, reactive, watch } from 'vue'
|
||||||
@ -81,6 +81,9 @@ const list = {
|
|||||||
plural_label: 'Deals',
|
plural_label: 'Deals',
|
||||||
singular_label: 'Deal',
|
singular_label: 'Deal',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = [{ label: list.title, route: { name: 'Deals' } }]
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { get: getOrderBy } = useOrderBy()
|
const { get: getOrderBy } = useOrderBy()
|
||||||
const { getArgs, storage } = useFilter()
|
const { getArgs, storage } = useFilter()
|
||||||
|
|||||||
@ -39,43 +39,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div v-if="lead?.data" class="flex h-full overflow-hidden">
|
<div v-if="lead?.data" class="flex h-full overflow-hidden">
|
||||||
<TabGroup as="div" class="flex flex-1 flex-col" @change="onTabChange">
|
<Tabs v-model="tabIndex" v-slot="{ tab }" :tabs="tabs">
|
||||||
<TabList class="relative flex items-center gap-6 border-b pl-5">
|
<Activities :title="tab.label" v-model:reload="reload" v-model="lead" />
|
||||||
<Tab
|
</Tabs>
|
||||||
ref="tabRef"
|
|
||||||
as="template"
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.label"
|
|
||||||
v-slot="{ selected }"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="-mb-[1px] flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 transition-all duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
|
||||||
:class="{ 'text-gray-900': selected }"
|
|
||||||
>
|
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
|
||||||
{{ tab.label }}
|
|
||||||
</button>
|
|
||||||
</Tab>
|
|
||||||
<div
|
|
||||||
ref="indicator"
|
|
||||||
class="absolute -bottom-[1px] h-[1px] w-[82px] bg-gray-900"
|
|
||||||
:style="{ left: `${indicatorLeftValue}px` }"
|
|
||||||
/>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels class="flex flex-1 overflow-hidden">
|
|
||||||
<TabPanel
|
|
||||||
class="flex flex-1 flex-col overflow-y-auto"
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.label"
|
|
||||||
>
|
|
||||||
<Activities
|
|
||||||
:title="tab.label"
|
|
||||||
v-model:reload="reload"
|
|
||||||
v-model="lead"
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</TabGroup>
|
|
||||||
<div class="flex w-[352px] flex-col justify-between border-l">
|
<div class="flex w-[352px] flex-col justify-between border-l">
|
||||||
<div
|
<div
|
||||||
class="flex h-[41px] items-center border-b px-5 py-2.5 text-lg font-semibold"
|
class="flex h-[41px] items-center border-b px-5 py-2.5 text-lg font-semibold"
|
||||||
@ -321,15 +287,13 @@ import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
|||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Toggler from '@/components/Toggler.vue'
|
import Toggler from '@/components/Toggler.vue'
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
|
||||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
|
||||||
import {
|
import {
|
||||||
leadStatuses,
|
leadStatuses,
|
||||||
statusDropdownOptions,
|
statusDropdownOptions,
|
||||||
openWebsite,
|
openWebsite,
|
||||||
createToast,
|
createToast,
|
||||||
|
activeAgents,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
@ -343,12 +307,13 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Tabs,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CameraIcon from '../components/Icons/CameraIcon.vue'
|
import CameraIcon from '../components/Icons/CameraIcon.vue'
|
||||||
|
|
||||||
|
|
||||||
const { getUser, users } = usersStore()
|
const { getUser, users } = usersStore()
|
||||||
const { contacts } = contactsStore()
|
const { contacts } = contactsStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -412,6 +377,7 @@ const breadcrumbs = computed(() => {
|
|||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tabIndex = ref(0)
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Activity',
|
label: 'Activity',
|
||||||
@ -447,21 +413,6 @@ function validateFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabRef = ref([])
|
|
||||||
const indicator = ref(null)
|
|
||||||
|
|
||||||
let indicatorLeft = ref(20)
|
|
||||||
const indicatorLeftValue = useTransition(indicatorLeft, {
|
|
||||||
duration: 250,
|
|
||||||
ease: TransitionPresets.easeOutCubic,
|
|
||||||
})
|
|
||||||
|
|
||||||
function onTabChange(index) {
|
|
||||||
const selectedTab = tabRef.value[index].el
|
|
||||||
indicator.value.style.width = `${selectedTab.offsetWidth}px`
|
|
||||||
indicatorLeft.value = selectedTab.offsetLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const detailSections = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -566,20 +517,6 @@ const detailSections = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeAgents = computed(() => {
|
|
||||||
const nonAgents = ['Administrator', 'admin@example.com', 'Guest']
|
|
||||||
return users.data
|
|
||||||
.filter((user) => !nonAgents.includes(user.name))
|
|
||||||
.sort((a, b) => a.full_name - b.full_name)
|
|
||||||
.map((user) => {
|
|
||||||
return {
|
|
||||||
label: user.full_name,
|
|
||||||
value: user.email,
|
|
||||||
...user,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function convertToDeal() {
|
function convertToDeal() {
|
||||||
lead.data.status = 'Qualified'
|
lead.data.status = 'Qualified'
|
||||||
lead.data.is_deal = 1
|
lead.data.is_deal = 1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: list.title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button variant="solid" label="Create" @click="showNewDialog = true">
|
<Button variant="solid" label="Create" @click="showNewDialog = true">
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="flex justify-between items-center px-5 pt-3 pb-4">
|
<div class="flex items-center justify-between px-5 pb-4 pt-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
<Dropdown :options="viewsDropdownOptions">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -55,7 +55,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ListView from '@/components/ListView.vue'
|
import ListView from '@/components/ListView.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import NewLead from '@/components/NewLead.vue'
|
import NewLead from '@/components/NewLead.vue'
|
||||||
import SortBy from '@/components/SortBy.vue'
|
import SortBy from '@/components/SortBy.vue'
|
||||||
import Filter from '@/components/Filter.vue'
|
import Filter from '@/components/Filter.vue'
|
||||||
@ -71,6 +70,7 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
createListResource,
|
createListResource,
|
||||||
createResource,
|
createResource,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ref, computed, reactive, watch } from 'vue'
|
import { ref, computed, reactive, watch } from 'vue'
|
||||||
@ -80,6 +80,9 @@ const list = {
|
|||||||
plural_label: 'Leads',
|
plural_label: 'Leads',
|
||||||
singular_label: 'Lead',
|
singular_label: 'Lead',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = [{ label: list.title, route: { name: 'Leads' } }]
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { get: getOrderBy } = useOrderBy()
|
const { get: getOrderBy } = useOrderBy()
|
||||||
const { getArgs, storage } = useFilter()
|
const { getArgs, storage } = useFilter()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="[{ label: list.title }]" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button variant="solid" label="Create" @click="createNote">
|
<Button variant="solid" label="Create" @click="createNote">
|
||||||
@ -79,7 +79,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import NoteModal from '@/components/NoteModal.vue'
|
import NoteModal from '@/components/NoteModal.vue'
|
||||||
@ -92,6 +91,7 @@ import {
|
|||||||
call,
|
call,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Breadcrumbs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
@ -104,6 +104,8 @@ const list = {
|
|||||||
singular_label: 'Note',
|
singular_label: 'Note',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = [{ label: list.title, route: { name: 'Notes' } }]
|
||||||
|
|
||||||
const showNoteModal = ref(false)
|
const showNoteModal = ref(false)
|
||||||
const currentNote = ref(null)
|
const currentNote = ref(null)
|
||||||
|
|
||||||
|
|||||||
@ -157,7 +157,7 @@ export function startCase(str) {
|
|||||||
const { users } = usersStore()
|
const { users } = usersStore()
|
||||||
|
|
||||||
export const activeAgents = computed(() => {
|
export const activeAgents = computed(() => {
|
||||||
const nonAgents = ['Administrator', 'Guest']
|
const nonAgents = ['Administrator', 'admin@example.com', 'Guest']
|
||||||
return users.data
|
return users.data
|
||||||
.filter((user) => !nonAgents.includes(user.name))
|
.filter((user) => !nonAgents.includes(user.name))
|
||||||
.sort((a, b) => a.full_name - b.full_name)
|
.sort((a, b) => a.full_name - b.full_name)
|
||||||
|
|||||||
1261
frontend/yarn.lock
1261
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": ["frappe-ui", "frontend"],
|
"aworkspaces": ["frappe-ui", "frontend"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "cd frontend && yarn install",
|
"postinstall": "cd frontend && yarn install",
|
||||||
"dev": "cd frontend && yarn dev",
|
"dev": "cd frontend && yarn dev",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user