Redone Times

This commit is contained in:
2025-11-08 18:49:21 +01:00
parent 26b7dfc06c
commit cf31d43702
11 changed files with 536 additions and 2055 deletions

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import dayjs from "dayjs";
const props = defineProps<{
modelValue: boolean;
entry?: null;
}>();
const emit = defineEmits(["update:modelValue", "saved"]);
const { create, update } = useStaffTime();
// v-model für das Modal
const show = computed({
get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v),
});
// 🧱 Lokale reactive Kopie, die beim Öffnen aus props.entry befüllt wird
const local = reactive<{
id?: string;
description: string;
started_at: string;
stopped_at: string | null;
type: string;
}>({
id: "",
description: "",
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
type: "work",
});
// 📡 Wenn das Modal geöffnet wird, Entry-Daten übernehmen
watch(
() => props.entry,
(val) => {
if (val) {
Object.assign(local, {
id: val.id,
description: val.description || "",
started_at: dayjs(val.started_at).format("YYYY-MM-DDTHH:mm"),
stopped_at: val.stopped_at
? dayjs(val.stopped_at).format("YYYY-MM-DDTHH:mm")
: dayjs(val.started_at).add(1, "hour").format("YYYY-MM-DDTHH:mm"),
type: val.type || "work",
});
} else {
Object.assign(local, {
id: "",
description: "",
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
type: "work",
});
}
},
{ immediate: true }
);
const loading = ref(false);
async function handleSubmit() {
loading.value = true;
try {
const payload = {
description: local.description,
started_at: dayjs(local.started_at).toISOString(),
stopped_at: local.stopped_at ? dayjs(local.stopped_at).toISOString() : null,
type: local.type,
};
if (local.id) {
await update(local.id, payload);
} else {
await create(payload);
}
emit("saved");
show.value = false;
} finally {
loading.value = false;
}
}
</script>
<template>
<UModal v-model="show" :ui="{ width: 'w-full sm:max-w-md' }" :key="local.id || 'new'">
<UCard>
<template #header>
<div class="font-semibold text-lg">
{{ local.id ? "Zeit bearbeiten" : "Neue Zeit erfassen" }}
</div>
</template>
<UForm @submit.prevent="handleSubmit" class="space-y-4">
<UFormGroup label="Beschreibung" name="description">
<UInput v-model="local.description" placeholder="Was wurde gemacht?" />
</UFormGroup>
<UFormGroup label="Startzeit" name="started_at">
<UInput v-model="local.started_at" type="datetime-local" />
</UFormGroup>
<UFormGroup label="Endzeit" name="stopped_at">
<UInput v-model="local.stopped_at" type="datetime-local" />
</UFormGroup>
<div class="flex justify-end gap-2 mt-4">
<UButton color="gray" label="Abbrechen" @click="show = false" />
<UButton color="primary" :loading="loading" type="submit" label="Speichern" />
</div>
</UForm>
</UCard>
</UModal>
</template>

8
composables/useFormat.ts Normal file
View File

@@ -0,0 +1,8 @@
export const useFormatDuration = (durationInMinutes:number,) => {
if (!durationInMinutes || durationInMinutes <= 0) return "00:00"
const hrs = Math.floor(durationInMinutes / 60)
const mins = Math.floor(durationInMinutes % 60)
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}`
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,232 +0,0 @@
import {PDFDocument, StandardFonts, rgb} from "pdf-lib"
import dayjs from "dayjs"
const getCoordinatesForPDFLib = (x ,y, page) => {
/*
* @param x the wanted X Parameter in Millimeters from Top Left
* @param y the wanted Y Parameter in Millimeters from Top Left
* @param page the page Object
*
* @returns x,y object
* */
let retX = x * 2.83
let retY = page.getHeight()-(y*2.83)
return {
x: retX,
y: retY
}
}
const getDuration = (time) => {
const minutes = Math.floor(dayjs(time.endDate).diff(dayjs(time.startDate),'minutes',true))
const hours = Math.floor(minutes/60)
return {
//dezimal: dez,
hours: hours,
minutes: minutes,
composed: `${hours}:${String(minutes % 60).padStart(2,"0")} Std`
}
}
export const useCreateWorkingTimesPdf = async (input,backgroundSourceBuffer) => {
const uri = ref("test")
const genPDF = async () => {
const pdfDoc = await PDFDocument.create()
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
let pages = []
let pageCounter = 1
const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer)
const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0])
const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0])
const page1 = pdfDoc.addPage()
page1.drawPage(firstPageBackground, {
x: 0,
y: 0,
})
pages.push(page1)
//Falzmarke 1
/*pages[pageCounter - 1].drawLine({
start: getCoordinatesForPDFLib(0,105,page1),
end: getCoordinatesForPDFLib(7,105,page1),
thickness: 0.25,
color: rgb(0,0,0),
opacity: 1
})*/
//Lochmarke
/*pages[pageCounter - 1].drawLine({
start: getCoordinatesForPDFLib(0,148.5,page1),
end: getCoordinatesForPDFLib(7,148.5,page1),
thickness: 0.25,
color: rgb(0,0,0),
opacity: 1
})*/
//Falzmarke 2
/*pages[pageCounter - 1].drawLine({
start: getCoordinatesForPDFLib(0,210,page1),
end: getCoordinatesForPDFLib(7,210,page1),
thickness: 0.25,
color: rgb(0,0,0),
opacity: 1
})*/
console.log(input)
pages[pageCounter - 1].drawText(`Mitarbeiter: ${input.profile}`,{
x: getCoordinatesForPDFLib(20,65,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,65,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Eingereicht: ${Math.floor(input.sumWorkingMinutesEingereicht/60)}:${String(input.sumWorkingMinutesEingereicht % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Genehmigt: ${Math.floor(input.sumWorkingMinutesApproved/60)}:${String(input.sumWorkingMinutesApproved % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Feiertagsausgleich: ${Math.floor(input.sumWorkingMinutesRecreationDays/60)}:${String(input.sumWorkingMinutesRecreationDays % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Urlaubsausgleich: ${Math.floor(input.sumWorkingMinutesVacationDays/60)}:${String(input.sumWorkingMinutesVacationDays % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Krankheitsausgleich: ${Math.floor(input.sumWorkingMinutesSickDays/60)}:${String(input.sumWorkingMinutesSickDays % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Soll Stunden: ${Math.floor(input.timeSpanWorkingMinutes/60)}:${String(input.timeSpanWorkingMinutes % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Inoffizielles Saldo: ${Math.sign(input.saldoInOfficial) === 1 ? "+" : "-"} ${Math.floor(Math.abs(input.saldoInOfficial/60))}:${String(Math.abs(input.saldoInOfficial) % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Saldo: ${Math.sign(input.saldo) === 1 ? "+" : "-"} ${Math.floor(Math.abs(input.saldo/60))}:${String(Math.abs(input.saldo) % 60).padStart(2,"0")} Std`,{
x: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Start:`,{
x: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Ende:`,{
x: getCoordinatesForPDFLib(60,110,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(60,110,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`Dauer:`,{
x: getCoordinatesForPDFLib(100,110,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(100,110,pages[pageCounter -1]).y,
size: 10,
})
let rowHeight = 115
let splitted = []
let reversedInput = input.times.slice().reverse()
const splittedLength = Math.floor((reversedInput.length - 25) / 40)
splitted.push(reversedInput.slice(0,25))
let lastIndex = 25
for (let i = 0; i < splittedLength; ++i ) {
splitted.push(reversedInput.slice(lastIndex, lastIndex + (i + 1) * 40))
lastIndex = lastIndex + (i + 1) * 40 + 1
}
splitted.push(reversedInput.slice(lastIndex, reversedInput.length))
splitted.forEach((chunk,index) => {
if(index > 0) {
const page = pdfDoc.addPage()
page.drawPage(secondPageBackground, {
x: 0,
y: 0,
})
pages.push(page)
pageCounter++
rowHeight = 20
}
chunk.forEach(time => {
pages[pageCounter - 1].drawText(`${dayjs(time.startDate).format("HH:mm DD.MM.YY")}`,{
x: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`${dayjs(time.endDate).format("HH:mm DD.MM.YY")}`,{
x: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).y,
size: 10,
})
pages[pageCounter - 1].drawText(`${getDuration(time).composed}`,{
x: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).x,
y: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).y,
size: 10,
})
rowHeight += 6
})
})
uri.value = await pdfDoc.saveAsBase64({dataUri: true})
}
await genPDF()
return uri.value
}

View File

@@ -1,8 +0,0 @@
export const useZipCheck = async (zip) => {
const supabase = useSupabaseClient()
const result = (await supabase.from("citys").select().eq("zip",Number(zip)).maybeSingle()).data
return result ? result.short : null
}

View File

@@ -4,6 +4,7 @@ const exports = ref([])
const auth = useAuthStore() const auth = useAuthStore()
const toast = useToast() const toast = useToast()
const router = useRouter()
const setupPage = async () => { const setupPage = async () => {
exports.value = await useNuxtApp().$api("/api/exports",{ exports.value = await useNuxtApp().$api("/api/exports",{
@@ -56,7 +57,10 @@ const createExport = async () => {
<template #right> <template #right>
<UButton <UButton
@click="showCreateExportModal = true" @click="showCreateExportModal = true"
>+ Export</UButton> >+ DATEV</UButton>
<UButton
@click="router.push('/export/create/sepa')"
>+ SEPA</UButton>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UTable <UTable

View File

@@ -0,0 +1,234 @@
<script setup lang="ts">
const { $dayjs } = useNuxtApp()
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
// 🔹 State
const workingtimes = ref([])
const absencerequests = ref([])
const workingTimeInfo = ref(null)
const selectedPresetRange = ref("Dieser Monat bis heute")
const selectedStartDay = ref("")
const selectedEndDay = ref("")
const openTab = ref(0)
const showDocument = ref(false)
const uri = ref("")
const itemInfo = ref({})
function formatMinutesToHHMM(minutes = 0) {
const h = Math.floor(minutes / 60)
const m = Math.floor(minutes % 60)
return `${h}:${String(m).padStart(2, "0")} h`
}
// 📅 Zeitraumumschaltung
function changeRange() {
const rangeMap = {
"Diese Woche": { selector: "isoWeek", subtract: 0 },
"Dieser Monat": { selector: "M", subtract: 0 },
"Dieser Monat bis heute": { selector: "M", subtract: 0 },
"Dieses Jahr": { selector: "y", subtract: 0 },
"Letzte Woche": { selector: "isoWeek", subtract: 1 },
"Letzter Monat": { selector: "M", subtract: 1 },
"Letztes Jahr": { selector: "y", subtract: 1 }
}
const { selector, subtract } = rangeMap[selectedPresetRange.value] || { selector: "M", subtract: 0 }
selectedStartDay.value = $dayjs()
.subtract(subtract, selector === "isoWeek" ? "week" : selector)
.startOf(selector)
.format("YYYY-MM-DD")
selectedEndDay.value =
selectedPresetRange.value === "Dieser Monat bis heute"
? $dayjs().format("YYYY-MM-DD")
: $dayjs()
.subtract(subtract, selector === "isoWeek" ? "week" : selector)
.endOf(selector)
.format("YYYY-MM-DD")
loadWorkingTimeInfo()
}
const profile = ref(null)
// 📊 Daten laden
async function setupPage() {
await changeRange()
profile.value = (await useNuxtApp().$api("/api/tenant/profiles")).data.find(i => i.user_id === route.params.id)
console.log(profile.value)
}
async function loadWorkingTimeInfo() {
workingTimeInfo.value = await useFunctions().getWorkingTimesEvaluationData(
route.params.id,
selectedStartDay.value,
selectedEndDay.value
)
openTab.value = 0
}
// 📄 PDF generieren
async function generateDocument() {
const path = (await useEntities("letterheads").select("*"))[0].path // TODO SELECT
uri.value = await useFunctions().useCreatePDF({
full_name: profile.value.full_name,
...workingTimeInfo.value}, path, "timesheet")
showDocument.value = true
}
async function onTabChange(index: number) {
if (index === 1) await generateDocument()
}
// Initialisierung
await setupPage()
changeRange()
</script>
<template>
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push('/staff/time')"
>
Anwesenheiten
</UButton>
</template>
<template #center>
<h1 class="text-xl font-medium truncate">
Auswertung Anwesenheiten: {{ profile?.full_name || '' }}
</h1>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<UFormGroup label="Zeitraum:">
<USelectMenu
:options="[
'Dieser Monat bis heute',
'Diese Woche',
'Dieser Monat',
'Dieses Jahr',
'Letzte Woche',
'Letzter Monat',
'Letztes Jahr'
]"
v-model="selectedPresetRange"
@change="changeRange"
/>
</UFormGroup>
<UFormGroup label="Start:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="selectedStartDay ? $dayjs(selectedStartDay).format('DD.MM.YYYY') : 'Datum wählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
</template>
</UPopover>
</UFormGroup>
</template>
</UDashboardToolbar>
<UDashboardPanelContent>
<UTabs
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
v-model="openTab"
@change="onTabChange"
>
<template #item="{ item }">
<div v-if="item.label === 'Information'">
<UCard v-if="workingTimeInfo" class="my-5">
<template #header>
<h3 class="text-base font-semibold">Zusammenfassung</h3>
</template>
<div class="grid grid-cols-2 gap-3 text-sm">
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
<p>Feiertagsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b> / {{ workingTimeInfo.sumRecreationDays }} Tage</p>
<p>Urlaubs-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.sumVacationDays }} Tage</p>
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.sumSickDays }} Tage</p>
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
<p class="col-span-2">
Inoffizielles Saldo: <b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
</p>
<p class="col-span-2">
Saldo: <b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
</p>
</div>
</UCard>
<UDashboardPanel>
<UTable
v-if="workingTimeInfo"
:rows="workingTimeInfo.times"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
:columns="[
{ key: 'state', label: 'Status' },
{ key: 'start', label: 'Start' },
{ key: 'end', label: 'Ende' },
{ key: 'duration', label: 'Dauer' },
{ key: 'description', label: 'Beschreibung' }
]"
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
>
<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 #start-data="{ row }">
{{ $dayjs(row.startDate).format('HH:mm DD.MM.YY') }} Uhr
</template>
<template #end-data="{ row }">
{{ $dayjs(row.endDate).format('HH:mm DD.MM.YY') }} Uhr
</template>
<template #duration-data="{ row }">
{{ useFormatDuration(row.duration_minutes) }}
</template>
</UTable>
</UDashboardPanel>
</div>
<div v-else-if="item.label === 'Bericht'">
<PDFViewer
v-if="showDocument"
:uri="uri"
/>
</div>
</template>
</UTabs>
</UDashboardPanelContent>
</template>

173
pages/staff/time/index.vue Normal file
View File

@@ -0,0 +1,173 @@
<script setup lang="ts">
import { useStaffTime } from '~/composables/useStaffTime'
import { useAuthStore } from '~/stores/auth'
const { list, start, stop } = useStaffTime()
const auth = useAuthStore()
const router = useRouter()
const entries = ref([])
const active = computed(() => entries.value.find(e => !e.stopped_at && e.user_id === auth.user.id))
const loading = ref(false)
const showModal = ref(false)
const editEntry = ref(null)
// 👥 Nutzer-Filter (nur für Berechtigte)
const users = ref([])
const selectedUser = ref<string | null>(null)
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
async function loadUsers() {
if (!canViewAll.value) return
// Beispiel: User aus Supabase holen
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
users.value = res
}
async function load() {
entries.value = await list(
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
)
}
async function handleStart() {
loading.value = true
await start('Arbeitszeit gestartet')
await load()
loading.value = false
}
async function handleStop() {
if (!active.value) return
loading.value = true
await stop(active.value.id)
await load()
loading.value = false
}
function handleEdit(entry: any) {
editEntry.value = entry
showModal.value = true
}
onMounted(async () => {
await loadUsers()
await load()
})
</script>
<template>
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
<!-- TOOLBAR -->
<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>
<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"
/>
<UButton
color="primary"
icon="i-heroicons-plus"
label="Zeit"
@click="() => { editEntry = null; showModal = true }"
/>
</template>
</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 }))
]"
placeholder="Benutzer auswählen"
value-attribute="value"
option-attribute="label"
class="min-w-[220px]"
@change="load"
/>
<!-- 🔹 Button zur Auswertung -->
<UButton
v-if="selectedUser"
color="gray"
icon="i-heroicons-chart-bar"
label="Auswertung"
variant="soft"
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
/>
</div>
</template>
</UDashboardToolbar>
<!-- TABELLE -->
<UDashboardPanelContent>
<UTable
:rows="entries"
@select="(row) => handleEdit(row)"
:columns="[
{ key: 'state', label: 'Status' },
{ key: 'started_at', label: 'Start' },
{ key: 'stopped_at', label: 'Ende' },
{ key: 'duration_minutes', label: 'Dauer' },
{ key: 'description', label: 'Beschreibung' },
...(canViewAll ? [{ key: 'user_name', label: 'Benutzer' }] : []),
{ key: 'actions', label: '' },
]"
>
<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 #started_at-data="{ row }">
{{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
</template>
<template #stopped_at-data="{ row }">
<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>
</template>
<template #duration_minutes-data="{ row }">
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
</template>
<template #user_name-data="{ row }">
{{ row.user_id ? users.find(i => i.user_id === row.user_id).full_name : '-' }}
</template>
<template #actions-data="{ row }">
<UButton variant="ghost" icon="i-heroicons-pencil-square" @click="handleEdit(row)" />
</template>
</UTable>
</UDashboardPanelContent>
<!-- MODAL -->
<StaffTimeEntryModal v-model="showModal" :entry="editEntry" @saved="load" />
</template>

View File

@@ -1,150 +0,0 @@
<script setup>
import dayjs from "dayjs";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
startDate: new Date(),
endDate: new Date(),
profile: profileStore.activeProfile.id
})
const oldItemInfo = ref({})
const setupPage = () => {
if(route.params.id && mode.value === 'edit') {
itemInfo.value = dataStore.getWorkingTimeById(Number(route.params.id))
//setStartEnd()
}
oldItemInfo.value = itemInfo.value
if(route.query) {
if(route.query.profile) itemInfo.value.profile = route.query.profile
}
}
/*const setStartEnd = () => {
console.log("test")
console.log(String(itemInfo.value.date).split("T")[0])
itemInfo.value.date = String(itemInfo.value.date).split("T")[0]
itemInfo.value.start = new Date(itemInfo.value.date + "T" + itemInfo.value.start)
itemInfo.value.end = new Date(itemInfo.value.date + "T" + itemInfo.value.end)
}*/
setupPage()
</script>
<template>
<UDashboardNavbar
:title="mode === 'show' ? `Anwesenheit: ${itemInfo.profile}` : (itemInfo.id ? 'Anwesenheit bearbeiten' :'Anwesenheit erstellen')"
>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/workingtimes`)"
>
Anwesenheiten
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
class="text-xl font-medium"
>{{itemInfo.id ? 'Anwesenheit bearbeiten' : 'Anwesenheit erstellen'}}</h1>
</template>
<template #right>
<UButton
color="rose"
v-if="mode === 'edit'"
@click="router.push('/workingtimes')"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'edit' && itemInfo.id"
@click="dataStore.updateItem('workingtimes',itemInfo)"
>
Speichern
</UButton>
<UButton
v-if="mode === 'edit' && itemInfo.id"
@click="dataStore.updateItem('workingtimes',{...itemInfo, approved: true})"
>
Genehmigen & Speichern
</UButton>
<UButton
v-else-if="mode === 'edit' && !itemInfo.id"
@click="dataStore.createNewItem('workingtimes',itemInfo)"
>
Erstellen
</UButton>
</template>
</UDashboardNavbar>
<UForm class="p-5">
<UFormGroup
label="Mitarbeiter:"
>
<USelectMenu
:options="profileStore.profiles"
v-model="itemInfo.profile"
option-attribute="fullName"
value-attribute="id"
/>
</UFormGroup>
<UFormGroup label="Start:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.startDate ? dayjs(itemInfo.startDate).format('HH:mm') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.startDate" @close="itemInfo.endDate = itemInfo.startDate" mode="dateTime"/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.endDate ? dayjs(itemInfo.endDate).format('HH:mm') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.endDate" @close="close" mode="time"/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Genehmigt:"
>
<UCheckbox
v-model="itemInfo.approved"
/>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="itemInfo.notes"
/>
</UFormGroup>
</UForm>
</template>
<style scoped>
</style>

View File

@@ -1,291 +0,0 @@
<script setup>
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import isoWeek from "dayjs/plugin/isoWeek";
import isBetween from "dayjs/plugin/isBetween";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import {useCreateWorkingTimesPdf} from "~/composables/useWorkingTimePDFGenerator.js";
import {useFunctions} from "~/composables/useFunctions.js";
dayjs.extend(customParseFormat)
dayjs.extend(isoWeek)
dayjs.extend(isBetween)
dayjs.extend(isSameOrAfter)
dayjs.extend(isSameOrBefore)
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute()
const router = useRouter()
const itemInfo = ref({})
const oldItemInfo = ref({})
const workingtimes = ref([])
const absencerequests = ref([])
const workingTimeInfo = ref(null)
const selectedPresetRange = ref("Dieser Monat bis heute")
const selectedStartDay = ref("")
const selectedEndDay = ref("")
const setupPage = async () => {
if(route.params.id) itemInfo.value = profileStore.getProfileById(route.params.id)
//if(itemInfo.value.id) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
workingtimes.value = (await supabase.from("workingtimes").select().eq("profile",itemInfo.value.id).order("startDate",{ascending:false})).data
absencerequests.value = (await supabase.from("absencerequests").select().eq("profile",itemInfo.value.id).order("startDate",{ascending: false})).data
await loadWorkingTimeInfo()
}
const loadWorkingTimeInfo = async () => {
workingTimeInfo.value = await useFunctions().getWorkingTimesEvaluationData(route.params.id,selectedStartDay.value,selectedEndDay.value)
console.log(workingTimeInfo.value)
openTab.value = 0
}
const changeRange = () => {
let selector = "M"
let subtract = 0
if(selectedPresetRange.value === "Diese Woche") {
selector = "isoWeek"
subtract = 0
} else if(selectedPresetRange.value === "Dieser Monat") {
selector = "M"
subtract = 0
} else if(selectedPresetRange.value === "Dieser Monat bis heute") {
selector = "M"
subtract = 0
} else if(selectedPresetRange.value === "Dieses Jahr") {
selector = "y"
subtract = 0
} else if(selectedPresetRange.value === "Letzte Woche") {
selector = "isoWeek"
subtract = 1
} else if(selectedPresetRange.value === "Letzter Monat") {
selector = "M"
subtract = 1
} else if(selectedPresetRange.value === "Letztes Jahr") {
selector = "y"
subtract = 1
}
selectedStartDay.value = dayjs().subtract(subtract,selector === "isoWeek" ? "week" : selector).startOf(selector).format("YYYY-MM-DD")
if(selectedPresetRange.value === "Dieser Monat bis heute") {
selectedEndDay.value = dayjs().format("YYYY-MM-DD")
} else {
selectedEndDay.value = dayjs().subtract(subtract,selector === "isoWeek" ? "week" : selector).endOf(selector).format("YYYY-MM-DD")
}
openTab.value = 0
loadWorkingTimeInfo()
}
const getDuration = (time) => {
const minutes = Math.floor(dayjs(time.endDate).diff(dayjs(time.startDate),'minutes',true))
const hours = Math.floor(minutes/60)
return {
//dezimal: dez,
hours: hours,
minutes: minutes,
composed: `${hours}:${String(minutes % 60).padStart(2,"0")} h`
}
}
const showDocument = ref(false)
const uri = ref("")
const generateDocument = async () => {
const ownTenant = profileStore.ownTenant
const path = (await supabase.from("letterheads").select().eq("tenant",profileStore.currentTenant)).data[0].path
console.log(path)
const {data,error} = await supabase.storage.from("files").download(path)
uri.value = await useCreateWorkingTimesPdf({
profile: profileStore.getProfileById(route.params.id).fullName,
...workingTimeInfo.value}, await data.arrayBuffer())
//alert(uri.value)
showDocument.value = true
}
const generateEvaluation = async () => {
await generateDocument()
}
const openTab = ref(0)
const onTabChange = async (index) => {
if(index === 1) {
await generateEvaluation()
}
}
setupPage()
changeRange()
</script>
<template>
<UDashboardNavbar
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/workingtimes`)"
>
Anwesenheiten
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium']"
>{{itemInfo ? `Auswertung Anwesenheiten: ${itemInfo.fullName}` : ``}}</h1>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<UFormGroup
label="Vorlage:"
>
<USelectMenu
:options="['Dieser Monat bis heute','Diese Woche', 'Dieser Monat', 'Dieses Jahr', 'Letzte Woche', 'Letzter Monat', 'Letztes Jahr']"
v-model="selectedPresetRange"
@change="changeRange"
/>
</UFormGroup>
<UFormGroup label="Start:" >
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="selectedStartDay ? dayjs(selectedStartDay).format('DD.MM.YYYY') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="selectedEndDay ? dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
</template>
</UPopover>
</UFormGroup>
</template>
</UDashboardToolbar>
<UDashboardPanelContent>
<UTabs
:items="[{label: 'Information'},{label: 'Bericht'}]"
@change="onTabChange"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Information'">
<UCard class="truncate my-5" v-if="workingTimeInfo">
<template #header>
Zusammenfassung
</template>
<p>Eingreicht: {{Math.floor(workingTimeInfo.sumWorkingMinutesEingereicht/60)}}:{{String(workingTimeInfo.sumWorkingMinutesEingereicht % 60).padStart(2,"0")}} h</p>
<p>Genehmigt: {{Math.floor(workingTimeInfo.sumWorkingMinutesApproved/60)}}:{{String(workingTimeInfo.sumWorkingMinutesApproved % 60).padStart(2,"0")}} h</p>
<p>Feiertagsausgleich: {{Math.floor(workingTimeInfo.sumWorkingMinutesRecreationDays/60)}}:{{String(workingTimeInfo.sumWorkingMinutesRecreationDays % 60).padStart(2,"0")}} h / {{workingTimeInfo.sumRecreationDays}} Tage</p>
<p>Urlaubs-/Berufsschulausgleich: {{Math.floor(workingTimeInfo.sumWorkingMinutesVacationDays/60)}}:{{String(workingTimeInfo.sumWorkingMinutesVacationDays % 60).padStart(2,"0")}} h / {{workingTimeInfo.sumVacationDays}} Tage</p>
<p>Krankheitsausgleich: {{Math.floor(workingTimeInfo.sumWorkingMinutesSickDays/60)}}:{{String(workingTimeInfo.sumWorkingMinutesSickDays % 60).padStart(2,"0")}} h / {{workingTimeInfo.sumSickDays}} Tage</p>
<p>Soll Stunden: {{Math.floor(workingTimeInfo.timeSpanWorkingMinutes/60)}}:{{String(workingTimeInfo.timeSpanWorkingMinutes % 60 ).padStart(2,"0")}} h</p>
<!-- <p>Abwesend: </p>
<p>Ausgleich:</p>
-->
<p>Inoffizielles Saldo(eingereichte Stunden): {{Math.sign(workingTimeInfo.saldoInOfficial) === 1 ? "+" : "-"}} {{Math.floor(Math.abs(workingTimeInfo.saldoInOfficial/60))}}:{{String(Math.abs(workingTimeInfo.saldoInOfficial) % 60).padStart(2,"0")}} h</p>
<p>Saldo(genehmigte Stunden): {{Math.sign(workingTimeInfo.saldo) === 1 ? "+" : "-"}} {{Math.floor(Math.abs(workingTimeInfo.saldo/60))}}:{{String(Math.abs(workingTimeInfo.saldo) % 60).padStart(2,"0")}} h</p>
</UCard>
<div style="overflow-y: scroll; height: 45vh">
<UTable
v-if="workingTimeInfo"
:rows="workingTimeInfo.times"
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine Anwesenheiten anzuzeigen` }"
:columns="[
{
key: 'state',
label: 'Status'
}, {
key: 'approved',
label: 'Genehmigt'
}, {
key: 'start',
label: 'Start'
}, {
key: 'end',
label: 'Ende'
}, {
key: 'duration',
label: 'Dauer'
}, {
key: 'notes',
label: 'Notizen'
}
]"
>
<template #profile-data="{row}">
{{profileStore.profiles.find(profile => profile.id === row.profile) ? profileStore.profiles.find(profile => profile.id === row.profile).fullName : row.profile }}
</template>
<template #approved-data="{row}">
<span v-if="row.approved" class="text-primary-500">Ja</span>
<span v-else class="text-rose-600">Nein</span>
</template>
<template #start-data="{row}">
{{dayjs(row.startDate).format("HH:mm DD.MM.YY")}} Uhr
</template>
<template #end-data="{row}">
{{dayjs(row.endDate).format("HH:mm DD.MM.YY")}} Uhr
</template>
<template #duration-data="{row}">
{{getDuration(row).composed}}
</template>
</UTable>
</div>
</div>
<div v-else-if="item.label === 'Bericht'">
<UProgress animation="carousel" v-if="!showDocument"/>
<object
:data="uri"
v-else
type="application/pdf"
class="w-full previewDocument"
/>
</div>
</template>
</UTabs>
</UDashboardPanelContent>
</template>
<style scoped>
.previewDocument {
height: 80vh;
}
</style>

View File

@@ -1,327 +0,0 @@
<script setup>
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat"
import {useCapacitor} from "~/composables/useCapacitor.js";
import {setPageLayout} from "#app";
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
dayjs.extend(customParseFormat)
const dataStore = useDataStore()
const supabase = useSupabaseClient()
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const platform = await useCapacitor().getIsPhone() ? "mobile" : "default"
const filterUser = ref(auth.profile.old_profile_id || "c149b249-5baf-43a2-9cc0-1947d3710a43")
const profiles = ref([])
const selectableProfiles = ref([])
const workingtimes = ref([])
const setupPage = async () => {
if(platform === "mobile") {
setPageLayout("mobile")
}
if(route.query) {
if(route.query.profile) filterUser.value = route.query.profile
}
//workingtimes.value = (await supabase.from("workingtimes").select().eq("profile",filterUser.value).order("startDate",{ascending: false})).data
workingtimes.value = (await useEntities("workingtimes").select(null,"startDate",false))
let res = await useNuxtApp().$api("/api/tenant/profiles")
console.log(res)
profiles.value = res.data
selectableProfiles.value = res.data.filter(i => i.old_profile_id)
console.log(profiles.value)
console.log(selectableProfiles.value)
}
const changeFilterUser = async () => {
await router.push(`/workingtimes/?profile=${filterUser.value}`)
await setupPage()
}
setupPage()
const filteredRows = computed(() => {
let times = workingtimes.value
times = times.filter(i => i.profile === filterUser.value)
return times/*.map(i => {
return {
...i,
disabledExpand: i.approved
}
})*/
})
const itemInfo = ref({
profile: "",
start: new Date(),
end: "",
notes: null,
})
const columns = [
{
key:"state",
label: "Status"
},
{
key: "approved",
label: "Genehmigt"
},
{
key: "profile",
label: "Mitarbeiter"
},
{
key: "date",
label: "Datum"
},
{
key:"startDate",
label:"Start"
},
{
key: "endDate",
label: "Ende"
},
{
key: "duration",
label: "Dauer"
},
{
key: "notes",
label: "Notizen"
}
]
const getDuration = (time) => {
const minutes = Math.floor(dayjs(time.endDate).diff(dayjs(time.startDate),'minutes',true))
const hours = Math.floor(minutes/60)
return {
//dezimal: dez,
hours: hours,
minutes: minutes,
composed: `${hours}:${String(minutes % 60).padStart(2,"0")} h`
}
}
const updateWorkingTime = async (data) => {
await dataStore.updateItem('workingtimes',data)
await setupPage()
}
const toggleRow = (row) => {
if(expand.value.openedRows.includes(row)){
expand.value.openedRows = []
} else {
expand.value.openedRows = [row]
}
}
const expand = ref({
openedRows: [],
row: {}
})
const setEndDate = (row) => {
row.startDate = dayjs(row.startDate).toISOString()
row.endDate = dayjs(row.endDate).set("year", dayjs(row.startDate).get("year")).set("month", dayjs(row.startDate).get("month")).set("date", dayjs(row.startDate).get("date"))
row.endDate = dayjs(row.endDate).toISOString()
}
</script>
<template>
<!-- <FloatingActionButton
:label="`+ Anwesenheit`"
variant="outline"
v-if="platform === 'mobile'"
@click="router.push(`/workingtimes/create`)"
:pos="0"
/>
<FloatingActionButton
:label="`Auswertung`"
variant="outline"
v-if="platform === 'mobile'"
@click="router.push(`/workingtimes/evaluate/${profileStore.activeProfile.id}`)"
:pos="1"
/>-->
<UDashboardNavbar title="Anwesenheiten" v-if="platform !== 'mobile'" :badge="filteredRows.length">
<template #right>
<UButton
@click="router.push(`/workingtimes/edit?profile=${filterUser}`)"
disabled
>
+ Anwesenheit
</UButton>
</template>
</UDashboardNavbar>
<UDashboardNavbar title="Anwesenheiten" v-else>
<template #toggle>
<div></div>
</template>
</UDashboardNavbar>
<UDashboardToolbar v-if="platform !== 'mobile'">
<template #left>
<USelectMenu
:options="selectableProfiles"
option-attribute="full_name"
value-attribute="old_profile_id"
v-model="filterUser"
@change="changeFilterUser"
>
<template #label>
{{ selectableProfiles.find(i => i.old_profile_id === filterUser) ? selectableProfiles.find(i => i.old_profile_id === filterUser).full_name : "Kein Benutzer ausgewählt"}}
</template>
</USelectMenu>
<UButton
@click="router.push(`/workingtimes/evaluate/${filterUser}`)"
>
Auswertung
</UButton>
</template>
</UDashboardToolbar>
<UTable
class="mt-3"
:columns="columns"
:rows="filteredRows"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
@select="(i) => toggleRow(i)"
v-model:expand="expand"
:multiple-expand="false"
>
<template #expand="{ row }">
<InputGroup class="p-4">
<UTooltip text="Genehmigen & Speichern" v-if="!row.approved">
<UButton
@click="updateWorkingTime({...row, approved: true})"
icon="i-heroicons-check"
/>
</UTooltip>
<UTooltip text="Speichern" v-if="!row.approved">
<UButton
@click="updateWorkingTime(row)"
icon="i-mdi-content-save"
variant="outline"
/>
</UTooltip>
<UTooltip text="Bearbeiten">
<UButton
@click="router.push(`/workingtimes/edit/${row.id}`)"
variant="outline"
icon="i-heroicons-pencil-solid"
/>
</UTooltip>
</InputGroup>
<div class="p-4" v-if="!row.approved">
<UFormGroup label="Start:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="row.startDate ? dayjs(row.startDate).format('HH:mm') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="row.startDate" @close="setEndDate(row)" mode="dateTime"/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
:disabled="!row.endDate"
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="row.endDate ? dayjs(row.endDate).format('HH:mm') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="row.endDate" @close="close" mode="time"/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="row.notes"
/>
</UFormGroup>
</div>
<div class="px-4 pb-4" v-else>
<!--
<p><span class="font-bold">Mitarbeitende/r:</span> {{profileStore.getProfileById(row.profile).fullName}}</p>
-->
<p><span class="font-bold">Start:</span> {{dayjs(row.startDate).format("DD.MM.YYYY HH:mm")}}</p>
<p><span class="font-bold">Ende:</span> {{dayjs(row.endDate).format("DD.MM.YYYY HH:mm")}}</p>
<p><span class="font-bold">Genehmigt:</span> {{row.approved ? "Ja" : "Nein"}}</p>
<p><span class="font-bold">Notizen:</span> {{row.notes}}</p>
</div>
</template>
<template #profile-data="{row}">
{{ profiles.find(i => i.old_profile_id === row.profile) ? profiles.find(i => i.old_profile_id === row.profile).full_name : ""}}
</template>
<template #approved-data="{row}">
<span v-if="row.approved" class="text-primary-500">Ja</span>
<span v-else class="text-rose-600">Nein</span>
</template>
<template #date-data="{row}">
{{dayjs(row.startDate).format("DD.MM.YYYY")}}
</template>
<template #startDate-data="{row}">
{{dayjs(row.startDate).format("HH:mm")}} Uhr
</template>
<template #endDate-data="{row}">
{{row.endDate ? dayjs(row.endDate).format("HH:mm") + " Uhr" : ""}}
</template>
<template #duration-data="{row}">
{{row.endDate ? getDuration(row).composed : ""}}
</template>
</UTable>
</template>
<style scoped>
</style>