Files
secu-frontend/src/views/OrderDetailView.vue
OpenClaw a26e97bc93 fix: Correct button visibility logic
- Mitarbeiter: sees buttons only for OWN pending assignment
- Chef/Disponent: sees buttons for ALL assignments
2026-03-13 14:58:38 +00:00

368 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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(userId: string, confirm: boolean) {
try {
await api.put(`/orders/${route.params.id}/assignment/${userId}`, {
status: confirm ? 'confirmed' : 'declined'
})
const assignment = assignments.value.find(a => a.user_id === userId)
if (assignment) {
assignment.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 (own assignment) OR management (any) -->
<template v-if="(assignment.user_id === authStore.user?.id && assignment.status === 'pending') || authStore.canManageOrders">
<button
class="btn btn-success text-sm"
:class="{ 'opacity-50': assignment.status === 'confirmed' }"
@click="confirmAssignment(assignment.user_id, true)"
:title="assignment.status === 'confirmed' ? 'Bereits bestätigt' : 'Bestätigen'"
></button>
<button
class="btn btn-danger text-sm"
:class="{ 'opacity-50': assignment.status === 'declined' }"
@click="confirmAssignment(assignment.user_id, false)"
:title="assignment.status === 'declined' ? 'Bereits abgelehnt' : 'Ablehnen'"
></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>