421 lines
13 KiB
Vue
421 lines
13 KiB
Vue
<script setup>
|
|
import { useStaffTime } from '~/composables/useStaffTime'
|
|
import { useAuthStore } from '~/stores/auth'
|
|
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
|
|
|
definePageMeta({
|
|
layout: "default",
|
|
})
|
|
|
|
const { list, start, stop, submit, approve } = useStaffTime()
|
|
const auth = useAuthStore()
|
|
const router = useRouter()
|
|
// MOBILE DETECTION
|
|
const platformIsNative = useCapacitor().getIsNative()
|
|
|
|
// STATE
|
|
const loading = ref(false)
|
|
const showModal = ref(false)
|
|
const editEntry = ref(null)
|
|
|
|
// 👥 Nutzer-Filter
|
|
const users = ref([])
|
|
// WICHTIG: Standardmäßig IMMER den aktuellen User wählen (kein null mehr)
|
|
const selectedUser = ref(auth.user.id)
|
|
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
|
|
|
|
// LIST + ACTIVE
|
|
const entries = ref([])
|
|
|
|
// Active berechnet sich jetzt aus den geladenen Einträgen.
|
|
// Da wir immer nur EINEN User laden, ist ein laufender Eintrag hier
|
|
// automatisch der aktive Eintrag des angezeigten Users.
|
|
const active = computed(() => entries.value.find(e => !e.stopped_at))
|
|
|
|
// Prüft, ob wir gerade das eigene Profil ansehen (für Start/Stop Buttons)
|
|
const isViewingSelf = computed(() => selectedUser.value === auth.user.id)
|
|
|
|
|
|
const typeLabel = {
|
|
work: "Arbeitszeit",
|
|
vacation: "Urlaub",
|
|
sick: "Krankheit",
|
|
holiday: "Feiertag",
|
|
other: "Sonstiges"
|
|
}
|
|
|
|
const typeColor = {
|
|
work: "gray",
|
|
vacation: "yellow",
|
|
sick: "rose",
|
|
holiday: "blue",
|
|
other: "gray"
|
|
}
|
|
|
|
|
|
async function loadUsers() {
|
|
if (!canViewAll.value) return
|
|
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
|
|
users.value = res
|
|
}
|
|
|
|
// LOAD ENTRIES (Erzwingt jetzt immer eine User-ID)
|
|
async function load() {
|
|
if (!selectedUser.value) return
|
|
|
|
entries.value = await list({
|
|
user_id: selectedUser.value // 👈 Hier wird der Filter erzwungen
|
|
})
|
|
}
|
|
|
|
async function handleStart() {
|
|
// Sicherheitshalber: Starten nur erlauben, wenn man sich selbst ansieht
|
|
if (!isViewingSelf.value) return
|
|
|
|
loading.value = true
|
|
await start("Arbeitszeit gestartet")
|
|
await load()
|
|
loading.value = false
|
|
}
|
|
|
|
async function handleStop() {
|
|
if (!active.value) return
|
|
// Stoppen darf man ggf. auch andere (als Admin), daher hier kein isViewingSelf Check
|
|
// oder je nach Logik anpassen. Hier erlauben wir das Stoppen des angezeigten Timers.
|
|
|
|
loading.value = true
|
|
await stop(active.value.id)
|
|
await load()
|
|
loading.value = false
|
|
}
|
|
|
|
function handleEdit(entry) {
|
|
editEntry.value = entry
|
|
showModal.value = true
|
|
}
|
|
|
|
async function handleSubmit(entry) {
|
|
await submit(entry)
|
|
await load()
|
|
}
|
|
|
|
async function handleApprove(entry) {
|
|
await approve(entry)
|
|
await load()
|
|
}
|
|
|
|
// Watcher: Wenn der User im Dropdown gewechselt wird, neu laden
|
|
watch(selectedUser, () => {
|
|
load()
|
|
})
|
|
|
|
onMounted(async () => {
|
|
await loadUsers()
|
|
await load() // Lädt initial den eingeloggten User
|
|
setPageLayout(platformIsNative ? 'mobile' : 'default')
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<template v-if="!platformIsNative">
|
|
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
|
|
|
<!--<UDashboardToolbar>
|
|
<template #left>
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
|
<span v-if="active" class="text-primary-600 font-medium">
|
|
<span v-if="isViewingSelf">Läuft seit</span>
|
|
<span v-else>Mitarbeiter aktiv seit</span>
|
|
{{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
|
</span>
|
|
<span v-else class="text-gray-500">
|
|
<span v-if="isViewingSelf">Keine aktive Zeit</span>
|
|
<span v-else>Mitarbeiter nicht aktiv</span>
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #right>
|
|
<template v-if="isViewingSelf">
|
|
<UButton
|
|
v-if="active"
|
|
color="red"
|
|
icon="i-heroicons-stop"
|
|
:loading="loading"
|
|
label="Stoppen"
|
|
@click="handleStop"
|
|
/>
|
|
<UButton
|
|
v-else
|
|
color="green"
|
|
icon="i-heroicons-play"
|
|
:loading="loading"
|
|
label="Starten"
|
|
@click="handleStart"
|
|
/>
|
|
</template>
|
|
<template v-else-if="active && canViewAll">
|
|
<UButton
|
|
color="red"
|
|
variant="soft"
|
|
icon="i-heroicons-stop"
|
|
:loading="loading"
|
|
label="Mitarbeiter stoppen"
|
|
@click="handleStop"
|
|
/>
|
|
</template>
|
|
|
|
|
|
<UButton
|
|
color="primary"
|
|
icon="i-heroicons-plus"
|
|
label="Zeit"
|
|
@click="() => { editEntry = null; showModal = true }"
|
|
/>
|
|
</template>
|
|
</UDashboardToolbar>-->
|
|
|
|
<UDashboardToolbar>
|
|
<template #left>
|
|
<div v-if="canViewAll" class="flex items-center gap-2">
|
|
<USelectMenu
|
|
v-model="selectedUser"
|
|
:options="users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))"
|
|
placeholder="Benutzer auswählen"
|
|
value-attribute="value"
|
|
option-attribute="label"
|
|
class="min-w-[220px]"
|
|
:clearable="false"
|
|
/>
|
|
|
|
<UTooltip text="Anwesenheiten auswerten">
|
|
<UButton
|
|
:disabled="!selectedUser"
|
|
color="gray"
|
|
icon="i-heroicons-chart-bar"
|
|
label="Auswertung"
|
|
variant="soft"
|
|
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
|
|
/>
|
|
</UTooltip>
|
|
|
|
</div>
|
|
</template>
|
|
</UDashboardToolbar>
|
|
|
|
|
|
<UDashboardPanelContent>
|
|
<UTable
|
|
:rows="entries"
|
|
:columns="[
|
|
{ key: 'actions', label: '' },
|
|
{ key: 'state', label: 'Status' },
|
|
{ key: 'started_at', label: 'Start' },
|
|
{ key: 'stopped_at', label: 'Ende' },
|
|
{ key: 'duration_minutes', label: 'Dauer' },
|
|
// { key: 'user', label: 'Mitarbeiter' }, // Spalte entfernt, da wir eh nur einen User sehen
|
|
{ key: 'type', label: 'Typ' },
|
|
{ key: 'description', label: 'Beschreibung' },
|
|
]"
|
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
|
|
>
|
|
<template #state-data="{ row }">
|
|
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
|
<span v-else-if="row.state === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
|
<span v-else-if="row.state === 'draft'" class="text-red-500">Entwurf</span>
|
|
</template>
|
|
<template #type-data="{ row }">
|
|
<UBadge :color="typeColor[row.type] || 'gray'">
|
|
{{ typeLabel[row.type] || row.type }}
|
|
</UBadge>
|
|
</template>
|
|
|
|
<template #started_at-data="{ row }">
|
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
|
|
</span>
|
|
<span v-else>
|
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #stopped_at-data="{ row }">
|
|
<span v-if="!row.stopped_at" class="text-primary-500 font-medium">
|
|
läuft...
|
|
</span>
|
|
<span v-else-if="row.type === 'vacation' || row.type === 'sick'">
|
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
|
|
</span>
|
|
<span v-else>
|
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #duration_minutes-data="{ row }">
|
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
|
</span>
|
|
<span v-else>
|
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #actions-data="{ row }">
|
|
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted' && canViewAll">
|
|
<UButton
|
|
variant="ghost"
|
|
icon="i-heroicons-check-circle"
|
|
@click="handleApprove(row)"
|
|
/>
|
|
</UTooltip>
|
|
<UTooltip text="Zeit einreichen" v-if="row.state === 'draft'">
|
|
<UButton
|
|
variant="ghost"
|
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
|
@click="handleSubmit(row)"
|
|
/>
|
|
</UTooltip>
|
|
<UTooltip text="Zeit bearbeiten" v-if="row.state === 'draft'">
|
|
<UButton
|
|
variant="ghost"
|
|
icon="i-heroicons-pencil-square"
|
|
@click="handleEdit(row)"
|
|
/>
|
|
</UTooltip>
|
|
</template>
|
|
|
|
<template #description-data="{ row }">
|
|
<span v-if="row.type === 'vacation'">{{row.vacation_reason}}</span>
|
|
<span v-else-if="row.type === 'sick'">{{row.sick_reason}}</span>
|
|
<span v-else>{{row.description}}</span>
|
|
</template>
|
|
</UTable>
|
|
</UDashboardPanelContent>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<UDashboardNavbar title="Zeiterfassung" />
|
|
|
|
<div class="relative flex flex-col h-[100dvh] overflow-hidden">
|
|
|
|
<div v-if="isViewingSelf" class="p-4 bg-white dark:bg-gray-900 border-b sticky top-0 z-20 shadow-sm">
|
|
<UCard class="p-3">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-gray-500 text-sm">Aktive Zeit</p>
|
|
<p v-if="active" class="text-primary-600 font-semibold">
|
|
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
|
</p>
|
|
<p v-else class="text-gray-600">Keine aktive Zeit</p>
|
|
</div>
|
|
|
|
<UButton
|
|
v-if="active"
|
|
color="red"
|
|
icon="i-heroicons-stop"
|
|
:loading="loading"
|
|
@click="handleStop"
|
|
/>
|
|
<UButton
|
|
v-else
|
|
color="green"
|
|
icon="i-heroicons-play"
|
|
:loading="loading"
|
|
@click="handleStart"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<div class="px-3 mt-3">
|
|
<UButton
|
|
color="gray"
|
|
icon="i-heroicons-chart-bar"
|
|
label="Auswertung"
|
|
class="w-full"
|
|
variant="soft"
|
|
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
|
|
/>
|
|
</div>
|
|
|
|
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
|
|
|
<UCard
|
|
v-for="row in entries"
|
|
:key="row.id"
|
|
class="p-4 border rounded-xl active:scale-[0.98] transition cursor-pointer"
|
|
@click="handleEdit(row)"
|
|
>
|
|
<div class="flex justify-between items-center">
|
|
<div class="font-semibold flex items-center gap-2">
|
|
<span>{{ row.description || 'Keine Beschreibung' }}</span>
|
|
|
|
<UBadge :color="typeColor[row.type]" class="text-xs">
|
|
{{ typeLabel[row.type] }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<UBadge
|
|
:color="{ approved: 'primary', submitted: 'cyan', draft: 'red' }[row.state]"
|
|
>
|
|
{{ { approved: 'Genehmigt', submitted: 'Eingereicht', draft: 'Entwurf' }[row.state] || row.state }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
|
</p>
|
|
|
|
<p class="text-sm text-gray-500">
|
|
Ende:
|
|
<span v-if="row.stopped_at">
|
|
{{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
|
|
</span>
|
|
<span v-else class="text-primary-500 font-medium">läuft...</span>
|
|
</p>
|
|
|
|
<p class="text-sm text-gray-500">
|
|
Dauer:
|
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
|
</p>
|
|
|
|
<div class="flex gap-2 mt-3">
|
|
<UButton
|
|
v-if="row.state === 'draft'"
|
|
color="gray"
|
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
|
label="Einreichen"
|
|
variant="soft"
|
|
@click.stop="handleSubmit(row)"
|
|
/>
|
|
<UButton
|
|
v-if="row.state === 'submitted' && canViewAll"
|
|
color="primary"
|
|
icon="i-heroicons-check"
|
|
label="Genehmigen"
|
|
variant="soft"
|
|
@click.stop="handleApprove(row)"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
</UDashboardPanelContent>
|
|
|
|
<FloatingActionButton
|
|
icon="i-heroicons-plus"
|
|
class="!fixed bottom-6 right-6 z-50"
|
|
color="primary"
|
|
@click="() => { editEntry = null; showModal = true }"
|
|
/>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<StaffTimeEntryModal
|
|
v-model="showModal"
|
|
:entry="editEntry"
|
|
@saved="load"
|
|
:users="users"
|
|
:can-select-user="canViewAll"
|
|
:default-user-id="selectedUser"
|
|
/>
|
|
</template> |