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

177
src/views/ContactsView.vue Normal file
View File

@@ -0,0 +1,177 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useContactsStore } from '@/stores/contacts'
import { useDebounceFn } from '@vueuse/core'
const router = useRouter()
const contacts = useContactsStore()
const search = ref('')
const showNewModal = ref(false)
const newContact = ref({
firstName: '',
lastName: '',
email: '',
phone: '',
position: ''
})
onMounted(() => {
contacts.fetchContacts()
})
const debouncedSearch = useDebounceFn(() => {
contacts.fetchContacts({ search: search.value })
}, 300)
watch(search, debouncedSearch)
async function createContact() {
if (!newContact.value.firstName || !newContact.value.lastName) return
try {
const contact = await contacts.createContact(newContact.value)
showNewModal.value = false
newContact.value = { firstName: '', lastName: '', email: '', phone: '', position: '' }
router.push(`/contacts/${contact.id}`)
} catch (e) {
console.error('Error:', e)
}
}
function getInitials(contact) {
return (contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')
}
</script>
<template>
<div class="p-6">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-white">Kontakte</h1>
<p class="text-pulse-muted">{{ contacts.meta.total }} Kontakte</p>
</div>
<button @click="showNewModal = true" class="btn-primary">
<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>
Neuer Kontakt
</button>
</div>
<!-- Search -->
<div class="mb-6">
<div class="relative 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"
placeholder="Suchen..."
/>
</div>
</div>
<!-- Table -->
<div class="card">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>Telefon</th>
<th>Firma</th>
<th>Position</th>
</tr>
</thead>
<tbody>
<tr
v-for="contact in contacts.contacts"
:key="contact.id"
class="cursor-pointer"
@click="router.push(`/contacts/${contact.id}`)"
>
<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">
{{ getInitials(contact) }}
</div>
<div>
<p class="font-medium text-white">{{ contact.firstName }} {{ contact.lastName }}</p>
</div>
</div>
</td>
<td class="text-pulse-muted">{{ contact.email || '-' }}</td>
<td class="text-pulse-muted">{{ contact.phone || '-' }}</td>
<td>
<span v-if="contact.company" class="text-pulse-text">{{ contact.company.name }}</span>
<span v-else class="text-pulse-muted">-</span>
</td>
<td class="text-pulse-muted">{{ contact.position || '-' }}</td>
</tr>
</tbody>
</table>
</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>
</div>
</div>
<!-- New Contact Modal -->
<Teleport to="body">
<div v-if="showNewModal" class="fixed inset-0 z-50 flex items-center justify-center">
<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>
<form @submit.prevent="createContact" class="p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">Vorname *</label>
<input v-model="newContact.firstName" type="text" class="input" required />
</div>
<div>
<label class="label">Nachname *</label>
<input v-model="newContact.lastName" type="text" class="input" required />
</div>
</div>
<div>
<label class="label">E-Mail</label>
<input v-model="newContact.email" type="email" class="input" placeholder="name@firma.de" />
</div>
<div>
<label class="label">Telefon</label>
<input v-model="newContact.phone" type="tel" class="input" placeholder="+49 123 456789" />
</div>
<div>
<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">
<button type="button" @click="showNewModal = false" class="btn-secondary flex-1">
Abbrechen
</button>
<button type="submit" class="btn-primary flex-1">
Erstellen
</button>
</div>
</form>
</div>
</div>
</Teleport>
</div>
</template>