Fixed Times
This commit is contained in:
@@ -7,7 +7,20 @@ export const useFunctions = () => {
|
|||||||
const supabase = useSupabaseClient()
|
const supabase = useSupabaseClient()
|
||||||
|
|
||||||
const getWorkingTimesEvaluationData = async (user_id, startDate, endDate) => {
|
const getWorkingTimesEvaluationData = async (user_id, startDate, endDate) => {
|
||||||
return (await useNuxtApp().$api(`/api/functions/timeevaluation/${user_id}?start_date=${startDate}&end_date=${endDate}`))
|
// Der neue Endpunkt ist /staff/time/evaluation und erwartet die Benutzer-ID als targetUserId Query-Parameter.
|
||||||
|
|
||||||
|
// Wir bauen den Query-String zusammen.
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
from: startDate,
|
||||||
|
to: endDate,
|
||||||
|
targetUserId: user_id, // Die ID wird als targetUserId übergeben
|
||||||
|
});
|
||||||
|
|
||||||
|
// Der neue API-Pfad verwendet nur noch den Basis-Endpunkt.
|
||||||
|
const url = `/api/staff/time/evaluation?${queryParams.toString()}`;
|
||||||
|
|
||||||
|
// Annahme: useNuxtApp().$api führt den GET-Request aus und liefert die Daten zurück.
|
||||||
|
return (await useNuxtApp().$api(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
const useNextNumber = async (numberRange) => {
|
const useNextNumber = async (numberRange) => {
|
||||||
|
|||||||
@@ -1,77 +1,160 @@
|
|||||||
interface StaffTimeEntry {
|
import { defineStore } from 'pinia'
|
||||||
id: string
|
import { useAuthStore } from '~/stores/auth'
|
||||||
started_at: string
|
|
||||||
stopped_at?: string | null
|
|
||||||
duration_minutes?: number | null
|
|
||||||
type: string
|
|
||||||
description?: string | null
|
|
||||||
created_at?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useStaffTime() {
|
export const useStaffTime = () => {
|
||||||
const { $api } = useNuxtApp()
|
const { $api, $dayjs } = useNuxtApp()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Zeitspannen (Spans) für die Liste.
|
||||||
|
* Nutzt jetzt GET /api/staff/time/spans
|
||||||
|
*/
|
||||||
|
const list = async (filter?: { user_id?: string, from?: string, to?: string }) => {
|
||||||
|
// Standard: Aktueller Monat
|
||||||
|
const from = filter?.from || $dayjs().startOf('month').format('YYYY-MM-DD')
|
||||||
|
const to = filter?.to || $dayjs().endOf('month').format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
// Ziel-User: Entweder aus Filter (Admin-Ansicht) oder eigener User
|
||||||
|
const targetUserId = filter?.user_id || auth.user.id
|
||||||
|
|
||||||
async function list(params?: { user_id?: string }) {
|
const params = new URLSearchParams({
|
||||||
const query = new URLSearchParams()
|
from,
|
||||||
if (params?.user_id) query.append("user_id", params.user_id)
|
to,
|
||||||
|
// Der Endpoint erwartet targetUserId, wenn man Daten eines anderen Users will
|
||||||
|
targetUserId
|
||||||
|
})
|
||||||
|
|
||||||
return await $api(`/api/staff/time${query.toString() ? `?${query}` : ''}`, { method: 'GET' })
|
try {
|
||||||
|
// 💡 UPDATE: Prefix /api hinzugefügt
|
||||||
|
const spans = await $api(`/api/staff/time/spans?${params.toString()}`)
|
||||||
|
|
||||||
|
return (spans || []).map((span: any) => {
|
||||||
|
const start = $dayjs(span.startedAt)
|
||||||
|
// Wenn endedAt null ist, läuft die Zeit noch -> Dauer bis "jetzt" berechnen für Anzeige
|
||||||
|
const end = span.endedAt ? $dayjs(span.endedAt) : $dayjs()
|
||||||
|
const duration = end.diff(start, 'minute')
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ID: Wir nehmen die erste Event-ID (Start-Event) als Referenz für Aktionen
|
||||||
|
id: span.sourceEventIds && span.sourceEventIds.length > 0 ? span.sourceEventIds[0] : null,
|
||||||
|
|
||||||
|
// Mapping Backend-Status -> Frontend-State
|
||||||
|
state: span.status,
|
||||||
|
|
||||||
|
// Zeitstempel
|
||||||
|
started_at: span.startedAt,
|
||||||
|
stopped_at: span.endedAt,
|
||||||
|
|
||||||
|
duration_minutes: duration,
|
||||||
|
|
||||||
|
// Da der Endpoint nur die Spans zurückgibt, setzen wir die UserID
|
||||||
|
// auf den User, den wir angefragt haben.
|
||||||
|
user_id: targetUserId,
|
||||||
|
|
||||||
|
type: span.type,
|
||||||
|
|
||||||
|
// Payload/Description falls vorhanden
|
||||||
|
description: span.payload?.description || ''
|
||||||
|
}
|
||||||
|
}).sort((a: any, b: any) => $dayjs(b.started_at).valueOf() - $dayjs(a.started_at).valueOf()) // Sortierung: Neueste oben
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Laden der Zeiten:", error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start(description?: string) {
|
/**
|
||||||
return await $api<StaffTimeEntry>('/api/staff/time', {
|
* Startet einen neuen Zeiteintrag.
|
||||||
|
* POST /api/staff/time/event
|
||||||
|
*/
|
||||||
|
const start = async (description = "Arbeitszeit") => {
|
||||||
|
try {
|
||||||
|
// 💡 UPDATE: Prefix /api hinzugefügt
|
||||||
|
await $api('/api/staff/time/event', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
started_at: new Date().toISOString(),
|
eventtype: 'work_start',
|
||||||
type: 'work',
|
eventtime: new Date().toISOString(),
|
||||||
description,
|
payload: { description }
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Starten:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stop(id: string) {
|
/**
|
||||||
return await $api<StaffTimeEntry>(`/api/staff/time/${id}/stop`, {
|
* Stoppt die aktuelle Zeit.
|
||||||
method: 'PUT',
|
* POST /api/staff/time/event
|
||||||
body: { stopped_at: new Date().toISOString() },
|
*/
|
||||||
})
|
const stop = async (entryId?: string) => {
|
||||||
}
|
try {
|
||||||
|
// 💡 UPDATE: Prefix /api hinzugefügt
|
||||||
async function submit(id: string) {
|
await $api('/api/staff/time/event', {
|
||||||
return await $api<StaffTimeEntry>(`/api/staff/time/${id}`, {
|
method: 'POST',
|
||||||
method: 'PUT',
|
|
||||||
body: { state: 'submitted' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function approve(id: string) {
|
|
||||||
const auth = useAuthStore()
|
|
||||||
const now = useNuxtApp().$dayjs().toISOString()
|
|
||||||
|
|
||||||
return await $api(`/api/staff/time/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
body: {
|
||||||
state: 'approved',
|
eventtype: 'work_end',
|
||||||
//@ts-ignore
|
eventtime: new Date().toISOString()
|
||||||
approved_by: auth.user.id,
|
}
|
||||||
approved_at: now,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Stoppen:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(id: string) {
|
/**
|
||||||
return await $api<StaffTimeEntry>(`/api/staff/time/${id}`, { method: 'GET' })
|
* Reicht einen Eintrag ein.
|
||||||
|
* POST /api/staff/time/submit
|
||||||
|
*/
|
||||||
|
const submit = async (entryId: string) => {
|
||||||
|
if (!entryId) return
|
||||||
|
try {
|
||||||
|
// 💡 UPDATE: Prefix /api hinzugefügt
|
||||||
|
await $api('/api/staff/time/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
eventIds: [entryId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Einreichen:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(data: Record<string, any>) {
|
/**
|
||||||
return await $api('/api/staff/time', { method: 'POST', body: data })
|
* Genehmigt einen Eintrag.
|
||||||
|
* POST /api/staff/time/approve
|
||||||
|
*/
|
||||||
|
const approve = async (entry: any) => {
|
||||||
|
if (!entry || !entry.id || !entry.user_id) {
|
||||||
|
console.error("Ungültiger Eintrag für Genehmigung (ID oder UserID fehlt)", entry)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(id: string, data: Record<string, any>) {
|
try {
|
||||||
return await $api(`/api/staff/time/${id}`, { method: 'PUT', body: data })
|
// 💡 UPDATE: Prefix /api hinzugefügt
|
||||||
|
await $api('/api/staff/time/approve', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
eventIds: [entry.id],
|
||||||
|
employeeUserId: entry.user_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Genehmigen:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { list, start, stop,submit,approve, get, create, update }
|
return {
|
||||||
|
list,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
submit,
|
||||||
|
approve
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,25 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
// 🔹 State
|
// 🔹 State
|
||||||
const workingtimes = ref([])
|
const workingTimeInfo = ref<{
|
||||||
const absencerequests = ref([])
|
userId: string;
|
||||||
const workingTimeInfo = ref(null)
|
spans: any[]; // Neue Struktur für die Detailansicht/Tabelle
|
||||||
|
summary: {
|
||||||
|
sumWorkingMinutesSubmitted: number;
|
||||||
|
sumWorkingMinutesApproved: number;
|
||||||
|
sumWorkingMinutesRecreationDays: number;
|
||||||
|
sumRecreationDays: number;
|
||||||
|
sumWorkingMinutesVacationDays: number;
|
||||||
|
sumVacationDays: number;
|
||||||
|
sumWorkingMinutesSickDays: number;
|
||||||
|
sumSickDays: number;
|
||||||
|
timeSpanWorkingMinutes: number;
|
||||||
|
saldoApproved: number;
|
||||||
|
saldoSubmitted: number;
|
||||||
|
} | null; // Neue Struktur für die Zusammenfassung
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const platformIsNative = ref(useCapacitor().getIsNative())
|
const platformIsNative = ref(useCapacitor().getIsNative())
|
||||||
|
|
||||||
@@ -20,12 +35,38 @@ const showDocument = ref(false)
|
|||||||
const uri = ref("")
|
const uri = ref("")
|
||||||
const itemInfo = ref({})
|
const itemInfo = ref({})
|
||||||
|
|
||||||
|
const profile = ref(null)
|
||||||
|
|
||||||
|
// 💡 Die ID des Benutzers, dessen Daten wir abrufen (aus der Route)
|
||||||
|
const evaluatedUserId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert Minuten in das Format HH:MM h
|
||||||
|
*/
|
||||||
function formatMinutesToHHMM(minutes = 0) {
|
function formatMinutesToHHMM(minutes = 0) {
|
||||||
const h = Math.floor(minutes / 60)
|
const h = Math.floor(minutes / 60)
|
||||||
const m = Math.floor(minutes % 60)
|
const m = Math.floor(minutes % 60)
|
||||||
return `${h}:${String(m).padStart(2, "0")} h`
|
return `${h}:${String(m).padStart(2, "0")} h`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Dauer zwischen startedAt und endedAt in Minuten.
|
||||||
|
*/
|
||||||
|
function calculateDurationMinutes(start: string, end: string): number {
|
||||||
|
const startTime = $dayjs(start);
|
||||||
|
const endTime = $dayjs(end);
|
||||||
|
return endTime.diff(startTime, 'minute');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert die Dauer (in Minuten) in HH:MM h
|
||||||
|
*/
|
||||||
|
function formatSpanDuration(start: string, end: string): string {
|
||||||
|
const minutes = calculateDurationMinutes(start, end);
|
||||||
|
return formatMinutesToHHMM(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 📅 Zeitraumumschaltung
|
// 📅 Zeitraumumschaltung
|
||||||
function changeRange() {
|
function changeRange() {
|
||||||
const rangeMap = {
|
const rangeMap = {
|
||||||
@@ -56,41 +97,67 @@ function changeRange() {
|
|||||||
loadWorkingTimeInfo()
|
loadWorkingTimeInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = ref(null)
|
// 📊 Daten laden (Initialisierung)
|
||||||
|
|
||||||
// 📊 Daten laden
|
|
||||||
async function setupPage() {
|
async function setupPage() {
|
||||||
await changeRange()
|
await changeRange()
|
||||||
|
|
||||||
profile.value = (await useNuxtApp().$api("/api/tenant/profiles")).data.find(i => i.user_id === route.params.id)
|
// Lade das Profil des Benutzers, der ausgewertet wird (route.params.id)
|
||||||
|
try {
|
||||||
|
const response = await useNuxtApp().$api(`/api/tenant/profiles`);
|
||||||
|
// Findet das Profil des Benutzers, dessen ID in der Route steht
|
||||||
|
profile.value = response.data.find(i => i.user_id === evaluatedUserId.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Laden des Profils:", error);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(profile.value)
|
console.log(profile.value)
|
||||||
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💡 ANGEPASST: Ruft den neuen Endpunkt ab und speichert das gesamte Payload-Objekt
|
||||||
async function loadWorkingTimeInfo() {
|
async function loadWorkingTimeInfo() {
|
||||||
workingTimeInfo.value = await useFunctions().getWorkingTimesEvaluationData(
|
|
||||||
route.params.id,
|
// Erstellt Query-Parameter für den neuen Backend-Endpunkt
|
||||||
selectedStartDay.value,
|
const queryParams = new URLSearchParams({
|
||||||
selectedEndDay.value
|
from: selectedStartDay.value,
|
||||||
)
|
to: selectedEndDay.value,
|
||||||
|
targetUserId: evaluatedUserId.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `/api/staff/time/evaluation?${queryParams.toString()}`;
|
||||||
|
|
||||||
|
// Führt den GET-Request zum neuen Endpunkt aus und speichert das gesamte Payload-Objekt { userId, spans, summary }
|
||||||
|
const data = await useNuxtApp().$api(url);
|
||||||
|
|
||||||
|
workingTimeInfo.value = data;
|
||||||
|
|
||||||
openTab.value = 0
|
openTab.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📄 PDF generieren
|
// 📄 PDF generieren
|
||||||
|
// Frontend (index.vue)
|
||||||
async function generateDocument() {
|
async function generateDocument() {
|
||||||
const path = (await useEntities("letterheads").select("*"))[0].path // TODO SELECT
|
if (!workingTimeInfo.value || !workingTimeInfo.value.summary) return;
|
||||||
|
|
||||||
|
const path = (await useEntities("letterheads").select("*"))[0].path
|
||||||
|
|
||||||
uri.value = await useFunctions().useCreatePDF({
|
uri.value = await useFunctions().useCreatePDF({
|
||||||
full_name: profile.value.full_name,
|
full_name: profile.value.full_name,
|
||||||
employee_number: profile.value.employee_number ? profile.value.employee_number : "-",
|
employee_number: profile.value.employee_number || "-",
|
||||||
...workingTimeInfo.value}, path, "timesheet")
|
|
||||||
|
|
||||||
|
// Wir übergeben das summary-Objekt flach (für Header-Daten)
|
||||||
|
...workingTimeInfo.value.summary,
|
||||||
|
|
||||||
|
// UND wir müssen die Spans explizit übergeben, damit die Tabelle generiert werden kann
|
||||||
|
spans: workingTimeInfo.value.spans
|
||||||
|
}, path, "timesheet")
|
||||||
|
|
||||||
showDocument.value = true
|
showDocument.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileSaved = ref(false)
|
const fileSaved = ref(false)
|
||||||
|
|
||||||
|
// 💾 Datei speichern
|
||||||
async function saveFile() {
|
async function saveFile() {
|
||||||
try {
|
try {
|
||||||
let fileData = {
|
let fileData = {
|
||||||
@@ -107,9 +174,6 @@ async function saveFile() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTabChange(index: number) {
|
async function onTabChange(index: number) {
|
||||||
@@ -118,7 +182,6 @@ async function onTabChange(index: number) {
|
|||||||
|
|
||||||
// Initialisierung
|
// Initialisierung
|
||||||
await setupPage()
|
await setupPage()
|
||||||
changeRange()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -206,22 +269,24 @@ changeRange()
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div v-if="item.label === 'Information'">
|
<div v-if="item.label === 'Information'">
|
||||||
<UCard v-if="workingTimeInfo" class="my-5">
|
|
||||||
|
<UCard v-if="workingTimeInfo && workingTimeInfo.summary" class="my-5">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
||||||
</template>
|
</template>
|
||||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||||
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
|
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSubmitted) }}</b></p>
|
||||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesApproved) }}</b></p>
|
||||||
<p>Feiertagsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b> / {{ workingTimeInfo.sumRecreationDays }} Tage</p>
|
<p>Feiertagsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesRecreationDays) }}</b> / {{ workingTimeInfo.summary.sumRecreationDays }} Tage</p>
|
||||||
<p>Urlaubs-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.sumVacationDays }} Tage</p>
|
<p>Urlaubs-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.summary.sumVacationDays }} Tage</p>
|
||||||
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.sumSickDays }} Tage</p>
|
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.summary.sumSickDays }} Tage</p>
|
||||||
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.timeSpanWorkingMinutes) }}</b></p>
|
||||||
|
|
||||||
<p class="col-span-2">
|
<p class="col-span-2">
|
||||||
Inoffizielles Saldo: <b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
Inoffizielles Saldo: <b>{{ (workingTimeInfo.summary.saldoSubmitted >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoSubmitted)) }}</b>
|
||||||
</p>
|
</p>
|
||||||
<p class="col-span-2">
|
<p class="col-span-2">
|
||||||
Saldo: <b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
Saldo: <b>{{ (workingTimeInfo.summary.saldoApproved >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoApproved)) }}</b>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
@@ -229,33 +294,39 @@ changeRange()
|
|||||||
<UDashboardPanel>
|
<UDashboardPanel>
|
||||||
<UTable
|
<UTable
|
||||||
v-if="workingTimeInfo"
|
v-if="workingTimeInfo"
|
||||||
:rows="workingTimeInfo.times"
|
:rows="workingTimeInfo.spans"
|
||||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||||
:columns="[
|
:columns="[
|
||||||
{ key: 'state', label: 'Status' },
|
{ key: 'status', label: 'Status' },
|
||||||
{ key: 'start', label: 'Start' },
|
{ key: 'startedAt', label: 'Start' },
|
||||||
{ key: 'end', label: 'Ende' },
|
{ key: 'endedAt', label: 'Ende' },
|
||||||
{ key: 'duration', label: 'Dauer' },
|
{ key: 'duration', label: 'Dauer' },
|
||||||
{ key: 'description', label: 'Beschreibung' }
|
{ key: 'type', label: 'Typ' }
|
||||||
]"
|
]"
|
||||||
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
|
@select="(row) => router.push(`/workingtimes/edit/${row.sourceEventIds[0]}`)"
|
||||||
>
|
>
|
||||||
<template #state-data="{row}">
|
<template #status-data="{row}">
|
||||||
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
<span v-if="row.status === '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.status === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
||||||
<span v-else-if="row.state === 'draft'" class="text-red-500">Entwurf</span>
|
<span v-else-if="row.status === 'factual'" class="text-gray-500">Faktisch</span>
|
||||||
|
<span v-else-if="row.status === 'draft'" class="text-red-500">Entwurf</span>
|
||||||
|
<span v-else>{{ row.status }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #start-data="{ row }">
|
<template #startedAt-data="{ row }">
|
||||||
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
{{ $dayjs(row.startedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #end-data="{ row }">
|
<template #endedAt-data="{ row }">
|
||||||
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
{{ $dayjs(row.endedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #duration-data="{ row }">
|
<template #duration-data="{ row }">
|
||||||
{{ useFormatDuration(row.duration_minutes) }}
|
{{ formatSpanDuration(row.startedAt, row.endedAt) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #type-data="{ row }">
|
||||||
|
{{ row.type.charAt(0).toUpperCase() + row.type.slice(1).replace('_', ' ') }}
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
@@ -273,12 +344,8 @@ changeRange()
|
|||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ====================== -->
|
|
||||||
<!-- 📱 MOBILE ANSICHT -->
|
|
||||||
<!-- ====================== -->
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
||||||
<!-- 🔙 Navigation -->
|
|
||||||
<UDashboardNavbar title="Auswertung">
|
<UDashboardNavbar title="Auswertung">
|
||||||
<template #toggle><div></div></template>
|
<template #toggle><div></div></template>
|
||||||
<template #left>
|
<template #left>
|
||||||
@@ -290,9 +357,7 @@ changeRange()
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<!-- 📌 Mobile Zeitraumwahl -->
|
|
||||||
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
||||||
<!-- Predefined Ranges -->
|
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="selectedPresetRange"
|
v-model="selectedPresetRange"
|
||||||
:options="[
|
:options="[
|
||||||
@@ -309,7 +374,6 @@ changeRange()
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Start/End Datum -->
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs text-gray-500 mb-1">Start</p>
|
<p class="text-xs text-gray-500 mb-1">Start</p>
|
||||||
@@ -341,7 +405,6 @@ changeRange()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 📑 Mobile Tabs -->
|
|
||||||
<UTabs
|
<UTabs
|
||||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||||
v-model="openTab"
|
v-model="openTab"
|
||||||
@@ -350,58 +413,50 @@ changeRange()
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
|
|
||||||
<!-- ====================== -->
|
|
||||||
<!-- TAB 1 — INFORMATION -->
|
|
||||||
<!-- ====================== -->
|
|
||||||
<div v-if="item.label === 'Information'" class="space-y-4">
|
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||||
|
|
||||||
<!-- Summary Card -->
|
<UCard v-if="workingTimeInfo && workingTimeInfo.summary" class="mt-3">
|
||||||
<UCard v-if="workingTimeInfo" class="mt-3">
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
|
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSubmitted) }}</b></p>
|
||||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesApproved) }}</b></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Feiertagsausgleich:
|
Feiertagsausgleich:
|
||||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b>
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesRecreationDays) }}</b>
|
||||||
/ {{ workingTimeInfo.sumRecreationDays }} Tage
|
/ {{ workingTimeInfo.summary.sumRecreationDays }} Tage
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Urlaubs-/Berufsschule:
|
Urlaubs-/Berufsschule:
|
||||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b>
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesVacationDays) }}</b>
|
||||||
/ {{ workingTimeInfo.sumVacationDays }} Tage
|
/ {{ workingTimeInfo.summary.sumVacationDays }} Tage
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Krankheitsausgleich:
|
Krankheitsausgleich:
|
||||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b>
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSickDays) }}</b>
|
||||||
/ {{ workingTimeInfo.sumSickDays }} Tage
|
/ {{ workingTimeInfo.summary.sumSickDays }} Tage
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.timeSpanWorkingMinutes) }}</b></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Inoffizielles Saldo:
|
Inoffizielles Saldo:
|
||||||
<b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
<b>{{ (workingTimeInfo.summary.saldoSubmitted >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoSubmitted)) }}</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Saldo:
|
Saldo:
|
||||||
<b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
<b>{{ (workingTimeInfo.summary.saldoApproved >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoApproved)) }}</b>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====================== -->
|
|
||||||
<!-- TAB 2 — BERICHT -->
|
|
||||||
<!-- ====================== -->
|
|
||||||
<div v-else-if="item.label === 'Bericht'">
|
<div v-else-if="item.label === 'Bericht'">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="uri && !fileSaved"
|
v-if="uri && !fileSaved"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { useStaffTime } from '~/composables/useStaffTime'
|
import { useStaffTime } from '~/composables/useStaffTime'
|
||||||
import { useAuthStore } from '~/stores/auth'
|
import { useAuthStore } from '~/stores/auth'
|
||||||
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
||||||
@@ -12,20 +12,30 @@ const auth = useAuthStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// MOBILE DETECTION
|
// MOBILE DETECTION
|
||||||
const platformIsNative = useCapacitor().getIsNative()
|
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 loading = ref(false)
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editEntry = ref(null)
|
const editEntry = ref(null)
|
||||||
|
|
||||||
// 👥 Nutzer-Filter (nur für Berechtigte)
|
// 👥 Nutzer-Filter
|
||||||
const users = ref([])
|
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'))
|
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 = {
|
const typeLabel = {
|
||||||
work: "Arbeitszeit",
|
work: "Arbeitszeit",
|
||||||
vacation: "Urlaub",
|
vacation: "Urlaub",
|
||||||
@@ -45,21 +55,23 @@ const typeColor = {
|
|||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
if (!canViewAll.value) return
|
if (!canViewAll.value) return
|
||||||
// Beispiel: User aus Supabase holen
|
|
||||||
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
|
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
|
||||||
users.value = res
|
users.value = res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOAD ENTRIES (Erzwingt jetzt immer eine User-ID)
|
||||||
// LOAD ENTRIES (only own entries on mobile)
|
|
||||||
async function load() {
|
async function load() {
|
||||||
entries.value = await list(
|
if (!selectedUser.value) return
|
||||||
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
|
entries.value = await list({
|
||||||
|
user_id: selectedUser.value // 👈 Hier wird der Filter erzwungen
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStart() {
|
async function handleStart() {
|
||||||
|
// Sicherheitshalber: Starten nur erlauben, wenn man sich selbst ansieht
|
||||||
|
if (!isViewingSelf.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await start("Arbeitszeit gestartet")
|
await start("Arbeitszeit gestartet")
|
||||||
await load()
|
await load()
|
||||||
@@ -68,57 +80,64 @@ async function handleStart() {
|
|||||||
|
|
||||||
async function handleStop() {
|
async function handleStop() {
|
||||||
if (!active.value) return
|
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
|
loading.value = true
|
||||||
await stop(active.value.id)
|
await stop(active.value.id)
|
||||||
await load()
|
await load()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(entry: any) {
|
function handleEdit(entry) {
|
||||||
editEntry.value = entry
|
editEntry.value = entry
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(entry: any) {
|
async function handleSubmit(entry) {
|
||||||
await submit(entry.id)
|
await submit(entry)
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApprove(entry: any) {
|
async function handleApprove(entry) {
|
||||||
await approve(entry.id)
|
await approve(entry)
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watcher: Wenn der User im Dropdown gewechselt wird, neu laden
|
||||||
|
watch(selectedUser, () => {
|
||||||
onMounted(async () => {
|
load()
|
||||||
await load()
|
|
||||||
await loadUsers()
|
|
||||||
setPageLayout(platformIsNative ? 'mobile' : 'default')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadUsers()
|
||||||
|
await load() // Lädt initial den eingeloggten User
|
||||||
|
setPageLayout(platformIsNative ? 'mobile' : 'default')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- ============================= -->
|
|
||||||
<!-- DESKTOP VERSION -->
|
|
||||||
<!-- ============================= -->
|
|
||||||
<template v-if="!platformIsNative">
|
<template v-if="!platformIsNative">
|
||||||
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
||||||
|
|
||||||
<UDashboardToolbar>
|
<!--<UDashboardToolbar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
||||||
<span v-if="active" class="text-primary-600 font-medium">
|
<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>
|
||||||
<span v-else class="text-gray-500">Keine aktive Zeit</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
|
<template v-if="isViewingSelf">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="active"
|
v-if="active"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -135,6 +154,19 @@ onMounted(async () => {
|
|||||||
label="Starten"
|
label="Starten"
|
||||||
@click="handleStart"
|
@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
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
@@ -142,28 +174,22 @@ onMounted(async () => {
|
|||||||
@click="() => { editEntry = null; showModal = true }"
|
@click="() => { editEntry = null; showModal = true }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>-->
|
||||||
|
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<!-- 👥 User-Filter (nur bei Berechtigung) -->
|
|
||||||
<div v-if="canViewAll" class="flex items-center gap-2">
|
<div v-if="canViewAll" class="flex items-center gap-2">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="selectedUser"
|
v-model="selectedUser"
|
||||||
:options="[
|
:options="users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))"
|
||||||
{ label: 'Alle Benutzer', value: null },
|
|
||||||
...users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))
|
|
||||||
]"
|
|
||||||
placeholder="Benutzer auswählen"
|
placeholder="Benutzer auswählen"
|
||||||
value-attribute="value"
|
value-attribute="value"
|
||||||
option-attribute="label"
|
option-attribute="label"
|
||||||
class="min-w-[220px]"
|
class="min-w-[220px]"
|
||||||
@change="load"
|
:clearable="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 🔹 Button zur Auswertung -->
|
<UTooltip text="Anwesenheiten auswerten">
|
||||||
<UTooltip
|
|
||||||
:text="selectedUser ? 'Anwesenheiten des Mitarbeiters auswerten' : 'Mitarbeiter für die Auswertung auswählen'"
|
|
||||||
>
|
|
||||||
<UButton
|
<UButton
|
||||||
:disabled="!selectedUser"
|
:disabled="!selectedUser"
|
||||||
color="gray"
|
color="gray"
|
||||||
@@ -188,10 +214,11 @@ onMounted(async () => {
|
|||||||
{ key: 'started_at', label: 'Start' },
|
{ key: 'started_at', label: 'Start' },
|
||||||
{ key: 'stopped_at', label: 'Ende' },
|
{ key: 'stopped_at', label: 'Ende' },
|
||||||
{ key: 'duration_minutes', label: 'Dauer' },
|
{ 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: 'type', label: 'Typ' },
|
||||||
{ key: 'description', label: 'Beschreibung' },
|
{ key: 'description', label: 'Beschreibung' },
|
||||||
]"
|
]"
|
||||||
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
|
||||||
>
|
>
|
||||||
<template #state-data="{ row }">
|
<template #state-data="{ row }">
|
||||||
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
||||||
@@ -204,53 +231,37 @@ onMounted(async () => {
|
|||||||
</UBadge>
|
</UBadge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- START -->
|
|
||||||
<template #started_at-data="{ row }">
|
<template #started_at-data="{ row }">
|
||||||
<!-- Urlaub / Krankheit → nur Tag -->
|
|
||||||
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Arbeitszeit / andere → Datum + Uhrzeit -->
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<!-- ENDE -->
|
|
||||||
<template #stopped_at-data="{ row }">
|
<template #stopped_at-data="{ row }">
|
||||||
<span v-if="!row.stopped_at" class="text-primary-500 font-medium">
|
<span v-if="!row.stopped_at" class="text-primary-500 font-medium">
|
||||||
läuft...
|
läuft...
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Urlaub / Krankheit → nur Tag -->
|
|
||||||
<span v-else-if="row.type === 'vacation' || row.type === 'sick'">
|
<span v-else-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Arbeitszeit / andere → Datum + Uhrzeit -->
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #duration_minutes-data="{ row }">
|
<template #duration_minutes-data="{ row }">
|
||||||
|
|
||||||
<!-- Urlaub / Krankheit → Tage anzeigen -->
|
|
||||||
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
<!-- {{ useFormatDurationDays(row.startet_at, row.stopped_at) }}-->--
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Arbeitszeit / andere → Minutenformat -->
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions-data="{ row }">
|
<template #actions-data="{ row }">
|
||||||
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted'">
|
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted' && canViewAll">
|
||||||
<UButton
|
<UButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="i-heroicons-check-circle"
|
icon="i-heroicons-check-circle"
|
||||||
@@ -272,9 +283,7 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</template>
|
</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 }">
|
<template #description-data="{ row }">
|
||||||
<span v-if="row.type === 'vacation'">{{row.vacation_reason}}</span>
|
<span v-if="row.type === 'vacation'">{{row.vacation_reason}}</span>
|
||||||
<span v-else-if="row.type === 'sick'">{{row.sick_reason}}</span>
|
<span v-else-if="row.type === 'sick'">{{row.sick_reason}}</span>
|
||||||
@@ -284,22 +293,16 @@ onMounted(async () => {
|
|||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ============================= -->
|
|
||||||
<!-- MOBILE VERSION -->
|
|
||||||
<!-- ============================= -->
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<UDashboardNavbar title="Zeiterfassung" />
|
<UDashboardNavbar title="Zeiterfassung" />
|
||||||
|
|
||||||
<div class="relative flex flex-col h-[100dvh] overflow-hidden">
|
<div class="relative flex flex-col h-[100dvh] overflow-hidden">
|
||||||
|
|
||||||
<!-- 🔥 FIXED ACTIVE TIMER -->
|
<div v-if="isViewingSelf" class="p-4 bg-white dark:bg-gray-900 border-b sticky top-0 z-20 shadow-sm">
|
||||||
<div class="p-4 bg-white dark:bg-gray-900 border-b sticky top-0 z-20 shadow-sm">
|
|
||||||
<UCard class="p-3">
|
<UCard class="p-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">Aktive Zeit</p>
|
<p class="text-gray-500 text-sm">Aktive Zeit</p>
|
||||||
|
|
||||||
<p v-if="active" class="text-primary-600 font-semibold">
|
<p v-if="active" class="text-primary-600 font-semibold">
|
||||||
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -328,17 +331,15 @@ onMounted(async () => {
|
|||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-heroicons-chart-bar"
|
icon="i-heroicons-chart-bar"
|
||||||
label="Eigene Auswertung"
|
label="Auswertung"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click="router.push(`/staff/time/${auth.user.id}/evaluate`)"
|
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 📜 SCROLLABLE CONTENT -->
|
|
||||||
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
||||||
|
|
||||||
<!-- ZEIT-CARDS -->
|
|
||||||
<UCard
|
<UCard
|
||||||
v-for="row in entries"
|
v-for="row in entries"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
@@ -349,30 +350,16 @@ onMounted(async () => {
|
|||||||
<div class="font-semibold flex items-center gap-2">
|
<div class="font-semibold flex items-center gap-2">
|
||||||
<span>{{ row.description || 'Keine Beschreibung' }}</span>
|
<span>{{ row.description || 'Keine Beschreibung' }}</span>
|
||||||
|
|
||||||
<UBadge
|
<UBadge :color="typeColor[row.type]" class="text-xs">
|
||||||
:color="typeColor[row.type]"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
{{ typeLabel[row.type] }}
|
{{ typeLabel[row.type] }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UBadge
|
<UBadge
|
||||||
:color="{
|
:color="{ approved: 'primary', submitted: 'cyan', draft: 'red' }[row.state]"
|
||||||
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>
|
</UBadge>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
@@ -392,7 +379,6 @@ onMounted(async () => {
|
|||||||
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ACTION-BUTTONS -->
|
|
||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="row.state === 'draft'"
|
v-if="row.state === 'draft'"
|
||||||
@@ -402,21 +388,18 @@ onMounted(async () => {
|
|||||||
variant="soft"
|
variant="soft"
|
||||||
@click.stop="handleSubmit(row)"
|
@click.stop="handleSubmit(row)"
|
||||||
/>
|
/>
|
||||||
|
<UButton
|
||||||
<!-- <UButton
|
v-if="row.state === 'submitted' && canViewAll"
|
||||||
v-if="row.state === 'submitted'"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="i-heroicons-check"
|
icon="i-heroicons-check"
|
||||||
label="Genehmigen"
|
label="Genehmigen"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.stop="handleApprove(row)"
|
@click.stop="handleApprove(row)"
|
||||||
/>-->
|
/>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
<!-- ➕ FLOATING ACTION BUTTON -->
|
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
class="!fixed bottom-6 right-6 z-50"
|
class="!fixed bottom-6 right-6 z-50"
|
||||||
@@ -427,7 +410,6 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- MODAL -->
|
|
||||||
<StaffTimeEntryModal
|
<StaffTimeEntryModal
|
||||||
v-model="showModal"
|
v-model="showModal"
|
||||||
:entry="editEntry"
|
:entry="editEntry"
|
||||||
|
|||||||
Reference in New Issue
Block a user