Fixed Times

This commit is contained in:
2025-12-14 17:15:24 +01:00
parent 9ddda1a933
commit 780b899d42
4 changed files with 386 additions and 253 deletions

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup>
import { useStaffTime } from '~/composables/useStaffTime'
import { useAuthStore } from '~/stores/auth'
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
@@ -12,20 +12,30 @@ const auth = useAuthStore()
const router = useRouter()
// MOBILE DETECTION
const platformIsNative = useCapacitor().getIsNative()
// LIST + ACTIVE
const entries = ref([])
const active = computed(() => entries.value.find(e => !e.stopped_at && e.user_id === auth.user.id))
// STATE
const loading = ref(false)
const showModal = ref(false)
const editEntry = ref(null)
// 👥 Nutzer-Filter (nur für Berechtigte)
// 👥 Nutzer-Filter
const users = ref([])
const selectedUser = ref(platformIsNative ? auth.user.id : null)
// 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",
@@ -45,21 +55,23 @@ const typeColor = {
async function loadUsers() {
if (!canViewAll.value) return
// Beispiel: User aus Supabase holen
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
users.value = res
}
// LOAD ENTRIES (only own entries on mobile)
// LOAD ENTRIES (Erzwingt jetzt immer eine User-ID)
async function load() {
entries.value = await list(
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
)
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()
@@ -68,73 +80,93 @@ async function handleStart() {
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: any) {
function handleEdit(entry) {
editEntry.value = entry
showModal.value = true
}
async function handleSubmit(entry: any) {
await submit(entry.id)
async function handleSubmit(entry) {
await submit(entry)
await load()
}
async function handleApprove(entry: any) {
await approve(entry.id)
async function handleApprove(entry) {
await approve(entry)
await load()
}
onMounted(async () => {
await load()
await loadUsers()
setPageLayout(platformIsNative ? 'mobile' : 'default')
// 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>
<!-- ============================= -->
<!-- DESKTOP VERSION -->
<!-- ============================= -->
<template v-if="!platformIsNative">
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
<UDashboardToolbar>
<!--<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">
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
<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>
<span v-else class="text-gray-500">Keine aktive Zeit</span>
</div>
</template>
<template #right>
<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 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"
@@ -142,28 +174,22 @@ onMounted(async () => {
@click="() => { editEntry = null; showModal = true }"
/>
</template>
</UDashboardToolbar>
</UDashboardToolbar>-->
<UDashboardToolbar>
<template #left>
<!-- 👥 User-Filter (nur bei Berechtigung) -->
<div v-if="canViewAll" class="flex items-center gap-2">
<USelectMenu
v-model="selectedUser"
:options="[
{ label: 'Alle Benutzer', value: null },
...users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))
]"
: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]"
@change="load"
:clearable="false"
/>
<!-- 🔹 Button zur Auswertung -->
<UTooltip
:text="selectedUser ? 'Anwesenheiten des Mitarbeiters auswerten' : 'Mitarbeiter für die Auswertung auswählen'"
>
<UTooltip text="Anwesenheiten auswerten">
<UButton
:disabled="!selectedUser"
color="gray"
@@ -188,10 +214,11 @@ onMounted(async () => {
{ key: 'started_at', label: 'Start' },
{ key: 'stopped_at', label: 'Ende' },
{ key: 'duration_minutes', label: 'Dauer' },
{ key: 'user', label: 'Mitarbeiter' },
// { 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>
@@ -204,53 +231,37 @@ onMounted(async () => {
</UBadge>
</template>
<!-- START -->
<template #started_at-data="{ row }">
<!-- Urlaub / Krankheit nur Tag -->
<span v-if="row.type === 'vacation' || row.type === 'sick'">
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
</span>
<!-- Arbeitszeit / andere Datum + Uhrzeit -->
<span v-else>
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
</span>
</template>
<!-- ENDE -->
<template #stopped_at-data="{ row }">
<span v-if="!row.stopped_at" class="text-primary-500 font-medium">
läuft...
</span>
<!-- Urlaub / Krankheit nur Tag -->
<span v-else-if="row.type === 'vacation' || row.type === 'sick'">
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
</span>
<!-- Arbeitszeit / andere Datum + Uhrzeit -->
<span v-else>
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
</span>
</template>
<template #duration_minutes-data="{ row }">
<!-- Urlaub / Krankheit Tage anzeigen -->
<span v-if="row.type === 'vacation' || row.type === 'sick'">
<!-- {{ useFormatDurationDays(row.startet_at, row.stopped_at) }}-->--
</span>
<!-- Arbeitszeit / andere Minutenformat -->
</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'">
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted' && canViewAll">
<UButton
variant="ghost"
icon="i-heroicons-check-circle"
@@ -272,9 +283,7 @@ onMounted(async () => {
/>
</UTooltip>
</template>
<template #user-data="{ row }">
{{users.find(i => i.user_id === row.user_id) ? users.find(i => i.user_id === row.user_id).full_name : ""}}
</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>
@@ -284,22 +293,16 @@ onMounted(async () => {
</UDashboardPanelContent>
</template>
<!-- ============================= -->
<!-- MOBILE VERSION -->
<!-- ============================= -->
<template v-else>
<UDashboardNavbar title="Zeiterfassung" />
<div class="relative flex flex-col h-[100dvh] overflow-hidden">
<!-- 🔥 FIXED ACTIVE TIMER -->
<div class="p-4 bg-white dark:bg-gray-900 border-b sticky top-0 z-20 shadow-sm">
<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>
@@ -328,17 +331,15 @@ onMounted(async () => {
<UButton
color="gray"
icon="i-heroicons-chart-bar"
label="Eigene Auswertung"
label="Auswertung"
class="w-full"
variant="soft"
@click="router.push(`/staff/time/${auth.user.id}/evaluate`)"
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
/>
</div>
<!-- 📜 SCROLLABLE CONTENT -->
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
<!-- ZEIT-CARDS -->
<UCard
v-for="row in entries"
:key="row.id"
@@ -349,30 +350,16 @@ onMounted(async () => {
<div class="font-semibold flex items-center gap-2">
<span>{{ row.description || 'Keine Beschreibung' }}</span>
<UBadge
:color="typeColor[row.type]"
class="text-xs"
>
<UBadge :color="typeColor[row.type]" class="text-xs">
{{ typeLabel[row.type] }}
</UBadge>
</div>
<UBadge
:color="{
approved: 'primary',
submitted: 'cyan',
draft: 'red'
}[row.state]"
:color="{ approved: 'primary', submitted: 'cyan', draft: 'red' }[row.state]"
>
{{
{
approved: 'Genehmigt',
submitted: 'Eingereicht',
draft: 'Entwurf'
}[row.state] || row.state
}}
{{ { approved: 'Genehmigt', submitted: 'Eingereicht', draft: 'Entwurf' }[row.state] || row.state }}
</UBadge>
</div>
<p class="text-sm text-gray-500 mt-1">
@@ -382,8 +369,8 @@ onMounted(async () => {
<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>
{{ 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>
@@ -392,7 +379,6 @@ onMounted(async () => {
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
</p>
<!-- ACTION-BUTTONS -->
<div class="flex gap-2 mt-3">
<UButton
v-if="row.state === 'draft'"
@@ -402,21 +388,18 @@ onMounted(async () => {
variant="soft"
@click.stop="handleSubmit(row)"
/>
<!-- <UButton
v-if="row.state === 'submitted'"
<UButton
v-if="row.state === 'submitted' && canViewAll"
color="primary"
icon="i-heroicons-check"
label="Genehmigen"
variant="soft"
@click.stop="handleApprove(row)"
/>-->
/>
</div>
</UCard>
</UDashboardPanelContent>
<!-- FLOATING ACTION BUTTON -->
<FloatingActionButton
icon="i-heroicons-plus"
class="!fixed bottom-6 right-6 z-50"
@@ -427,7 +410,6 @@ onMounted(async () => {
</div>
</template>
<!-- MODAL -->
<StaffTimeEntryModal
v-model="showModal"
:entry="editEntry"
@@ -436,4 +418,4 @@ onMounted(async () => {
:can-select-user="canViewAll"
:default-user-id="selectedUser"
/>
</template>
</template>