- 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
307 lines
14 KiB
Vue
307 lines
14 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const auth = useAuthStore()
|
|
const sidebarOpen = ref(false)
|
|
const isMobile = ref(false)
|
|
|
|
// Expandable menu states
|
|
const leadsOpen = ref(true)
|
|
const workflowsOpen = ref(false)
|
|
|
|
// Main navigation items
|
|
const mainNavItems = [
|
|
{ name: 'Inbox', path: '/inbox', icon: 'inbox' },
|
|
{ name: 'Chancen', path: '/pipeline', icon: 'opportunities' },
|
|
{
|
|
name: 'Leads',
|
|
icon: 'leads',
|
|
expandable: 'leads',
|
|
children: [
|
|
{ name: 'Alle Leads', path: '/leads', icon: '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' },
|
|
]
|
|
|
|
// Bottom navigation items
|
|
const bottomNavItems = computed(() => {
|
|
const items = []
|
|
if (auth.user?.role === 'owner' || auth.user?.role === 'admin') {
|
|
items.push({ name: 'Team', path: '/team', icon: 'team' })
|
|
}
|
|
items.push({ name: 'Einstellungen', path: '/settings', icon: 'settings' })
|
|
return items
|
|
})
|
|
|
|
function checkMobile() {
|
|
isMobile.value = window.innerWidth < 768
|
|
if (!isMobile.value) {
|
|
sidebarOpen.value = true
|
|
} else {
|
|
sidebarOpen.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', checkMobile)
|
|
})
|
|
|
|
function toggleSidebar() {
|
|
sidebarOpen.value = !sidebarOpen.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() {
|
|
if (isMobile.value) {
|
|
sidebarOpen.value = false
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
auth.logout()
|
|
router.push('/login')
|
|
}
|
|
|
|
function isActive(path) {
|
|
if (path === '/') return route.path === '/'
|
|
return route.path.startsWith(path)
|
|
}
|
|
|
|
function isGroupActive(item) {
|
|
if (!item.children) return false
|
|
return item.children.some(child => route.path.startsWith(child.path))
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex h-full relative">
|
|
<!-- Mobile Overlay -->
|
|
<div
|
|
v-if="isMobile && sidebarOpen"
|
|
class="fixed inset-0 bg-black/50 z-40"
|
|
@click="closeSidebarOnMobile"
|
|
></div>
|
|
|
|
<!-- Mobile Header -->
|
|
<header
|
|
v-if="isMobile"
|
|
class="fixed top-0 left-0 right-0 h-14 bg-pulse-card border-b border-pulse-border flex items-center px-4 z-30"
|
|
>
|
|
<button
|
|
@click="toggleSidebar"
|
|
class="p-2 text-white hover:bg-pulse-border rounded-lg transition-colors"
|
|
>
|
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
<div class="flex items-center gap-2 ml-3">
|
|
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
|
|
<svg class="w-5 h-5 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>
|
|
</div>
|
|
<span class="text-lg font-bold text-white">Pulse</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Sidebar -->
|
|
<aside
|
|
:class="[
|
|
'flex flex-col bg-pulse-card border-r border-pulse-border transition-all duration-300',
|
|
isMobile ? 'fixed top-0 left-0 h-full z-50' : '',
|
|
isMobile && !sidebarOpen ? '-translate-x-full' : 'translate-x-0',
|
|
'w-64'
|
|
]"
|
|
>
|
|
<!-- 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>
|
|
<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
|
|
v-if="isMobile"
|
|
@click="closeSidebarOnMobile"
|
|
class="p-2 text-pulse-muted hover:text-white transition-colors"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 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.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 === '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 === '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 === '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 === '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>
|
|
<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>
|
|
|
|
<!-- Expandable menu -->
|
|
<div v-else class="space-y-0.5">
|
|
<button
|
|
@click="toggleExpand(item.expandable)"
|
|
:class="['sidebar-link w-full justify-between', isGroupActive(item) && 'active']"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<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': 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="isExpanded(item.expandable)"
|
|
class="pl-4 space-y-0.5"
|
|
>
|
|
<RouterLink
|
|
v-for="child in item.children"
|
|
:key="child.path"
|
|
: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 === '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 === '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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</nav>
|
|
|
|
<!-- 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="sidebar-link w-full text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
|
>
|
|
<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>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main :class="['flex-1 overflow-auto', isMobile ? 'pt-14' : '']">
|
|
<RouterView />
|
|
</main>
|
|
</div>
|
|
</template>
|