crm/frontend/src/components/Layouts/AppSidebar.vue
2025-08-15 20:27:57 +05:30

610 lines
16 KiB
Vue

<template>
<div
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
>
<div class="p-2">
<UserDropdown :isCollapsed="isSidebarCollapsed" />
</div>
<div class="flex-1 overflow-y-auto">
<div class="mb-3 flex flex-col">
<SidebarLink
id="notifications-btn"
:label="__('Notifications')"
:icon="NotificationsIcon"
:isCollapsed="isSidebarCollapsed"
@click="() => toggleNotificationPanel()"
class="relative mx-2 my-0.5"
>
<template #right>
<Badge
v-if="!isSidebarCollapsed && unreadNotificationsCount"
:label="unreadNotificationsCount"
variant="subtle"
/>
<div
v-else-if="unreadNotificationsCount"
class="absolute -left-1.5 top-1 z-20 h-[5px] w-[5px] translate-x-6 translate-y-1 rounded-full bg-surface-gray-6 ring-1 ring-white"
/>
</template>
</SidebarLink>
</div>
<div v-for="view in allViews" :key="view.label">
<div
v-if="!view.hideLabel && isSidebarCollapsed && view.views?.length"
class="mx-2 my-2 h-1 border-b"
/>
<Section
:label="view.name"
:hideLabel="view.hideLabel"
:opened="view.opened"
>
<template #header="{ opened, hide, toggle }">
<div
v-if="!hide"
class="flex cursor-pointer gap-1.5 px-1 text-base font-medium text-ink-gray-5 transition-all duration-300 ease-in-out"
:class="
isSidebarCollapsed
? 'ml-0 h-0 overflow-hidden opacity-0'
: 'ml-2 mt-4 h-7 w-auto opacity-100'
"
@click="toggle()"
>
<FeatherIcon
name="chevron-right"
class="h-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
<span>{{ __(view.name) }}</span>
</div>
</template>
<nav class="flex flex-col">
<SidebarLink
v-for="link in view.views"
:icon="link.icon"
:label="__(link.label)"
:to="link.to"
:isCollapsed="isSidebarCollapsed"
class="mx-2 my-0.5"
/>
</nav>
</Section>
</div>
</div>
<div class="m-2 flex flex-col gap-1">
<div class="flex flex-col gap-2 mb-1">
<SignupBanner
v-if="isDemoSite"
:isSidebarCollapsed="isSidebarCollapsed"
:afterSignup="() => capture('signup_from_demo_site')"
/>
<TrialBanner
v-if="isFCSite"
:isSidebarCollapsed="isSidebarCollapsed"
:afterUpgrade="() => capture('upgrade_plan_from_trial_banner')"
/>
<GettingStartedBanner
v-if="!isOnboardingStepsCompleted"
:isSidebarCollapsed="isSidebarCollapsed"
/>
</div>
<SidebarLink
v-if="isOnboardingStepsCompleted"
:label="__('Help')"
:isCollapsed="isSidebarCollapsed"
@click="
() => {
showHelpModal = minimize ? true : !showHelpModal
minimize = !showHelpModal
}
"
>
<template #icon>
<HelpIcon class="h-4 w-4" />
</template>
</SidebarLink>
<SidebarLink
:label="isSidebarCollapsed ? __('Expand') : __('Collapse')"
:isCollapsed="isSidebarCollapsed"
@click="isSidebarCollapsed = !isSidebarCollapsed"
class=""
>
<template #icon>
<span class="grid h-4 w-4 flex-shrink-0 place-items-center">
<CollapseSidebar
class="h-4 w-4 text-ink-gray-7 duration-300 ease-in-out"
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }"
/>
</span>
</template>
</SidebarLink>
</div>
<Notifications />
<Settings />
<HelpModal
v-if="showHelpModal"
v-model="showHelpModal"
v-model:articles="articles"
:logo="CRMLogo"
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
:afterSkipAll="() => capture('onboarding_steps_skipped')"
:afterReset="(step) => capture('onboarding_step_reset_' + step)"
:afterResetAll="() => capture('onboarding_steps_reset')"
docsLink="https://docs.frappe.io/crm"
/>
<IntermediateStepModal
v-model="showIntermediateModal"
:currentStep="currentStep"
/>
</div>
</template>
<script setup>
import LucideLayoutDashboard from '~icons/lucide/layout-dashboard'
import CRMLogo from '@/components/Icons/CRMLogo.vue'
import InviteIcon from '@/components/Icons/InviteIcon.vue'
import ConvertIcon from '@/components/Icons/ConvertIcon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import StepsIcon from '@/components/Icons/StepsIcon.vue'
import Section from '@/components/Section.vue'
import PinIcon from '@/components/Icons/PinIcon.vue'
import UserDropdown from '@/components/UserDropdown.vue'
import SquareAsterisk from '@/components/Icons/SquareAsterisk.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import NotificationsIcon from '@/components/Icons/NotificationsIcon.vue'
import HelpIcon from '@/components/Icons/HelpIcon.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import Notifications from '@/components/Notifications.vue'
import Settings from '@/components/Settings/Settings.vue'
import { viewsStore } from '@/stores/views'
import {
unreadNotificationsCount,
notificationsStore,
} from '@/stores/notifications'
import { usersStore } from '@/stores/users'
import { sessionStore } from '@/stores/session'
import { showSettings, activeSettingsPage } from '@/composables/settings'
import { showChangePasswordModal } from '@/composables/modals'
import { FeatherIcon, call } from 'frappe-ui'
import {
SignupBanner,
TrialBanner,
HelpModal,
GettingStartedBanner,
useOnboarding,
showHelpModal,
minimize,
IntermediateStepModal,
} from 'frappe-ui/frappe'
import { capture } from '@/telemetry'
import router from '@/router'
import { useStorage } from '@vueuse/core'
import { ref, reactive, computed, h, markRaw, onMounted } from 'vue'
const { getPinnedViews, getPublicViews } = viewsStore()
const { toggle: toggleNotificationPanel } = notificationsStore()
const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
const isFCSite = ref(window.is_fc_site)
const isDemoSite = ref(window.is_demo_site)
const links = [
{
label: 'Dashboard',
icon: LucideLayoutDashboard,
to: 'Dashboard',
},
{
label: 'Leads',
icon: LeadsIcon,
to: 'Leads',
},
{
label: 'Deals',
icon: DealsIcon,
to: 'Deals',
},
{
label: 'Contacts',
icon: ContactsIcon,
to: 'Contacts',
},
{
label: 'Organizations',
icon: OrganizationsIcon,
to: 'Organizations',
},
{
label: 'Notes',
icon: NoteIcon,
to: 'Notes',
},
{
label: 'Tasks',
icon: TaskIcon,
to: 'Tasks',
},
{
label: 'Call Logs',
icon: PhoneIcon,
to: 'Call Logs',
},
]
const allViews = computed(() => {
let _views = [
{
name: 'All Views',
hideLabel: true,
opened: true,
views: links.filter((link) => {
if (link.condition) {
return link.condition()
}
return true
}),
},
]
if (getPublicViews().length) {
_views.push({
name: 'Public views',
opened: true,
views: parseView(getPublicViews()),
})
}
if (getPinnedViews().length) {
_views.push({
name: 'Pinned views',
opened: true,
views: parseView(getPinnedViews()),
})
}
return _views
})
function parseView(views) {
return views.map((view) => {
return {
label: view.label,
icon: getIcon(view.route_name, view.icon),
to: {
name: view.route_name,
params: { viewType: view.type || 'list' },
query: { view: view.name },
},
}
})
}
function getIcon(routeName, icon) {
if (icon) return h('div', { class: 'size-auto' }, icon)
switch (routeName) {
case 'Leads':
return LeadsIcon
case 'Deals':
return DealsIcon
case 'Contacts':
return ContactsIcon
case 'Organizations':
return OrganizationsIcon
case 'Notes':
return NoteIcon
case 'Call Logs':
return PhoneIcon
default:
return PinIcon
}
}
// onboarding
const { user } = sessionStore()
const { users, isManager } = usersStore()
const { isOnboardingStepsCompleted, setUp } = useOnboarding('frappecrm')
async function getFirstLead() {
let firstLead = localStorage.getItem('firstLead' + user)
if (firstLead) return firstLead
return await call('crm.api.onboarding.get_first_lead')
}
async function getFirstDeal() {
let firstDeal = localStorage.getItem('firstDeal' + user)
if (firstDeal) return firstDeal
return await call('crm.api.onboarding.get_first_deal')
}
const showIntermediateModal = ref(false)
const currentStep = ref({})
const steps = reactive([
{
name: 'setup_your_password',
title: __('Setup your password'),
icon: markRaw(SquareAsterisk),
completed: false,
onClick: () => {
minimize.value = true
showChangePasswordModal.value = true
},
},
{
name: 'create_first_lead',
title: __('Create your first lead'),
icon: markRaw(LeadsIcon),
completed: false,
onClick: () => {
minimize.value = true
router.push({ name: 'Leads' })
},
},
{
name: 'invite_your_team',
title: __('Invite your team'),
icon: markRaw(InviteIcon),
completed: false,
onClick: () => {
minimize.value = true
showSettings.value = true
activeSettingsPage.value = 'Invite User'
},
condition: () => isManager(),
},
{
name: 'convert_lead_to_deal',
title: __('Convert lead to deal'),
icon: markRaw(ConvertIcon),
completed: false,
dependsOn: 'create_first_lead',
onClick: async () => {
minimize.value = true
currentStep.value = {
title: __('Convert lead to deal'),
buttonLabel: __('Convert'),
videoURL: '/assets/crm/videos/convertToDeal.mov',
onClick: async () => {
showIntermediateModal.value = false
currentStep.value = {}
let lead = await getFirstLead()
if (lead) {
router.push({ name: 'Lead', params: { leadId: lead } })
} else {
router.push({ name: 'Leads' })
}
},
}
showIntermediateModal.value = true
},
},
{
name: 'create_first_task',
title: __('Create your first task'),
icon: markRaw(TaskIcon),
completed: false,
onClick: async () => {
minimize.value = true
let deal = await getFirstDeal()
if (deal) {
router.push({
name: 'Deal',
params: { dealId: deal },
hash: '#tasks',
})
} else {
router.push({ name: 'Tasks' })
}
},
},
{
name: 'create_first_note',
title: __('Create your first note'),
icon: markRaw(NoteIcon),
completed: false,
onClick: async () => {
minimize.value = true
let deal = await getFirstDeal()
if (deal) {
router.push({
name: 'Deal',
params: { dealId: deal },
hash: '#notes',
})
} else {
router.push({ name: 'Notes' })
}
},
},
{
name: 'add_first_comment',
title: __('Add your first comment'),
icon: markRaw(CommentIcon),
completed: false,
dependsOn: 'create_first_lead',
onClick: async () => {
minimize.value = true
let deal = await getFirstDeal()
if (deal) {
router.push({
name: 'Deal',
params: { dealId: deal },
hash: '#comments',
})
} else {
router.push({ name: 'Leads' })
}
},
},
{
name: 'send_first_email',
title: __('Send email'),
icon: markRaw(EmailIcon),
completed: false,
dependsOn: 'create_first_lead',
onClick: async () => {
minimize.value = true
let deal = await getFirstDeal()
if (deal) {
router.push({
name: 'Deal',
params: { dealId: deal },
hash: '#emails',
})
} else {
router.push({ name: 'Leads' })
}
},
},
{
name: 'change_deal_status',
title: __('Change deal status'),
icon: markRaw(StepsIcon),
completed: false,
dependsOn: 'convert_lead_to_deal',
onClick: async () => {
minimize.value = true
currentStep.value = {
title: __('Change deal status'),
buttonLabel: __('Change'),
videoURL: '/assets/crm/videos/changeDealStatus.mov',
onClick: async () => {
showIntermediateModal.value = false
currentStep.value = {}
let deal = await getFirstDeal()
if (deal) {
router.push({
name: 'Deal',
params: { dealId: deal },
hash: '#activity',
})
} else {
router.push({ name: 'Leads' })
}
},
}
showIntermediateModal.value = true
},
},
])
onMounted(async () => {
await users.promise
const filteredSteps = steps.filter((step) => {
if (step.condition) {
return step.condition()
}
return true
})
setUp(filteredSteps)
})
// help center
const articles = ref([
{
title: __('Introduction'),
opened: false,
subArticles: [
{ name: 'introduction', title: __('Introduction') },
{ name: 'setting-up', title: __('Setting up') },
],
},
{
title: __('Settings'),
opened: false,
subArticles: [
{ name: 'profile', title: __('Profile') },
{ name: 'custom-branding', title: __('Custom branding') },
{ name: 'home-actions', title: __('Home actions') },
{ name: 'invite-users', title: __('Invite users') },
],
},
{
title: __('Masters'),
opened: false,
subArticles: [
{ name: 'lead', title: __('Lead') },
{ name: 'deal', title: __('Deal') },
{ name: 'contact', title: __('Contact') },
{ name: 'organization', title: __('Organization') },
{ name: 'note', title: __('Note') },
{ name: 'task', title: __('Task') },
{ name: 'call-log', title: __('Call log') },
{ name: 'email-template', title: __('Email template') },
],
},
{
title: __('Capturing leads'),
opened: false,
subArticles: [{ name: 'web-form', title: __('Web form') }],
},
{
title: __('Views'),
opened: false,
subArticles: [
{ name: 'view', title: __('Saved view') },
{ name: 'public-view', title: __('Public view') },
{ name: 'pinned-view', title: __('Pinned view') },
],
},
{
title: __('Other features'),
opened: false,
subArticles: [
{ name: 'email-communication', title: __('Email communication') },
{ name: 'comment', title: __('Comment') },
{ name: 'data', title: __('Data') },
{ name: 'service-level-agreement', title: __('Service level agreement') },
{ name: 'assignment-rule', title: __('Assignment rule') },
{ name: 'notification', title: __('Notification') },
],
},
{
title: __('Customization'),
opened: false,
subArticles: [
{ name: 'custom-fields', title: __('Custom fields') },
{ name: 'custom-actions', title: __('Custom actions') },
{ name: 'custom-statuses', title: __('Custom statuses') },
{ name: 'custom-list-actions', title: __('Custom list actions') },
{ name: 'quick-entry-layout', title: __('Quick entry layout') },
],
},
{
title: __('Integration'),
opened: false,
subArticles: [
{ name: 'twilio', title: __('Twilio') },
{ name: 'exotel', title: __('Exotel') },
{ name: 'whatsapp', title: __('WhatsApp') },
{ name: 'erpnext', title: __('ERPNext') },
],
},
{
title: __('Frappe CRM mobile'),
opened: false,
subArticles: [
{ name: 'mobile-app-installation', title: __('Mobile app installation') },
],
},
])
</script>