feat: Full responsive design - mobile sidebar, card lists, modals
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, 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(true)
|
||||
const sidebarOpen = ref(false)
|
||||
const isMobile = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', path: '/', icon: 'chart-pie' },
|
||||
@@ -16,6 +17,34 @@ const navItems = [
|
||||
{ name: 'Aktivitäten', path: '/activities', icon: 'clipboard-list' },
|
||||
]
|
||||
|
||||
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 closeSidebarOnMobile() {
|
||||
if (isMobile.value) {
|
||||
sidebarOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
@@ -28,60 +57,105 @@ function isActive(path) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<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',
|
||||
sidebarOpen ? 'w-64' : 'w-20'
|
||||
isMobile ? 'fixed top-0 left-0 h-full z-50' : '',
|
||||
isMobile && !sidebarOpen ? '-translate-x-full' : 'translate-x-0',
|
||||
'w-64'
|
||||
]"
|
||||
>
|
||||
<!-- 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 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>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-white">Pulse</span>
|
||||
</div>
|
||||
<span v-if="sidebarOpen" class="text-xl font-bold text-white">Pulse</span>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 py-4 px-3 space-y-1">
|
||||
<nav class="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:class="['sidebar-link', isActive(item.path) && 'active']"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<!-- Icons -->
|
||||
<svg v-if="item.icon === 'chart-pie'" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<svg v-if="item.icon === 'users'" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg v-if="item.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>
|
||||
<svg v-if="item.icon === 'building-office'" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg v-if="item.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>
|
||||
<svg v-if="item.icon === 'funnel'" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<svg v-if="item.icon === 'clipboard-list'" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg v-if="item.icon === 'clipboard-list'" 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>
|
||||
<span v-if="sidebarOpen">{{ item.name }}</span>
|
||||
<span>{{ 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">
|
||||
<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 v-if="sidebarOpen" class="flex-1 min-w-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-white truncate">
|
||||
{{ auth.user?.firstName }} {{ auth.user?.lastName }}
|
||||
</p>
|
||||
@@ -89,7 +163,7 @@ function isActive(path) {
|
||||
</div>
|
||||
<button
|
||||
@click="logout"
|
||||
class="p-2 text-pulse-muted hover:text-white transition-colors"
|
||||
class="p-2 text-pulse-muted hover:text-white transition-colors flex-shrink-0"
|
||||
title="Abmelden"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -101,7 +175,7 @@ function isActive(path) {
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
<main :class="['flex-1 overflow-auto', isMobile ? 'pt-14' : '']">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -62,14 +62,14 @@ const typeLabels = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Aktivitäten</h1>
|
||||
<p class="text-pulse-muted">Ihre Aufgaben und Aktivitäten</p>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-white">Aktivitäten</h1>
|
||||
<p class="text-pulse-muted text-sm">Ihre Aufgaben und Aktivitäten</p>
|
||||
</div>
|
||||
<button @click="showNewModal = true" class="btn-primary">
|
||||
<button @click="showNewModal = true" class="btn-primary w-full sm:w-auto">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -78,42 +78,42 @@ const typeLabels = {
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="card p-4">
|
||||
<p class="text-2xl font-bold text-white">{{ activities.stats?.totalToday || 0 }}</p>
|
||||
<p class="text-sm text-pulse-muted">Heute fällig</p>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
|
||||
<div class="card p-3 sm:p-4">
|
||||
<p class="text-xl sm:text-2xl font-bold text-white">{{ activities.stats?.totalToday || 0 }}</p>
|
||||
<p class="text-xs sm:text-sm text-pulse-muted">Heute fällig</p>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<p class="text-2xl font-bold text-green-400">{{ activities.stats?.completedToday || 0 }}</p>
|
||||
<p class="text-sm text-pulse-muted">Heute erledigt</p>
|
||||
<div class="card p-3 sm:p-4">
|
||||
<p class="text-xl sm:text-2xl font-bold text-green-400">{{ activities.stats?.completedToday || 0 }}</p>
|
||||
<p class="text-xs sm:text-sm text-pulse-muted">Heute erledigt</p>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<p class="text-2xl font-bold text-red-400">{{ activities.stats?.overdue || 0 }}</p>
|
||||
<p class="text-sm text-pulse-muted">Überfällig</p>
|
||||
<div class="card p-3 sm:p-4">
|
||||
<p class="text-xl sm:text-2xl font-bold text-red-400">{{ activities.stats?.overdue || 0 }}</p>
|
||||
<p class="text-xs sm:text-sm text-pulse-muted">Überfällig</p>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<p class="text-2xl font-bold text-blue-400">{{ activities.stats?.upcoming || 0 }}</p>
|
||||
<p class="text-sm text-pulse-muted">Diese Woche</p>
|
||||
<div class="card p-3 sm:p-4">
|
||||
<p class="text-xl sm:text-2xl font-bold text-blue-400">{{ activities.stats?.upcoming || 0 }}</p>
|
||||
<p class="text-xs sm:text-sm text-pulse-muted">Diese Woche</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<div class="flex gap-2 mb-6 overflow-x-auto pb-2 -mx-4 px-4 sm:mx-0 sm:px-0">
|
||||
<button
|
||||
@click="filter = 'all'; activities.fetchActivities()"
|
||||
:class="['btn-sm', filter === 'all' ? 'btn-primary' : 'btn-ghost']"
|
||||
:class="['btn-sm whitespace-nowrap', filter === 'all' ? 'btn-primary' : 'btn-ghost']"
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
@click="filter = 'upcoming'; activities.fetchActivities({ isCompleted: 'false' })"
|
||||
:class="['btn-sm', filter === 'upcoming' ? 'btn-primary' : 'btn-ghost']"
|
||||
:class="['btn-sm whitespace-nowrap', filter === 'upcoming' ? 'btn-primary' : 'btn-ghost']"
|
||||
>
|
||||
Offen
|
||||
</button>
|
||||
<button
|
||||
@click="filter = 'completed'; activities.fetchActivities({ isCompleted: 'true' })"
|
||||
:class="['btn-sm', filter === 'completed' ? 'btn-primary' : 'btn-ghost']"
|
||||
:class="['btn-sm whitespace-nowrap', filter === 'completed' ? 'btn-primary' : 'btn-ghost']"
|
||||
>
|
||||
Erledigt
|
||||
</button>
|
||||
@@ -124,13 +124,13 @@ const typeLabels = {
|
||||
<div
|
||||
v-for="activity in activities.activities"
|
||||
:key="activity.id"
|
||||
class="p-4 flex items-center gap-4 hover:bg-pulse-dark/30 transition-colors"
|
||||
class="p-3 sm:p-4 flex items-start sm:items-center gap-3 sm:gap-4 hover:bg-pulse-dark/30 transition-colors"
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<button
|
||||
@click.stop="completeActivity(activity.id)"
|
||||
:class="[
|
||||
'w-6 h-6 rounded-full border-2 flex items-center justify-center transition-colors',
|
||||
'w-6 h-6 rounded-full border-2 flex items-center justify-center transition-colors flex-shrink-0 mt-0.5 sm:mt-0',
|
||||
activity.isCompleted
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
: 'border-pulse-border hover:border-primary-500'
|
||||
@@ -141,26 +141,41 @@ const typeLabels = {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Type Icon -->
|
||||
<span class="text-xl">{{ typeIcons[activity.type] }}</span>
|
||||
<!-- Type Icon (hidden on very small screens) -->
|
||||
<span class="text-xl hidden sm:block">{{ typeIcons[activity.type] }}</span>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p :class="['font-medium', activity.isCompleted ? 'text-pulse-muted line-through' : 'text-white']">
|
||||
{{ activity.subject }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 text-sm text-pulse-muted">
|
||||
<div class="flex items-start sm:items-center gap-2">
|
||||
<span class="text-base sm:hidden">{{ typeIcons[activity.type] }}</span>
|
||||
<p :class="['font-medium text-sm sm:text-base', activity.isCompleted ? 'text-pulse-muted line-through' : 'text-white']">
|
||||
{{ activity.subject }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1 sm:gap-2 text-xs sm:text-sm text-pulse-muted mt-1">
|
||||
<span>{{ typeLabels[activity.type] }}</span>
|
||||
<span v-if="activity.contact">· {{ activity.contact.name }}</span>
|
||||
<span v-if="activity.company">· {{ activity.company.name }}</span>
|
||||
</div>
|
||||
<!-- Due date on mobile -->
|
||||
<span
|
||||
v-if="activity.dueDate"
|
||||
:class="[
|
||||
'text-xs mt-1 inline-block sm:hidden',
|
||||
new Date(activity.dueDate) < new Date() && !activity.isCompleted
|
||||
? 'text-red-400'
|
||||
: 'text-pulse-muted'
|
||||
]"
|
||||
>
|
||||
📅 {{ formatDate(activity.dueDate) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<!-- Due Date (desktop) -->
|
||||
<span
|
||||
v-if="activity.dueDate"
|
||||
:class="[
|
||||
'text-sm',
|
||||
'text-sm hidden sm:block flex-shrink-0',
|
||||
new Date(activity.dueDate) < new Date() && !activity.isCompleted
|
||||
? 'text-red-400'
|
||||
: 'text-pulse-muted'
|
||||
@@ -171,20 +186,27 @@ const typeLabels = {
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!activities.loading && !activities.activities.length" class="p-12 text-center">
|
||||
<div v-if="!activities.loading && !activities.activities.length" class="p-8 sm:p-12 text-center">
|
||||
<p class="text-pulse-muted">Keine Aktivitäten gefunden</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Activity Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||
<div class="absolute inset-0 bg-black/60" @click="showNewModal = false"></div>
|
||||
<div class="relative card w-full max-w-md mx-4">
|
||||
<div class="px-6 py-4 border-b border-pulse-border">
|
||||
<h2 class="text-lg font-semibold text-white">Neue Aktivität</h2>
|
||||
<div class="relative card w-full sm:max-w-md rounded-b-none sm:rounded-b-xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="px-4 sm:px-6 py-4 border-b border-pulse-border sticky top-0 bg-pulse-card z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Neue Aktivität</h2>
|
||||
<button @click="showNewModal = false" class="p-2 text-pulse-muted hover:text-white sm:hidden">
|
||||
<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>
|
||||
</div>
|
||||
<form @submit.prevent="createActivity" class="p-6 space-y-4">
|
||||
<form @submit.prevent="createActivity" class="p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<label class="label">Typ</label>
|
||||
<select v-model="newActivity.type" class="input">
|
||||
@@ -203,7 +225,7 @@ const typeLabels = {
|
||||
<label class="label">Fällig am</label>
|
||||
<input v-model="newActivity.dueDate" type="datetime-local" class="input" />
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 pt-2">
|
||||
<button type="button" @click="showNewModal = false" class="btn-secondary flex-1">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
@@ -54,14 +54,14 @@ async function createCompany() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Firmen</h1>
|
||||
<p class="text-pulse-muted">{{ meta.total }} Firmen</p>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-white">Firmen</h1>
|
||||
<p class="text-pulse-muted text-sm">{{ meta.total }} Firmen</p>
|
||||
</div>
|
||||
<button @click="showNewModal = true" class="btn-primary">
|
||||
<button @click="showNewModal = true" class="btn-primary w-full sm:w-auto">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -71,48 +71,51 @@ async function createCompany() {
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-6">
|
||||
<div class="relative max-w-md">
|
||||
<div class="relative w-full sm:max-w-md">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-pulse-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="input pl-10"
|
||||
class="input pl-10 w-full"
|
||||
placeholder="Suchen..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div
|
||||
v-for="company in companies"
|
||||
:key="company.id"
|
||||
class="card p-5 cursor-pointer hover:border-primary-500 transition-colors"
|
||||
class="card p-4 sm:p-5 cursor-pointer hover:border-primary-500 active:scale-[0.98] transition-all"
|
||||
@click="router.push(`/companies/${company.id}`)"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-purple-600/20 flex items-center justify-center text-purple-400 font-bold text-lg">
|
||||
<div class="flex items-start gap-3 sm:gap-4">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-purple-600/20 flex items-center justify-center text-purple-400 font-bold text-base sm:text-lg flex-shrink-0">
|
||||
{{ company.name?.[0]?.toUpperCase() }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-white truncate">{{ company.name }}</h3>
|
||||
<p v-if="company.industry" class="text-sm text-pulse-muted">{{ company.industry }}</p>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span v-if="company.size" class="badge-gray">{{ company.size }}</span>
|
||||
<p v-if="company.industry" class="text-sm text-pulse-muted truncate">{{ company.industry }}</p>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-2">
|
||||
<span v-if="company.size" class="badge-gray text-xs">{{ company.size }}</span>
|
||||
<span v-if="company.contactCount" class="text-xs text-pulse-muted">
|
||||
{{ company.contactCount }} Kontakte
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-pulse-muted flex-shrink-0 hidden sm:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && !companies.length" class="card p-12 text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-pulse-muted opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div v-if="!loading && !companies.length" class="card p-8 sm:p-12 text-center">
|
||||
<svg class="w-12 sm:w-16 h-12 sm:h-16 mx-auto mb-4 text-pulse-muted opacity-50" 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>
|
||||
<p class="text-pulse-muted">Noch keine Firmen vorhanden</p>
|
||||
@@ -123,13 +126,20 @@ async function createCompany() {
|
||||
|
||||
<!-- New Company Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||
<div class="absolute inset-0 bg-black/60" @click="showNewModal = false"></div>
|
||||
<div class="relative card w-full max-w-lg mx-4">
|
||||
<div class="px-6 py-4 border-b border-pulse-border">
|
||||
<h2 class="text-lg font-semibold text-white">Neue Firma</h2>
|
||||
<div class="relative card w-full sm:max-w-lg rounded-b-none sm:rounded-b-xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="px-4 sm:px-6 py-4 border-b border-pulse-border sticky top-0 bg-pulse-card z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Neue Firma</h2>
|
||||
<button @click="showNewModal = false" class="p-2 text-pulse-muted hover:text-white sm:hidden">
|
||||
<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>
|
||||
</div>
|
||||
<form @submit.prevent="createCompany" class="p-6 space-y-4">
|
||||
<form @submit.prevent="createCompany" class="p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<label class="label">Firmenname *</label>
|
||||
<input v-model="newCompany.name" type="text" class="input" required />
|
||||
@@ -153,7 +163,7 @@ async function createCompany() {
|
||||
<option value="1000+">1000+ Mitarbeiter</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 pt-2">
|
||||
<button type="button" @click="showNewModal = false" class="btn-secondary flex-1">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
@@ -46,14 +46,14 @@ function getInitials(contact) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Kontakte</h1>
|
||||
<p class="text-pulse-muted">{{ contacts.meta.total }} Kontakte</p>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-white">Kontakte</h1>
|
||||
<p class="text-pulse-muted text-sm">{{ contacts.meta.total }} Kontakte</p>
|
||||
</div>
|
||||
<button @click="showNewModal = true" class="btn-primary">
|
||||
<button @click="showNewModal = true" class="btn-primary w-full sm:w-auto">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -63,21 +63,21 @@ function getInitials(contact) {
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-6">
|
||||
<div class="relative max-w-md">
|
||||
<div class="relative w-full sm:max-w-md">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-pulse-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="input pl-10"
|
||||
class="input pl-10 w-full"
|
||||
placeholder="Suchen..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<!-- Desktop Table -->
|
||||
<div class="card hidden md:block">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -98,7 +98,7 @@ function getInitials(contact) {
|
||||
>
|
||||
<td>
|
||||
<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 text-sm font-medium">
|
||||
<div class="w-9 h-9 rounded-full bg-primary-600 flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
|
||||
{{ getInitials(contact) }}
|
||||
</div>
|
||||
<div>
|
||||
@@ -117,29 +117,84 @@ function getInitials(contact) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!contacts.loading && !contacts.contacts.length" class="p-12 text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-pulse-muted opacity-50" 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>
|
||||
<p class="text-pulse-muted">Noch keine Kontakte vorhanden</p>
|
||||
<button @click="showNewModal = true" class="btn-primary mt-4">
|
||||
Ersten Kontakt anlegen
|
||||
</button>
|
||||
<!-- Mobile Card List -->
|
||||
<div class="md:hidden space-y-3">
|
||||
<div
|
||||
v-for="contact in contacts.contacts"
|
||||
:key="contact.id"
|
||||
class="card p-4 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
@click="router.push(`/contacts/${contact.id}`)"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-primary-600 flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
|
||||
{{ getInitials(contact) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-white">{{ contact.firstName }} {{ contact.lastName }}</p>
|
||||
<p v-if="contact.position" class="text-sm text-pulse-muted">{{ contact.position }}</p>
|
||||
<p v-if="contact.company" class="text-sm text-primary-400">{{ contact.company.name }}</p>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<a
|
||||
v-if="contact.email"
|
||||
:href="`mailto:${contact.email}`"
|
||||
@click.stop
|
||||
class="inline-flex items-center gap-1 text-xs text-pulse-muted hover:text-white px-2 py-1 bg-pulse-dark rounded"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{{ contact.email }}
|
||||
</a>
|
||||
<a
|
||||
v-if="contact.phone"
|
||||
:href="`tel:${contact.phone}`"
|
||||
@click.stop
|
||||
class="inline-flex items-center gap-1 text-xs text-pulse-muted hover:text-white px-2 py-1 bg-pulse-dark rounded"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
{{ contact.phone }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-pulse-muted flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!contacts.loading && !contacts.contacts.length" class="card p-8 sm:p-12 text-center">
|
||||
<svg class="w-12 sm:w-16 h-12 sm:h-16 mx-auto mb-4 text-pulse-muted opacity-50" 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>
|
||||
<p class="text-pulse-muted">Noch keine Kontakte vorhanden</p>
|
||||
<button @click="showNewModal = true" class="btn-primary mt-4">
|
||||
Ersten Kontakt anlegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New Contact Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||
<div class="absolute inset-0 bg-black/60" @click="showNewModal = false"></div>
|
||||
<div class="relative card w-full max-w-lg mx-4">
|
||||
<div class="px-6 py-4 border-b border-pulse-border">
|
||||
<h2 class="text-lg font-semibold text-white">Neuer Kontakt</h2>
|
||||
<div class="relative card w-full sm:max-w-lg rounded-b-none sm:rounded-b-xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="px-4 sm:px-6 py-4 border-b border-pulse-border sticky top-0 bg-pulse-card z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Neuer Kontakt</h2>
|
||||
<button @click="showNewModal = false" class="p-2 text-pulse-muted hover:text-white sm:hidden">
|
||||
<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>
|
||||
</div>
|
||||
<form @submit.prevent="createContact" class="p-6 space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<form @submit.prevent="createContact" class="p-4 sm:p-6 space-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Vorname *</label>
|
||||
<input v-model="newContact.firstName" type="text" class="input" required />
|
||||
@@ -161,7 +216,7 @@ function getInitials(contact) {
|
||||
<label class="label">Position</label>
|
||||
<input v-model="newContact.position" type="text" class="input" placeholder="z.B. Geschäftsführer" />
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 pt-2">
|
||||
<button type="button" @click="showNewModal = false" class="btn-secondary flex-1">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDealsStore } from '@/stores/deals'
|
||||
const deals = useDealsStore()
|
||||
const showNewDealModal = ref(false)
|
||||
const draggedDeal = ref(null)
|
||||
const mobileSelectedStage = ref(null)
|
||||
|
||||
const newDeal = ref({
|
||||
title: '',
|
||||
@@ -18,6 +19,10 @@ onMounted(async () => {
|
||||
await deals.fetchPipelines()
|
||||
if (deals.currentPipeline) {
|
||||
await deals.fetchKanban(deals.currentPipeline.id)
|
||||
// Set first stage as default for mobile
|
||||
if (deals.kanbanData.stages?.length) {
|
||||
mobileSelectedStage.value = deals.kanbanData.stages[0].id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,17 +78,21 @@ function calculateStageValue(stageId) {
|
||||
const stageDeals = deals.kanbanData.deals[stageId] || []
|
||||
return stageDeals.reduce((sum, d) => sum + (d.value || 0), 0)
|
||||
}
|
||||
|
||||
function getStageDealsCount(stageId) {
|
||||
return (deals.kanbanData.deals[stageId] || []).length
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-pulse-border bg-pulse-card">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-4 sm:px-6 py-4 border-b border-pulse-border bg-pulse-card">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-white">Sales Pipeline</h1>
|
||||
<p class="text-sm text-pulse-muted">{{ deals.currentPipeline?.name || 'Loading...' }}</p>
|
||||
</div>
|
||||
<button @click="showNewDealModal = true" class="btn-primary">
|
||||
<button @click="showNewDealModal = true" class="btn-primary w-full sm:w-auto">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -91,16 +100,97 @@ function calculateStageValue(stageId) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Kanban Board -->
|
||||
<div class="flex-1 overflow-x-auto p-6">
|
||||
<div v-if="deals.loading" class="flex items-center justify-center h-64">
|
||||
<svg class="animate-spin h-8 w-8 text-primary-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Loading -->
|
||||
<div v-if="deals.loading" class="flex-1 flex items-center justify-center">
|
||||
<svg class="animate-spin h-8 w-8 text-primary-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex gap-4 h-full min-w-max">
|
||||
<!-- Mobile Stage Tabs -->
|
||||
<div v-if="!deals.loading" class="md:hidden border-b border-pulse-border bg-pulse-card">
|
||||
<div class="flex overflow-x-auto px-4 py-2 gap-2 -mx-4">
|
||||
<button
|
||||
v-for="(stage, index) in deals.kanbanData.stages"
|
||||
:key="stage.id"
|
||||
@click="mobileSelectedStage = stage.id"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg whitespace-nowrap transition-colors flex-shrink-0',
|
||||
mobileSelectedStage === stage.id
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-pulse-dark text-pulse-muted'
|
||||
]"
|
||||
>
|
||||
<div :class="['w-2 h-2 rounded-full', getStageColor(index, deals.kanbanData.stages.length)]"></div>
|
||||
<span class="text-sm font-medium">{{ stage.name }}</span>
|
||||
<span class="text-xs bg-black/20 px-1.5 py-0.5 rounded">
|
||||
{{ getStageDealsCount(stage.id) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Deals List -->
|
||||
<div v-if="!deals.loading" class="md:hidden flex-1 overflow-y-auto p-4">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<p class="text-sm text-pulse-muted">
|
||||
{{ formatCurrency(calculateStageValue(mobileSelectedStage)) }} in dieser Phase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="deal in deals.kanbanData.deals[mobileSelectedStage] || []"
|
||||
:key="deal.id"
|
||||
class="card p-4 active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h4 class="font-medium text-white">{{ deal.title }}</h4>
|
||||
<span class="text-sm font-semibold text-green-400">
|
||||
{{ formatCurrency(deal.value) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deal.company" class="text-sm text-pulse-muted mb-1">
|
||||
🏢 {{ deal.company.name }}
|
||||
</div>
|
||||
|
||||
<div v-if="deal.contact" class="text-sm text-pulse-muted mb-2">
|
||||
👤 {{ deal.contact.name }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span v-if="deal.expectedCloseDate" class="text-pulse-muted">
|
||||
📅 {{ new Date(deal.expectedCloseDate).toLocaleDateString('de-DE') }}
|
||||
</span>
|
||||
<span v-if="deal.probability" :class="[
|
||||
'px-2 py-0.5 rounded-full',
|
||||
deal.probability >= 70 ? 'bg-green-500/20 text-green-400' :
|
||||
deal.probability >= 40 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-red-500/20 text-red-400'
|
||||
]">
|
||||
{{ deal.probability }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-if="!(deals.kanbanData.deals[mobileSelectedStage] || []).length"
|
||||
class="flex flex-col items-center justify-center py-12 text-pulse-muted"
|
||||
>
|
||||
<svg class="w-12 h-12 mb-3 opacity-50" 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>
|
||||
<p>Keine Deals in dieser Phase</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Kanban Board -->
|
||||
<div v-if="!deals.loading" class="hidden md:block flex-1 overflow-x-auto p-6">
|
||||
<div class="flex gap-4 h-full min-w-max">
|
||||
<div
|
||||
v-for="(stage, index) in deals.kanbanData.stages"
|
||||
:key="stage.id"
|
||||
@@ -114,7 +204,7 @@ function calculateStageValue(stageId) {
|
||||
<div :class="['w-3 h-3 rounded-full', getStageColor(index, deals.kanbanData.stages.length)]"></div>
|
||||
<h3 class="font-semibold text-white">{{ stage.name }}</h3>
|
||||
<span class="text-xs text-pulse-muted bg-pulse-card px-2 py-0.5 rounded-full">
|
||||
{{ (deals.kanbanData.deals[stage.id] || []).length }}
|
||||
{{ getStageDealsCount(stage.id) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-pulse-muted">
|
||||
@@ -178,13 +268,20 @@ function calculateStageValue(stageId) {
|
||||
|
||||
<!-- New Deal Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showNewDealModal" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div v-if="showNewDealModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||
<div class="absolute inset-0 bg-black/60" @click="showNewDealModal = false"></div>
|
||||
<div class="relative card w-full max-w-md mx-4">
|
||||
<div class="px-6 py-4 border-b border-pulse-border">
|
||||
<h2 class="text-lg font-semibold text-white">Neuer Deal</h2>
|
||||
<div class="relative card w-full sm:max-w-md rounded-b-none sm:rounded-b-xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="px-4 sm:px-6 py-4 border-b border-pulse-border sticky top-0 bg-pulse-card z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Neuer Deal</h2>
|
||||
<button @click="showNewDealModal = false" class="p-2 text-pulse-muted hover:text-white sm:hidden">
|
||||
<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>
|
||||
</div>
|
||||
<form @submit.prevent="createDeal" class="p-6 space-y-4">
|
||||
<form @submit.prevent="createDeal" class="p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<label class="label">Titel *</label>
|
||||
<input v-model="newDeal.title" type="text" class="input" placeholder="z.B. Website Redesign" required />
|
||||
@@ -197,7 +294,7 @@ function calculateStageValue(stageId) {
|
||||
<label class="label">Erwarteter Abschluss</label>
|
||||
<input v-model="newDeal.expectedCloseDate" type="date" class="input" />
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 pt-2">
|
||||
<button type="button" @click="showNewDealModal = false" class="btn-secondary flex-1">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user