358 lines
13 KiB
Vue
358 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { api } from '@/api'
|
||
|
||
const { t } = useI18n()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const authStore = useAuthStore()
|
||
|
||
const order = ref<any>(null)
|
||
const assignments = ref<any[]>([])
|
||
const loading = ref(true)
|
||
const showAssignModal = ref(false)
|
||
const availableUsers = ref<any[]>([])
|
||
const loadingUsers = ref(false)
|
||
const selectedUsers = ref<string[]>([])
|
||
const assigning = ref(false)
|
||
|
||
onMounted(async () => {
|
||
await loadOrder()
|
||
})
|
||
|
||
async function loadOrder() {
|
||
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 loadAvailableUsers() {
|
||
loadingUsers.value = true
|
||
try {
|
||
const res = await api.get<{ users: any[] }>('/users')
|
||
// Filter out already assigned users
|
||
const assignedIds = assignments.value.map(a => a.user_id)
|
||
availableUsers.value = res.data.users.filter(u =>
|
||
!assignedIds.includes(u.id) && u.active
|
||
)
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loadingUsers.value = false
|
||
}
|
||
}
|
||
|
||
function openAssignModal() {
|
||
selectedUsers.value = []
|
||
showAssignModal.value = true
|
||
loadAvailableUsers()
|
||
}
|
||
|
||
function toggleUserSelection(userId: string) {
|
||
const idx = selectedUsers.value.indexOf(userId)
|
||
if (idx > -1) {
|
||
selectedUsers.value.splice(idx, 1)
|
||
} else {
|
||
selectedUsers.value.push(userId)
|
||
}
|
||
}
|
||
|
||
async function assignSelectedUsers() {
|
||
if (selectedUsers.value.length === 0) return
|
||
|
||
assigning.value = true
|
||
try {
|
||
// Assign each selected user
|
||
for (const userId of selectedUsers.value) {
|
||
await api.post(`/orders/${route.params.id}/assign`, { user_id: userId })
|
||
}
|
||
showAssignModal.value = false
|
||
await loadOrder()
|
||
} catch (e) {
|
||
alert(e instanceof Error ? e.message : t('messages.error'))
|
||
} finally {
|
||
assigning.value = false
|
||
}
|
||
}
|
||
|
||
async function removeAssignment(userId: string) {
|
||
if (!confirm(t('messages.confirmDelete'))) return
|
||
|
||
try {
|
||
await api.delete(`/orders/${route.params.id}/assign/${userId}`)
|
||
await loadOrder()
|
||
} catch (e) {
|
||
alert(e instanceof Error ? e.message : t('messages.error'))
|
||
}
|
||
}
|
||
|
||
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 : t('messages.error'))
|
||
}
|
||
}
|
||
|
||
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 : t('messages.error'))
|
||
}
|
||
}
|
||
|
||
function getStatusLabel(status: string) {
|
||
const key = `orders.statuses.${status}`
|
||
const translated = t(key)
|
||
if (translated !== key) return translated
|
||
|
||
// Fallback for assignment statuses
|
||
const fallback: Record<string, string> = {
|
||
pending: 'Ausstehend', confirmed: 'Bestätigt', declined: 'Abgelehnt'
|
||
}
|
||
return fallback[status] || status
|
||
}
|
||
|
||
function getRoleBadge(role: string) {
|
||
const badges: Record<string, { class: string; label: string }> = {
|
||
chef: { class: 'bg-purple-100 text-purple-700', label: 'Chef' },
|
||
disponent: { class: 'bg-blue-100 text-blue-700', label: 'Disponent' },
|
||
mitarbeiter: { class: 'bg-green-100 text-green-700', label: 'Mitarbeiter' },
|
||
subunternehmer: { class: 'bg-orange-100 text-orange-700', label: 'Subunternehmer' }
|
||
}
|
||
return badges[role] || { class: 'bg-gray-100 text-gray-700', label: role }
|
||
}
|
||
|
||
const spotsRemaining = computed(() => {
|
||
return (order.value?.required_staff || 0) - assignments.value.length
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="loading" class="text-center py-12 text-gray-500">{{ t('app.loading') }}</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">
|
||
← {{ t('app.back') }}
|
||
</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">{{ t('orders.location') }}</p>
|
||
<p class="font-medium">📍 {{ order.location }}</p>
|
||
</div>
|
||
<div v-if="order.start_time">
|
||
<p class="text-sm text-gray-500">{{ t('orders.startDate') }}</p>
|
||
<p class="font-medium">{{ new Date(order.start_time).toLocaleString() }}</p>
|
||
</div>
|
||
<div v-if="order.client_name">
|
||
<p class="text-sm text-gray-500">{{ t('orders.client') }}</p>
|
||
<p class="font-medium">{{ order.client_name }}</p>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm text-gray-500">{{ t('users.employees') }}</p>
|
||
<p class="font-medium">
|
||
{{ assignments.length }}/{{ order.required_staff }}
|
||
<span v-if="spotsRemaining > 0" class="text-orange-500 text-sm ml-1">
|
||
({{ spotsRemaining }} {{ t('app.none') }})
|
||
</span>
|
||
</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">{{ t('app.status') }}:</p>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button class="btn btn-secondary text-sm" @click="updateStatus('draft')">{{ t('orders.statuses.draft') }}</button>
|
||
<button class="btn btn-primary text-sm" @click="updateStatus('published')">{{ t('orders.statuses.published') }}</button>
|
||
<button class="btn btn-warning text-sm" @click="updateStatus('in_progress')">{{ t('orders.statuses.in_progress') }}</button>
|
||
<button class="btn btn-success text-sm" @click="updateStatus('completed')">{{ t('orders.statuses.completed') }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Assignments -->
|
||
<div class="card">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||
👥 {{ t('orders.assignedTo') }}
|
||
</h2>
|
||
<button
|
||
v-if="authStore.canManageOrders"
|
||
@click="openAssignModal"
|
||
class="btn btn-primary"
|
||
>
|
||
+ {{ t('app.add') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="assignments.length === 0" class="text-center py-8 text-gray-500">
|
||
<p class="text-4xl mb-2">👥</p>
|
||
<p>{{ t('messages.noData') }}</p>
|
||
<button
|
||
v-if="authStore.canManageOrders"
|
||
@click="openAssignModal"
|
||
class="btn btn-primary mt-4"
|
||
>
|
||
+ {{ t('users.employee') }} {{ t('app.add') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else class="space-y-3">
|
||
<div
|
||
v-for="assignment in assignments"
|
||
:key="assignment.id"
|
||
class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
|
||
>
|
||
<div class="flex items-center gap-4">
|
||
<div class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||
<span class="text-primary-600 dark:text-primary-300 font-medium">
|
||
{{ assignment.user_name?.split(' ').map((n: string) => n[0]).join('') }}
|
||
</span>
|
||
</div>
|
||
<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 || assignment.user_email }}</p>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- Remove button for management -->
|
||
<button
|
||
v-if="authStore.canManageOrders"
|
||
@click="removeAssignment(assignment.user_id)"
|
||
class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
|
||
:title="t('app.delete')"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</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">
|
||
📝 {{ t('app.notes') }}
|
||
</h2>
|
||
<p class="text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{{ order.special_instructions }}</p>
|
||
</div>
|
||
|
||
<!-- Assign Users Modal -->
|
||
<div v-if="showAssignModal" 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-lg mx-4 max-h-[80vh] flex flex-col">
|
||
<div class="p-4 border-b dark:border-gray-700">
|
||
<h2 class="text-xl font-semibold">👥 {{ t('users.employee') }} {{ t('app.add') }}</h2>
|
||
<p class="text-sm text-gray-500 mt-1">
|
||
{{ t('users.employees') }}: {{ selectedUsers.length }} {{ t('app.add') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto p-4">
|
||
<div v-if="loadingUsers" class="text-center py-8 text-gray-500">
|
||
{{ t('app.loading') }}
|
||
</div>
|
||
|
||
<div v-else-if="availableUsers.length === 0" class="text-center py-8 text-gray-500">
|
||
<p class="text-4xl mb-2">😕</p>
|
||
<p>{{ t('messages.noData') }}</p>
|
||
</div>
|
||
|
||
<div v-else class="space-y-2">
|
||
<button
|
||
v-for="user in availableUsers"
|
||
:key="user.id"
|
||
@click="toggleUserSelection(user.id)"
|
||
:class="[
|
||
'w-full flex items-center gap-4 p-3 rounded-lg transition-colors text-left',
|
||
selectedUsers.includes(user.id)
|
||
? 'bg-primary-100 dark:bg-primary-900/50 border-2 border-primary-500'
|
||
: 'bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 border-2 border-transparent'
|
||
]"
|
||
>
|
||
<div class="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center flex-shrink-0">
|
||
<span class="font-medium text-gray-600 dark:text-gray-300">
|
||
{{ user.first_name?.[0] }}{{ user.last_name?.[0] }}
|
||
</span>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="font-medium text-gray-900 dark:text-white">
|
||
{{ user.first_name }} {{ user.last_name }}
|
||
</p>
|
||
<p class="text-sm text-gray-500 truncate">{{ user.email }}</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span :class="['px-2 py-0.5 rounded text-xs', getRoleBadge(user.role).class]">
|
||
{{ getRoleBadge(user.role).label }}
|
||
</span>
|
||
<span v-if="selectedUsers.includes(user.id)" class="text-primary-600 text-xl">✓</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-4 border-t dark:border-gray-700 flex gap-3">
|
||
<button @click="showAssignModal = false" class="btn flex-1">
|
||
{{ t('app.cancel') }}
|
||
</button>
|
||
<button
|
||
@click="assignSelectedUsers"
|
||
:disabled="selectedUsers.length === 0 || assigning"
|
||
class="btn btn-primary flex-1"
|
||
>
|
||
{{ assigning ? t('app.loading') : `${selectedUsers.length} ${t('app.add')}` }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|