feat: Pulse CRM Frontend v1.0

Vue 3 + Vite + Tailwind CSS

Views:
- Dashboard mit Stats & Aktivitäten
- Kontakte mit Suche & CRUD
- Firmen mit Cards & CRUD
- Pipeline Kanban Board (Drag & Drop)
- Aktivitäten mit Filter & Timeline
- Settings mit DSGVO-Info

Features:
- Dark Theme (Pulse Design)
- Responsive Layout
- Pinia State Management
- Vue Router mit Guards
- Axios API Client

Task: #13 Frontend UI
This commit is contained in:
FluxKit
2026-02-11 11:24:04 +00:00
commit 01d542b6b6
29 changed files with 2609 additions and 0 deletions

108
src/layouts/AppLayout.vue Normal file
View File

@@ -0,0 +1,108 @@
<script setup>
import { ref } 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(true)
const navItems = [
{ name: 'Dashboard', path: '/', icon: 'chart-pie' },
{ name: 'Kontakte', path: '/contacts', icon: 'users' },
{ name: 'Firmen', path: '/companies', icon: 'building-office' },
{ name: 'Pipeline', path: '/pipeline', icon: 'funnel' },
{ name: 'Aktivitäten', path: '/activities', icon: 'clipboard-list' },
]
function logout() {
auth.logout()
router.push('/login')
}
function isActive(path) {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
</script>
<template>
<div class="flex h-full">
<!-- Sidebar -->
<aside
:class="[
'flex flex-col bg-pulse-card border-r border-pulse-border transition-all duration-300',
sidebarOpen ? 'w-64' : 'w-20'
]"
>
<!-- Logo -->
<div class="flex items-center gap-3 px-6 py-5 border-b border-pulse-border">
<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>
</div>
<span v-if="sidebarOpen" class="text-xl font-bold text-white">Pulse</span>
</div>
<!-- Navigation -->
<nav class="flex-1 py-4 px-3 space-y-1">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
:class="['sidebar-link', isActive(item.path) && 'active']"
>
<!-- Icons -->
<svg v-if="item.icon === 'chart-pie'" 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="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>
<svg v-if="item.icon === 'users'" 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="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>
<svg v-if="item.icon === 'building-office'" 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="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 === 'funnel'" 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="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>
<svg v-if="item.icon === 'clipboard-list'" 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="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>
<span v-if="sidebarOpen">{{ item.name }}</span>
</RouterLink>
</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">
{{ auth.user?.firstName?.[0] || 'U' }}
</div>
<div v-if="sidebarOpen" 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>
<button
@click="logout"
class="p-2 text-pulse-muted hover:text-white transition-colors"
title="Abmelden"
>
<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="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>
</button>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-auto">
<RouterView />
</main>
</div>
</template>