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:
177
src/views/ContactsView.vue
Normal file
177
src/views/ContactsView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user