Views added: - ShiftsView (Schichtplanung) - PatrolsView (Wächterkontrolle) - IncidentsView (Vorfallberichte) - VehiclesView (Fahrzeuge) - DocumentsView (Dokumente) - CustomersView (Kunden/CRM) - BillingView (Abrechnung) - ObjectsView (enhanced with contacts, instructions) Updated: - Router with all new routes - Sidebar with complete navigation
178 lines
6.9 KiB
Vue
178 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { api } from '@/api'
|
|
|
|
const loading = ref(true)
|
|
const customers = ref<any[]>([])
|
|
const showModal = ref(false)
|
|
const selectedCustomer = ref<any>(null)
|
|
const form = ref({ company_name: '', contact_person: '', email: '', phone: '', address: '', city: '', postal_code: '' })
|
|
|
|
onMounted(async () => { await loadData() })
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const res = await api.get<any>('/customers')
|
|
customers.value = res.data.customers || []
|
|
} catch (e) { console.error(e) }
|
|
loading.value = false
|
|
}
|
|
|
|
async function loadCustomer(id: string) {
|
|
try {
|
|
const res = await api.get<any>(`/customers/${id}`)
|
|
selectedCustomer.value = res.data
|
|
} catch (e) { console.error(e) }
|
|
}
|
|
|
|
async function createCustomer() {
|
|
try {
|
|
await api.post('/customers', form.value)
|
|
showModal.value = false
|
|
form.value = { company_name: '', contact_person: '', email: '', phone: '', address: '', city: '', postal_code: '' }
|
|
await loadData()
|
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
|
}
|
|
|
|
function statusBadge(s: string) {
|
|
const map: Record<string, any> = {
|
|
active: { text: 'Aktiv', class: 'bg-green-100 text-green-700' },
|
|
inactive: { text: 'Inaktiv', class: 'bg-gray-100 text-gray-700' },
|
|
prospect: { text: 'Interessent', class: 'bg-blue-100 text-blue-700' }
|
|
}
|
|
return map[s] || { text: s, class: 'bg-gray-100' }
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-6">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">👥 Kunden / CRM</h1>
|
|
<p class="text-gray-500">Kundenverwaltung & Verträge</p>
|
|
</div>
|
|
<button @click="showModal = true" class="btn btn-primary">+ Kunde</button>
|
|
</div>
|
|
|
|
<div class="flex gap-6">
|
|
<!-- Customer List -->
|
|
<div class="w-1/2">
|
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
|
|
|
<div v-else-if="customers.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
|
<p class="text-4xl mb-4">👥</p>
|
|
<p>Keine Kunden vorhanden</p>
|
|
</div>
|
|
|
|
<div v-else class="space-y-2">
|
|
<div v-for="c in customers" :key="c.id"
|
|
@click="loadCustomer(c.id)"
|
|
:class="['bg-white rounded-lg shadow p-4 cursor-pointer hover:shadow-md transition-shadow',
|
|
selectedCustomer?.id === c.id ? 'ring-2 ring-blue-500' : '']">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="font-semibold">{{ c.company_name }}</h3>
|
|
<p class="text-sm text-gray-500">{{ c.contact_person }}</p>
|
|
</div>
|
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(c.status).class]">
|
|
{{ statusBadge(c.status).text }}
|
|
</span>
|
|
</div>
|
|
<div class="mt-2 flex gap-4 text-xs text-gray-500">
|
|
<span v-if="c.active_contracts">📄 {{ c.active_contracts }} Verträge</span>
|
|
<span v-if="c.object_count">🏢 {{ c.object_count }} Objekte</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Detail -->
|
|
<div class="w-1/2">
|
|
<div v-if="!selectedCustomer" class="bg-gray-50 rounded-lg p-12 text-center text-gray-400">
|
|
<p>Wähle einen Kunden aus</p>
|
|
</div>
|
|
|
|
<div v-else class="bg-white rounded-lg shadow p-6">
|
|
<h2 class="text-xl font-bold mb-4">{{ selectedCustomer.company_name }}</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Kontakt</h3>
|
|
<p>{{ selectedCustomer.contact_person }}</p>
|
|
<p class="text-sm">📧 {{ selectedCustomer.email || '-' }}</p>
|
|
<p class="text-sm">📞 {{ selectedCustomer.phone || '-' }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Adresse</h3>
|
|
<p>{{ selectedCustomer.address }}</p>
|
|
<p>{{ selectedCustomer.postal_code }} {{ selectedCustomer.city }}</p>
|
|
</div>
|
|
|
|
<div v-if="selectedCustomer.contracts?.length">
|
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Verträge ({{ selectedCustomer.contracts.length }})</h3>
|
|
<div v-for="con in selectedCustomer.contracts" :key="con.id" class="text-sm p-2 bg-gray-50 rounded mb-1">
|
|
<span class="font-medium">{{ con.title || con.contract_number }}</span>
|
|
<span v-if="con.monthly_value" class="ml-2 text-green-600">{{ con.monthly_value }}€/Monat</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="selectedCustomer.objects?.length">
|
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Objekte ({{ selectedCustomer.objects.length }})</h3>
|
|
<div v-for="obj in selectedCustomer.objects" :key="obj.id" class="text-sm">
|
|
🏢 {{ obj.name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal -->
|
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6">
|
|
<h2 class="text-xl font-bold mb-4">Neuer Kunde</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Firmenname *</label>
|
|
<input v-model="form.company_name" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Ansprechpartner</label>
|
|
<input v-model="form.contact_person" class="input" />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
|
<input v-model="form.email" type="email" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Telefon</label>
|
|
<input v-model="form.phone" class="input" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Adresse</label>
|
|
<input v-model="form.address" class="input" />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">PLZ</label>
|
|
<input v-model="form.postal_code" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Stadt</label>
|
|
<input v-model="form.city" class="input" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-2 mt-6">
|
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
|
<button @click="createCustomer" class="btn btn-primary">Erstellen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|