233 lines
9.6 KiB
Vue
233 lines
9.6 KiB
Vue
<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-4 sm:p-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
|
<div>
|
|
<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 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>
|
|
Neuer Kontakt
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="mb-6">
|
|
<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 w-full"
|
|
placeholder="Suchen..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop Table -->
|
|
<div class="card hidden md:block">
|
|
<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 flex-shrink-0">
|
|
{{ 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>
|
|
</div>
|
|
|
|
<!-- 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-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 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-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 />
|
|
</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 flex-col-reverse sm:flex-row 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>
|