Add Objects view with full UI
This commit is contained in:
@@ -28,6 +28,7 @@ const navigation = computed(() => {
|
|||||||
{ name: 'Verfügbarkeit', href: '/availability', icon: '📅' },
|
{ name: 'Verfügbarkeit', href: '/availability', icon: '📅' },
|
||||||
{ name: 'Stundenzettel', href: '/timesheets', icon: '⏱️' },
|
{ name: 'Stundenzettel', href: '/timesheets', icon: '⏱️' },
|
||||||
{ name: 'Qualifikationen', href: '/qualifications', icon: '🎓' },
|
{ name: 'Qualifikationen', href: '/qualifications', icon: '🎓' },
|
||||||
|
{ name: 'Objekte', href: '/objects', icon: '🏢' },
|
||||||
)
|
)
|
||||||
|
|
||||||
if (authStore.isChef) {
|
if (authStore.isChef) {
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const router = createRouter({
|
|||||||
path: 'qualifications',
|
path: 'qualifications',
|
||||||
name: 'qualifications',
|
name: 'qualifications',
|
||||||
component: () => import('@/views/QualificationsView.vue')
|
component: () => import('@/views/QualificationsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'objects',
|
||||||
|
name: 'objects',
|
||||||
|
component: () => import('@/views/ObjectsView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
720
src/views/ObjectsView.vue
Normal file
720
src/views/ObjectsView.vue
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
interface ObjectType {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecObject {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
short_name?: string
|
||||||
|
object_number?: string
|
||||||
|
object_type: string
|
||||||
|
street?: string
|
||||||
|
house_number?: string
|
||||||
|
postal_code?: string
|
||||||
|
city?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
description?: string
|
||||||
|
customer_name?: string
|
||||||
|
status: string
|
||||||
|
contact_count: number
|
||||||
|
checkpoint_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const objects = ref<SecObject[]>([])
|
||||||
|
const objectTypes = ref<ObjectType[]>([])
|
||||||
|
const selectedObject = ref<any>(null)
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const filterType = ref('')
|
||||||
|
const filterStatus = ref('active')
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
const showModal = ref(false)
|
||||||
|
const showDetailModal = ref(false)
|
||||||
|
const editingObject = ref<any>(null)
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
short_name: '',
|
||||||
|
object_number: '',
|
||||||
|
object_type: 'other',
|
||||||
|
street: '',
|
||||||
|
house_number: '',
|
||||||
|
postal_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
|
access_info: '',
|
||||||
|
parking_info: '',
|
||||||
|
customer_name: '',
|
||||||
|
size_sqm: null as number | null,
|
||||||
|
floors: null as number | null
|
||||||
|
})
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// Contact form
|
||||||
|
const showContactModal = ref(false)
|
||||||
|
const contactForm = ref({
|
||||||
|
name: '',
|
||||||
|
role: '',
|
||||||
|
company: '',
|
||||||
|
phone: '',
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
availability: '',
|
||||||
|
is_primary: false,
|
||||||
|
is_emergency: false,
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Instruction form
|
||||||
|
const showInstructionModal = ref(false)
|
||||||
|
const instructionForm = ref({
|
||||||
|
title: '',
|
||||||
|
category: 'general',
|
||||||
|
content: '',
|
||||||
|
is_critical: false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadObjects(),
|
||||||
|
loadObjectTypes()
|
||||||
|
])
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadObjects() {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (filterStatus.value) params.append('status', filterStatus.value)
|
||||||
|
if (filterType.value) params.append('type', filterType.value)
|
||||||
|
if (searchQuery.value) params.append('search', searchQuery.value)
|
||||||
|
|
||||||
|
objects.value = await api.get(`/objects?${params}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load objects:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectTypes() {
|
||||||
|
try {
|
||||||
|
objectTypes.value = await api.get('/objects/types')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load types:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectDetail(id: number) {
|
||||||
|
try {
|
||||||
|
selectedObject.value = await api.get(`/objects/${id}`)
|
||||||
|
showDetailModal.value = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load object:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredObjects = computed(() => {
|
||||||
|
let result = [...objects.value]
|
||||||
|
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
result = result.filter(o =>
|
||||||
|
o.name.toLowerCase().includes(query) ||
|
||||||
|
o.city?.toLowerCase().includes(query) ||
|
||||||
|
o.object_number?.toLowerCase().includes(query) ||
|
||||||
|
o.customer_name?.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
function openAddModal() {
|
||||||
|
editingObject.value = null
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
short_name: '',
|
||||||
|
object_number: '',
|
||||||
|
object_type: 'other',
|
||||||
|
street: '',
|
||||||
|
house_number: '',
|
||||||
|
postal_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
|
access_info: '',
|
||||||
|
parking_info: '',
|
||||||
|
customer_name: '',
|
||||||
|
size_sqm: null,
|
||||||
|
floors: null
|
||||||
|
}
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(obj: SecObject) {
|
||||||
|
editingObject.value = obj
|
||||||
|
formData.value = { ...obj } as any
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveObject() {
|
||||||
|
if (!formData.value.name) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (editingObject.value) {
|
||||||
|
await api.put(`/objects/${editingObject.value.id}`, formData.value)
|
||||||
|
} else {
|
||||||
|
await api.post('/objects', formData.value)
|
||||||
|
}
|
||||||
|
showModal.value = false
|
||||||
|
await loadObjects()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save:', e)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteObject(id: number) {
|
||||||
|
if (!confirm('Objekt wirklich archivieren?')) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/objects/${id}`)
|
||||||
|
await loadObjects()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addContact() {
|
||||||
|
if (!contactForm.value.name || !selectedObject.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post(`/objects/${selectedObject.value.id}/contacts`, contactForm.value)
|
||||||
|
await loadObjectDetail(selectedObject.value.id)
|
||||||
|
showContactModal.value = false
|
||||||
|
contactForm.value = { name: '', role: '', company: '', phone: '', mobile: '', email: '', availability: '', is_primary: false, is_emergency: false, notes: '' }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to add contact:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteContact(contactId: number) {
|
||||||
|
if (!confirm('Kontakt löschen?')) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/objects/${selectedObject.value.id}/contacts/${contactId}`)
|
||||||
|
await loadObjectDetail(selectedObject.value.id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addInstruction() {
|
||||||
|
if (!instructionForm.value.title || !instructionForm.value.content || !selectedObject.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post(`/objects/${selectedObject.value.id}/instructions`, instructionForm.value)
|
||||||
|
await loadObjectDetail(selectedObject.value.id)
|
||||||
|
showInstructionModal.value = false
|
||||||
|
instructionForm.value = { title: '', category: 'general', content: '', is_critical: false }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to add instruction:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteInstruction(instructionId: number) {
|
||||||
|
if (!confirm('Dienstanweisung löschen?')) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/objects/${selectedObject.value.id}/instructions/${instructionId}`)
|
||||||
|
await loadObjectDetail(selectedObject.value.id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeIcon(type: string): string {
|
||||||
|
const t = objectTypes.value.find(t => t.key === type)
|
||||||
|
return t?.icon || '📍'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeName(type: string): string {
|
||||||
|
const t = objectTypes.value.find(t => t.key === type)
|
||||||
|
return t?.name || type
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstructionCategoryName(cat: string): string {
|
||||||
|
const cats: Record<string, string> = {
|
||||||
|
general: 'Allgemein',
|
||||||
|
patrol: 'Rundgang',
|
||||||
|
emergency: 'Notfall',
|
||||||
|
access: 'Zugang',
|
||||||
|
reporting: 'Meldewesen'
|
||||||
|
}
|
||||||
|
return cats[cat] || cat
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">🏢 Objekte</h1>
|
||||||
|
<button
|
||||||
|
v-if="authStore.canManageUsers"
|
||||||
|
@click="openAddModal"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
➕ Neues Objekt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">🔍 Suche</label>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
placeholder="Name, Stadt, Kunde..."
|
||||||
|
@input="loadObjects"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">🏷️ Typ</label>
|
||||||
|
<select v-model="filterType" class="input" @change="loadObjects">
|
||||||
|
<option value="">Alle Typen</option>
|
||||||
|
<option v-for="type in objectTypes" :key="type.key" :value="type.key">
|
||||||
|
{{ type.icon }} {{ type.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">📊 Status</label>
|
||||||
|
<select v-model="filterStatus" class="input" @change="loadObjects">
|
||||||
|
<option value="active">Aktiv</option>
|
||||||
|
<option value="inactive">Inaktiv</option>
|
||||||
|
<option value="archived">Archiviert</option>
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ filteredObjects.length }} Objekte
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="text-center py-12">
|
||||||
|
<div class="animate-spin text-4xl">⏳</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Objects Grid -->
|
||||||
|
<div v-else-if="filteredObjects.length === 0" class="card text-center py-12 text-gray-500">
|
||||||
|
Keine Objekte gefunden
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="obj in filteredObjects"
|
||||||
|
:key="obj.id"
|
||||||
|
class="card hover:shadow-lg transition-all cursor-pointer"
|
||||||
|
@click="loadObjectDetail(obj.id)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="text-4xl">{{ getTypeIcon(obj.object_type) }}</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white truncate">
|
||||||
|
{{ obj.name }}
|
||||||
|
</h3>
|
||||||
|
<p v-if="obj.city" class="text-sm text-gray-500">
|
||||||
|
📍 {{ obj.postal_code }} {{ obj.city }}
|
||||||
|
</p>
|
||||||
|
<p v-if="obj.customer_name" class="text-sm text-gray-500">
|
||||||
|
👤 {{ obj.customer_name }}
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<span class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
||||||
|
{{ getTypeName(obj.object_type) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="obj.contact_count" class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
|
||||||
|
👥 {{ obj.contact_count }}
|
||||||
|
</span>
|
||||||
|
<span v-if="obj.checkpoint_count" class="text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
|
||||||
|
📍 {{ obj.checkpoint_count }} CP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div v-if="authStore.canManageUsers" class="flex gap-2 mt-4 pt-3 border-t dark:border-gray-700" @click.stop>
|
||||||
|
<button @click="openEditModal(obj)" class="text-sm text-blue-600 hover:underline">
|
||||||
|
✏️ Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button @click="deleteObject(obj.id)" class="text-sm text-red-600 hover:underline">
|
||||||
|
🗑️ Archivieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail Modal -->
|
||||||
|
<div v-if="showDetailModal && selectedObject" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-start justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-4xl">{{ getTypeIcon(selectedObject.object_type) }}</span>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ selectedObject.name }}</h2>
|
||||||
|
<p v-if="selectedObject.city" class="text-gray-500">
|
||||||
|
{{ selectedObject.street }} {{ selectedObject.house_number }},
|
||||||
|
{{ selectedObject.postal_code }} {{ selectedObject.city }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="showDetailModal = false" class="text-2xl hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div v-if="selectedObject.phone" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||||
|
<p class="text-xs text-gray-500">📞 Telefon</p>
|
||||||
|
<p class="font-medium">{{ selectedObject.phone }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedObject.email" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||||
|
<p class="text-xs text-gray-500">📧 E-Mail</p>
|
||||||
|
<p class="font-medium truncate">{{ selectedObject.email }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedObject.customer_name" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||||
|
<p class="text-xs text-gray-500">👤 Kunde</p>
|
||||||
|
<p class="font-medium">{{ selectedObject.customer_name }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedObject.object_number" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||||
|
<p class="text-xs text-gray-500"># Objektnummer</p>
|
||||||
|
<p class="font-medium">{{ selectedObject.object_number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div v-if="selectedObject.description" class="mb-6">
|
||||||
|
<h3 class="font-semibold mb-2">📝 Beschreibung</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ selectedObject.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access Info -->
|
||||||
|
<div v-if="selectedObject.access_info || selectedObject.parking_info" class="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<div v-if="selectedObject.access_info" class="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded">
|
||||||
|
<h3 class="font-semibold mb-2">🔑 Zugang</h3>
|
||||||
|
<p class="text-sm whitespace-pre-wrap">{{ selectedObject.access_info }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedObject.parking_info" class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded">
|
||||||
|
<h3 class="font-semibold mb-2">🅿️ Parken</h3>
|
||||||
|
<p class="text-sm whitespace-pre-wrap">{{ selectedObject.parking_info }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contacts -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-semibold">👥 Ansprechpartner</h3>
|
||||||
|
<button v-if="authStore.canManageUsers" @click="showContactModal = true" class="text-sm text-blue-600 hover:underline">
|
||||||
|
➕ Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!selectedObject.contacts?.length" class="text-gray-500 text-sm">
|
||||||
|
Keine Kontakte hinterlegt
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid gap-2">
|
||||||
|
<div
|
||||||
|
v-for="contact in selectedObject.contacts"
|
||||||
|
:key="contact.id"
|
||||||
|
class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 p-3 rounded"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ contact.name }}
|
||||||
|
<span v-if="contact.is_primary" class="text-xs bg-blue-500 text-white px-1 rounded ml-1">Haupt</span>
|
||||||
|
<span v-if="contact.is_emergency" class="text-xs bg-red-500 text-white px-1 rounded ml-1">Notfall</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="contact.role" class="text-sm text-gray-500">{{ contact.role }}</p>
|
||||||
|
<p v-if="contact.phone || contact.mobile" class="text-sm">
|
||||||
|
📞 {{ contact.phone || contact.mobile }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button v-if="authStore.canManageUsers" @click="deleteContact(contact.id)" class="text-red-500 hover:text-red-700">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-semibold">📋 Dienstanweisungen</h3>
|
||||||
|
<button v-if="authStore.canManageUsers" @click="showInstructionModal = true" class="text-sm text-blue-600 hover:underline">
|
||||||
|
➕ Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!selectedObject.instructions?.length" class="text-gray-500 text-sm">
|
||||||
|
Keine Dienstanweisungen hinterlegt
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="instr in selectedObject.instructions"
|
||||||
|
:key="instr.id"
|
||||||
|
:class="['p-4 rounded border-l-4', instr.is_critical ? 'border-red-500 bg-red-50 dark:bg-red-900/20' : 'border-gray-300 bg-gray-50 dark:bg-gray-700']"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ instr.title }}
|
||||||
|
<span class="text-xs text-gray-500 ml-2">{{ getInstructionCategoryName(instr.category) }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mt-1 whitespace-pre-wrap text-gray-600 dark:text-gray-300">{{ instr.content }}</p>
|
||||||
|
</div>
|
||||||
|
<button v-if="authStore.canManageUsers" @click="deleteInstruction(instr.id)" class="text-red-500 hover:text-red-700 ml-2">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkpoints -->
|
||||||
|
<div v-if="selectedObject.checkpoints?.length" class="mb-6">
|
||||||
|
<h3 class="font-semibold mb-3">📍 Kontrollpunkte</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="cp in selectedObject.checkpoints"
|
||||||
|
:key="cp.id"
|
||||||
|
class="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-3 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
{{ cp.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Close Button -->
|
||||||
|
<div class="flex justify-end pt-4 border-t dark:border-gray-700">
|
||||||
|
<button @click="showDetailModal = false" class="btn">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Object Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
{{ editingObject ? '✏️ Objekt bearbeiten' : '➕ Neues Objekt' }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="saveObject" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-medium mb-1">Name *</label>
|
||||||
|
<input v-model="formData.name" type="text" class="input" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kurzname</label>
|
||||||
|
<input v-model="formData.short_name" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Objektnummer</label>
|
||||||
|
<input v-model="formData.object_number" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Typ</label>
|
||||||
|
<select v-model="formData.object_type" class="input">
|
||||||
|
<option v-for="type in objectTypes" :key="type.key" :value="type.key">
|
||||||
|
{{ type.icon }} {{ type.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kunde</label>
|
||||||
|
<input v-model="formData.customer_name" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="dark:border-gray-700">
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-medium mb-1">Straße</label>
|
||||||
|
<input v-model="formData.street" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Nr.</label>
|
||||||
|
<input v-model="formData.house_number" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">PLZ</label>
|
||||||
|
<input v-model="formData.postal_code" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-medium mb-1">Stadt</label>
|
||||||
|
<input v-model="formData.city" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Telefon</label>
|
||||||
|
<input v-model="formData.phone" type="tel" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
||||||
|
<input v-model="formData.email" type="email" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Beschreibung</label>
|
||||||
|
<textarea v-model="formData.description" class="input" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">🔑 Zugangsinfos</label>
|
||||||
|
<textarea v-model="formData.access_info" class="input" rows="2" placeholder="Schlüssel, Codes..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">🅿️ Parkhinweise</label>
|
||||||
|
<textarea v-model="formData.parking_info" class="input" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="button" @click="showModal = false" class="btn flex-1">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn btn-primary flex-1">
|
||||||
|
{{ saving ? 'Speichern...' : 'Speichern' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Contact Modal -->
|
||||||
|
<div v-if="showContactModal" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">👤 Kontakt hinzufügen</h2>
|
||||||
|
<form @submit.prevent="addContact" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Name *</label>
|
||||||
|
<input v-model="contactForm.name" type="text" class="input" required />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Rolle</label>
|
||||||
|
<input v-model="contactForm.role" type="text" class="input" placeholder="z.B. Hausmeister" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Firma</label>
|
||||||
|
<input v-model="contactForm.company" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Telefon</label>
|
||||||
|
<input v-model="contactForm.phone" type="tel" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Mobil</label>
|
||||||
|
<input v-model="contactForm.mobile" type="tel" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
||||||
|
<input v-model="contactForm.email" type="email" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input v-model="contactForm.is_primary" type="checkbox" class="rounded" />
|
||||||
|
<span class="text-sm">Hauptkontakt</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input v-model="contactForm.is_emergency" type="checkbox" class="rounded" />
|
||||||
|
<span class="text-sm">Notfallkontakt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" @click="showContactModal = false" class="btn flex-1">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn btn-primary flex-1">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Instruction Modal -->
|
||||||
|
<div v-if="showInstructionModal" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">📋 Dienstanweisung hinzufügen</h2>
|
||||||
|
<form @submit.prevent="addInstruction" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Titel *</label>
|
||||||
|
<input v-model="instructionForm.title" type="text" class="input" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kategorie</label>
|
||||||
|
<select v-model="instructionForm.category" class="input">
|
||||||
|
<option value="general">Allgemein</option>
|
||||||
|
<option value="patrol">Rundgang</option>
|
||||||
|
<option value="emergency">Notfall</option>
|
||||||
|
<option value="access">Zugang</option>
|
||||||
|
<option value="reporting">Meldewesen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Inhalt *</label>
|
||||||
|
<textarea v-model="instructionForm.content" class="input" rows="4" required></textarea>
|
||||||
|
</div>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input v-model="instructionForm.is_critical" type="checkbox" class="rounded" />
|
||||||
|
<span class="text-sm text-red-600">⚠️ Als kritisch markieren</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" @click="showInstructionModal = false" class="btn flex-1">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn btn-primary flex-1">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user