feat: Redesign navigation menu like Close.com

- Inbox, Chancen (Pipeline), Leads, Kontakte, Firmen, Aktivitäten
- Konversationen, Workflows, Berichte (coming soon)
- Team & Einstellungen at bottom
- Expandable submenus for Leads and Workflows
- User profile at top of sidebar
- German labels
This commit is contained in:
FluxKit
2026-02-25 13:25:19 +00:00
parent 954a1f01d6
commit 7b0fd718b0
2 changed files with 140 additions and 86 deletions

View File

@@ -8,29 +8,46 @@ const route = useRoute()
const auth = useAuthStore()
const sidebarOpen = ref(false)
const isMobile = ref(false)
const leadsOpen = ref(true) // Leads submenu open by default
const baseNavItems = [
// Expandable menu states
const leadsOpen = ref(true)
const workflowsOpen = ref(false)
// Main navigation items
const mainNavItems = [
{ name: 'Inbox', path: '/inbox', icon: 'inbox' },
{ name: 'Dashboard', path: '/', icon: 'chart-pie' },
{ name: 'Chancen', path: '/pipeline', icon: 'opportunities' },
{
name: 'Leads',
icon: 'leads',
expandable: 'leads',
children: [
{ name: 'Kontakte', path: '/contacts', icon: 'users' },
{ name: 'Firmen', path: '/companies', icon: 'building-office' },
{ name: 'Alle Leads', path: '/leads', icon: 'list' },
]
},
{ name: 'Pipeline', path: '/pipeline', icon: 'funnel' },
{ name: 'Aktivitäten', path: '/activities', icon: 'clipboard-list' },
{ name: 'Kontakte', path: '/contacts', icon: 'contacts' },
{ name: 'Firmen', path: '/companies', icon: 'companies' },
{ name: 'Aktivitäten', path: '/activities', icon: 'activities' },
{ name: 'Konversationen', path: '/conversations', icon: 'conversations', badge: 'soon' },
{
name: 'Workflows',
icon: 'workflows',
expandable: 'workflows',
badge: 'soon',
children: [
{ name: 'Automatisierungen', path: '/workflows/automations', icon: 'automation' },
]
},
{ name: 'Berichte', path: '/reports', icon: 'reports', badge: 'soon' },
]
// Team nav item only for admin/owner
const navItems = computed(() => {
const items = [...baseNavItems]
// Bottom navigation items
const bottomNavItems = computed(() => {
const items = []
if (auth.user?.role === 'owner' || auth.user?.role === 'admin') {
items.push({ name: 'Team', path: '/team', icon: 'user-group' })
items.push({ name: 'Team', path: '/team', icon: 'team' })
}
items.push({ name: 'Einstellungen', path: '/settings', icon: 'settings' })
return items
})
@@ -46,10 +63,6 @@ function checkMobile() {
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
// Auto-expand leads if on contacts or companies page
if (route.path.startsWith('/contacts') || route.path.startsWith('/companies')) {
leadsOpen.value = true
}
})
onUnmounted(() => {
@@ -60,8 +73,15 @@ function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
function toggleLeads() {
leadsOpen.value = !leadsOpen.value
function toggleExpand(key) {
if (key === 'leads') leadsOpen.value = !leadsOpen.value
if (key === 'workflows') workflowsOpen.value = !workflowsOpen.value
}
function isExpanded(key) {
if (key === 'leads') return leadsOpen.value
if (key === 'workflows') return workflowsOpen.value
return false
}
function closeSidebarOnMobile() {
@@ -80,8 +100,9 @@ function isActive(path) {
return route.path.startsWith(path)
}
function isLeadsActive() {
return route.path.startsWith('/contacts') || route.path.startsWith('/companies')
function isGroupActive(item) {
if (!item.children) return false
return item.children.some(child => route.path.startsWith(child.path))
}
</script>
@@ -126,15 +147,16 @@ function isLeadsActive() {
'w-64'
]"
>
<!-- Logo -->
<div class="flex items-center justify-between gap-3 px-6 py-5 border-b border-pulse-border">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<!-- User Profile -->
<div class="flex items-center gap-3 px-4 py-4 border-b border-pulse-border">
<div class="w-10 h-10 rounded-full bg-primary-600 flex items-center justify-center text-white font-medium flex-shrink-0">
{{ auth.user?.firstName?.[0] || 'U' }}{{ auth.user?.lastName?.[0] || '' }}
</div>
<span class="text-xl font-bold text-white">Pulse</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">
{{ auth.user?.firstName }} {{ auth.user?.lastName }}
</p>
<p class="text-xs text-pulse-muted truncate">Pulse CRM</p>
</div>
<!-- Close button on mobile -->
<button
@@ -148,74 +170,94 @@ function isLeadsActive() {
</button>
</div>
<!-- Navigation -->
<nav class="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
<template v-for="item in navItems" :key="item.path || item.name">
<!-- Main Navigation -->
<nav class="flex-1 py-2 px-2 space-y-0.5 overflow-y-auto">
<template v-for="item in mainNavItems" :key="item.path || item.name">
<!-- Regular nav item -->
<RouterLink
v-if="!item.children"
:to="item.path"
:class="['sidebar-link', isActive(item.path) && 'active']"
@click="closeSidebarOnMobile"
:to="item.badge === 'soon' ? '#' : item.path"
:class="[
'sidebar-link group',
isActive(item.path) && 'active',
item.badge === 'soon' && 'opacity-60 cursor-not-allowed'
]"
@click.prevent="item.badge === 'soon' ? null : (closeSidebarOnMobile(), $router.push(item.path))"
>
<!-- Icons -->
<svg v-if="item.icon === 'inbox'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<svg v-if="item.icon === 'chart-pie'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
<svg v-if="item.icon === 'opportunities'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<svg v-if="item.icon === 'funnel'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
<svg v-if="item.icon === 'contacts'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<svg v-if="item.icon === 'clipboard-list'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg v-if="item.icon === 'companies'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<svg v-if="item.icon === 'activities'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<svg v-if="item.icon === 'user-group'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
<svg v-if="item.icon === 'conversations'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span>{{ item.name }}</span>
<svg v-if="item.icon === 'reports'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span class="flex-1">{{ item.name }}</span>
<span v-if="item.badge === 'soon'" class="text-[10px] px-1.5 py-0.5 bg-pulse-border rounded text-pulse-muted">Bald</span>
</RouterLink>
<!-- Leads submenu -->
<div v-else class="space-y-1">
<!-- Expandable menu -->
<div v-else class="space-y-0.5">
<button
@click="toggleLeads"
:class="['sidebar-link w-full justify-between', isLeadsActive() && 'active']"
@click="toggleExpand(item.expandable)"
:class="['sidebar-link w-full justify-between', isGroupActive(item) && 'active']"
>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
<svg v-if="item.icon === 'leads'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<svg v-if="item.icon === 'workflows'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>{{ item.name }}</span>
</div>
<div class="flex items-center gap-1">
<span v-if="item.badge === 'soon'" class="text-[10px] px-1.5 py-0.5 bg-pulse-border rounded text-pulse-muted">Bald</span>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-180': leadsOpen }"
:class="{ 'rotate-180': isExpanded(item.expandable) }"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
<!-- Submenu items -->
<div
v-show="leadsOpen"
class="pl-4 space-y-1"
v-show="isExpanded(item.expandable)"
class="pl-4 space-y-0.5"
>
<RouterLink
v-for="child in item.children"
:key="child.path"
:to="child.path"
:class="['sidebar-link', isActive(child.path) && 'active']"
:to="item.badge === 'soon' ? '#' : child.path"
:class="[
'sidebar-link text-sm',
isActive(child.path) && 'active',
item.badge === 'soon' && 'opacity-60 cursor-not-allowed'
]"
@click="closeSidebarOnMobile"
>
<svg v-if="child.icon === 'users'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
<svg v-if="child.icon === 'list'" class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
<svg v-if="child.icon === 'building-office'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
<svg v-if="child.icon === 'automation'" class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>{{ child.name }}</span>
</RouterLink>
@@ -224,29 +266,36 @@ function isLeadsActive() {
</template>
</nav>
<!-- User -->
<div class="border-t border-pulse-border p-4">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-primary-600 flex items-center justify-center text-white font-medium flex-shrink-0">
{{ auth.user?.firstName?.[0] || 'U' }}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">
{{ auth.user?.firstName }} {{ auth.user?.lastName }}
</p>
<p class="text-xs text-pulse-muted truncate">{{ auth.user?.email }}</p>
</div>
<!-- Bottom Navigation -->
<div class="border-t border-pulse-border py-2 px-2 space-y-0.5">
<RouterLink
v-for="item in bottomNavItems"
:key="item.path"
:to="item.path"
:class="['sidebar-link', isActive(item.path) && 'active']"
@click="closeSidebarOnMobile"
>
<svg v-if="item.icon === 'team'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<svg v-if="item.icon === 'settings'" class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{{ item.name }}</span>
</RouterLink>
<!-- Logout -->
<button
@click="logout"
class="p-2 text-pulse-muted hover:text-white transition-colors flex-shrink-0"
title="Abmelden"
class="sidebar-link w-full text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Abmelden</span>
</button>
</div>
</div>
</aside>
<!-- Main Content -->

View File

@@ -23,6 +23,11 @@ const routes = [
name: 'Inbox',
component: () => import('@/views/InboxView.vue')
},
{
path: 'leads',
name: 'Leads',
component: () => import('@/views/ContactsView.vue') // Uses contacts view for now
},
{
path: 'contacts',
name: 'Contacts',