🎨 Frontend komplett implementiert

Views:
- Login/Registrierung
- Dashboard mit Stats
- Aufträge (Liste + Detail)
- Mitarbeiterverwaltung
- Verfügbarkeitskalender
- Stundenzettel
- Einstellungen
- Module (Dev-Panel)

Features:
- Vue 3 + Composition API
- TailwindCSS mit Dark Mode
- Pinia State Management
- JWT Auth mit Refresh
- Responsive Design
- Rollen-basierte Navigation
This commit is contained in:
2026-02-20 15:18:06 +00:00
parent 82d388f555
commit 474c6d2470
27 changed files with 2328 additions and 2 deletions

View File

@@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { api } from '@/api'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const order = ref<any>(null)
const assignments = ref<any[]>([])
const loading = ref(true)
onMounted(async () => {
try {
const res = await api.get<{ order: any; assignments: any[] }>(`/orders/${route.params.id}`)
order.value = res.data.order
assignments.value = res.data.assignments
} catch (e) {
console.error(e)
router.push('/orders')
} finally {
loading.value = false
}
})
async function updateStatus(status: string) {
try {
await api.put(`/orders/${route.params.id}`, { status })
order.value.status = status
} catch (e) {
alert(e instanceof Error ? e.message : 'Fehler')
}
}
async function confirmAssignment(confirm: boolean) {
try {
await api.put(`/orders/${route.params.id}/assignment`, {
status: confirm ? 'confirmed' : 'declined'
})
const myAssignment = assignments.value.find(a => a.user_id === authStore.user?.id)
if (myAssignment) {
myAssignment.status = confirm ? 'confirmed' : 'declined'
}
} catch (e) {
alert(e instanceof Error ? e.message : 'Fehler')
}
}
function getStatusLabel(status: string) {
const labels: Record<string, string> = {
draft: 'Entwurf', published: 'Veröffentlicht', in_progress: 'In Bearbeitung',
completed: 'Abgeschlossen', cancelled: 'Abgesagt',
pending: 'Ausstehend', confirmed: 'Bestätigt', declined: 'Abgelehnt'
}
return labels[status] || status
}
</script>
<template>
<div v-if="loading" class="text-center py-12 text-gray-500">Lädt...</div>
<div v-else-if="order" class="space-y-6">
<!-- Back button -->
<router-link to="/orders" class="text-primary-600 hover:text-primary-700 text-sm">
Zurück zu Aufträge
</router-link>
<!-- Header -->
<div class="card">
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-3">
<span class="text-gray-500">#{{ order.number }}</span>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ order.title }}</h1>
</div>
<p v-if="order.description" class="mt-2 text-gray-600 dark:text-gray-400">
{{ order.description }}
</p>
</div>
<span :class="['badge', order.status === 'completed' ? 'badge-success' : 'badge-primary']">
{{ getStatusLabel(order.status) }}
</span>
</div>
<!-- Info Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
<div v-if="order.location">
<p class="text-sm text-gray-500">Ort</p>
<p class="font-medium">📍 {{ order.location }}</p>
</div>
<div v-if="order.start_time">
<p class="text-sm text-gray-500">Start</p>
<p class="font-medium">{{ new Date(order.start_time).toLocaleString('de-DE') }}</p>
</div>
<div v-if="order.client_name">
<p class="text-sm text-gray-500">Kunde</p>
<p class="font-medium">{{ order.client_name }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Benötigte MA</p>
<p class="font-medium">{{ assignments.length }}/{{ order.required_staff }}</p>
</div>
</div>
<!-- Status actions for management -->
<div v-if="authStore.canManageOrders" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-500 mb-2">Status ändern:</p>
<div class="flex flex-wrap gap-2">
<button class="btn btn-secondary text-sm" @click="updateStatus('draft')">Entwurf</button>
<button class="btn btn-primary text-sm" @click="updateStatus('published')">Veröffentlichen</button>
<button class="btn btn-warning text-sm" @click="updateStatus('in_progress')">In Bearbeitung</button>
<button class="btn btn-success text-sm" @click="updateStatus('completed')">Abschließen</button>
</div>
</div>
</div>
<!-- Assignments -->
<div class="card">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
👥 Zugewiesene Mitarbeiter
</h2>
<div v-if="assignments.length === 0" class="text-center py-4 text-gray-500">
Noch keine Mitarbeiter zugewiesen
</div>
<div v-else class="space-y-3">
<div
v-for="assignment in assignments"
:key="assignment.id"
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
>
<div>
<p class="font-medium text-gray-900 dark:text-white">{{ assignment.user_name }}</p>
<p class="text-sm text-gray-500">{{ assignment.user_phone }}</p>
</div>
<div class="flex items-center gap-2">
<span :class="['badge', assignment.status === 'confirmed' ? 'badge-success' : assignment.status === 'declined' ? 'badge-danger' : 'badge-warning']">
{{ getStatusLabel(assignment.status) }}
</span>
<!-- Confirm/Decline buttons for assigned user -->
<template v-if="assignment.user_id === authStore.user?.id && assignment.status === 'pending'">
<button class="btn btn-success text-sm" @click="confirmAssignment(true)"></button>
<button class="btn btn-danger text-sm" @click="confirmAssignment(false)"></button>
</template>
</div>
</div>
</div>
</div>
<!-- Special Instructions -->
<div v-if="order.special_instructions" class="card">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
📝 Besondere Hinweise
</h2>
<p class="text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{{ order.special_instructions }}</p>
</div>
</div>
</template>