482 lines
16 KiB
Vue
482 lines
16 KiB
Vue
<script setup lang="ts">
|
|
const { $dayjs } = useNuxtApp()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const auth = useAuthStore()
|
|
const toast = useToast()
|
|
|
|
// 🔹 State
|
|
const workingTimeInfo = ref<{
|
|
userId: string;
|
|
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(false)
|
|
|
|
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({})
|
|
|
|
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) {
|
|
const h = Math.floor(minutes / 60)
|
|
const m = Math.floor(minutes % 60)
|
|
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
|
|
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()
|
|
}
|
|
|
|
// 📊 Daten laden (Initialisierung)
|
|
async function setupPage() {
|
|
await changeRange()
|
|
|
|
// 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)
|
|
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
|
}
|
|
|
|
// 💡 ANGEPASST: Ruft den neuen Endpunkt ab und speichert das gesamte Payload-Objekt
|
|
async function loadWorkingTimeInfo() {
|
|
|
|
// Erstellt Query-Parameter für den neuen Backend-Endpunkt
|
|
const queryParams = new URLSearchParams({
|
|
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
|
|
}
|
|
|
|
// 📄 PDF generieren
|
|
// Frontend (index.vue)
|
|
async function generateDocument() {
|
|
if (!workingTimeInfo.value || !workingTimeInfo.value.summary) return;
|
|
|
|
const path = (await useEntities("letterheads").select("*"))[0].path
|
|
|
|
uri.value = await useFunctions().useCreatePDF({
|
|
full_name: profile.value.full_name,
|
|
employee_number: profile.value.employee_number || "-",
|
|
|
|
// 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
|
|
}
|
|
|
|
const fileSaved = ref(false)
|
|
|
|
// 💾 Datei speichern
|
|
async function saveFile() {
|
|
try {
|
|
let fileData = {
|
|
auth_profile: profile.value.id,
|
|
tenant: auth.activeTenant
|
|
}
|
|
|
|
let file = useFiles().dataURLtoFile(uri.value, `${profile.value.full_name}-${$dayjs(selectedStartDay.value).format("YYYY-MM-DD")}-${$dayjs(selectedEndDay.value).format("YYYY-MM-DD")}.pdf`)
|
|
|
|
await useFiles().uploadFiles(fileData, [file])
|
|
|
|
toast.add({title:"Auswertung erfolgreich gespeichert"})
|
|
fileSaved.value = true
|
|
} catch (error) {
|
|
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
|
}
|
|
}
|
|
|
|
async function onTabChange(index: number) {
|
|
if (index === 1) await generateDocument()
|
|
}
|
|
|
|
// Initialisierung
|
|
await setupPage()
|
|
</script>
|
|
|
|
<template>
|
|
<template v-if="!platformIsNative">
|
|
<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>
|
|
<template #right>
|
|
<UTooltip
|
|
:text="fileSaved ? 'Bericht bereits gespeichert' : 'Bericht speichern'"
|
|
v-if="openTab === 1 && uri"
|
|
>
|
|
<UButton
|
|
icon="i-mdi-content-save"
|
|
:disabled="fileSaved"
|
|
@click="saveFile"
|
|
>Bericht</UButton>
|
|
</UTooltip>
|
|
|
|
</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 && workingTimeInfo.summary" 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.summary.sumWorkingMinutesSubmitted) }}</b></p>
|
|
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesApproved) }}</b></p>
|
|
<p>Feiertagsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesRecreationDays) }}</b> / {{ workingTimeInfo.summary.sumRecreationDays }} Tage</p>
|
|
<p>Urlaubs-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.summary.sumVacationDays }} Tage</p>
|
|
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.summary.sumSickDays }} Tage</p>
|
|
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.timeSpanWorkingMinutes) }}</b></p>
|
|
|
|
<p class="col-span-2">
|
|
Inoffizielles Saldo: <b>{{ (workingTimeInfo.summary.saldoSubmitted >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoSubmitted)) }}</b>
|
|
</p>
|
|
<p class="col-span-2">
|
|
Saldo: <b>{{ (workingTimeInfo.summary.saldoApproved >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoApproved)) }}</b>
|
|
</p>
|
|
</div>
|
|
</UCard>
|
|
|
|
<UDashboardPanel>
|
|
<UTable
|
|
v-if="workingTimeInfo"
|
|
:rows="workingTimeInfo.spans"
|
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
|
:columns="[
|
|
{ key: 'status', label: 'Status' },
|
|
{ key: 'startedAt', label: 'Start' },
|
|
{ key: 'endedAt', label: 'Ende' },
|
|
{ key: 'duration', label: 'Dauer' },
|
|
{ key: 'type', label: 'Typ' }
|
|
]"
|
|
@select="(row) => router.push(`/workingtimes/edit/${row.sourceEventIds[0]}`)"
|
|
>
|
|
<template #status-data="{row}">
|
|
<span v-if="row.status === 'approved'" class="text-primary-500">Genehmigt</span>
|
|
<span v-else-if="row.status === 'submitted'" class="text-cyan-500">Eingereicht</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 #startedAt-data="{ row }">
|
|
{{ $dayjs(row.startedAt).format('HH:mm DD.MM.YY') }} Uhr
|
|
</template>
|
|
|
|
<template #endedAt-data="{ row }">
|
|
{{ $dayjs(row.endedAt).format('HH:mm DD.MM.YY') }} Uhr
|
|
</template>
|
|
|
|
<template #duration-data="{ row }">
|
|
{{ formatSpanDuration(row.startedAt, row.endedAt) }}
|
|
</template>
|
|
|
|
<template #type-data="{ row }">
|
|
{{ row.type.charAt(0).toUpperCase() + row.type.slice(1).replace('_', ' ') }}
|
|
</template>
|
|
</UTable>
|
|
</UDashboardPanel>
|
|
</div>
|
|
|
|
<div v-else-if="item.label === 'Bericht'">
|
|
<PDFViewer
|
|
v-if="showDocument"
|
|
:uri="uri"
|
|
location="show_time_evaluation"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</UTabs>
|
|
</UDashboardPanelContent>
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<UDashboardNavbar title="Auswertung">
|
|
<template #toggle><div></div></template>
|
|
<template #left>
|
|
<UButton
|
|
icon="i-heroicons-chevron-left"
|
|
variant="ghost"
|
|
@click="router.push('/staff/time')"
|
|
/>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
|
<USelectMenu
|
|
v-model="selectedPresetRange"
|
|
:options="[
|
|
'Dieser Monat bis heute',
|
|
'Diese Woche',
|
|
'Dieser Monat',
|
|
'Dieses Jahr',
|
|
'Letzte Woche',
|
|
'Letzter Monat',
|
|
'Letztes Jahr'
|
|
]"
|
|
@change="changeRange"
|
|
placeholder="Zeitraum wählen"
|
|
class="w-full"
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<p class="text-xs text-gray-500 mb-1">Start</p>
|
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
|
<UButton
|
|
icon="i-heroicons-calendar"
|
|
class="w-full"
|
|
:label="$dayjs(selectedStartDay).format('DD.MM.YYYY')"
|
|
/>
|
|
<template #panel>
|
|
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
|
</template>
|
|
</UPopover>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="text-xs text-gray-500 mb-1">Ende</p>
|
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
|
<UButton
|
|
icon="i-heroicons-calendar"
|
|
class="w-full"
|
|
:label="$dayjs(selectedEndDay).format('DD.MM.YYYY')"
|
|
/>
|
|
<template #panel>
|
|
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
|
</template>
|
|
</UPopover>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<UTabs
|
|
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
|
v-model="openTab"
|
|
@change="onTabChange"
|
|
class="mt-3 mx-3"
|
|
>
|
|
<template #item="{ item }">
|
|
|
|
<div v-if="item.label === 'Information'" class="space-y-4">
|
|
|
|
<UCard v-if="workingTimeInfo && workingTimeInfo.summary" class="mt-3">
|
|
<template #header>
|
|
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
|
</template>
|
|
|
|
<div class="space-y-2 text-sm">
|
|
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSubmitted) }}</b></p>
|
|
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesApproved) }}</b></p>
|
|
|
|
<p>
|
|
Feiertagsausgleich:
|
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesRecreationDays) }}</b>
|
|
/ {{ workingTimeInfo.summary.sumRecreationDays }} Tage
|
|
</p>
|
|
|
|
<p>
|
|
Urlaubs-/Berufsschule:
|
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesVacationDays) }}</b>
|
|
/ {{ workingTimeInfo.summary.sumVacationDays }} Tage
|
|
</p>
|
|
|
|
<p>
|
|
Krankheitsausgleich:
|
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSickDays) }}</b>
|
|
/ {{ workingTimeInfo.summary.sumSickDays }} Tage
|
|
</p>
|
|
|
|
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.timeSpanWorkingMinutes) }}</b></p>
|
|
|
|
<p>
|
|
Inoffizielles Saldo:
|
|
<b>{{ (workingTimeInfo.summary.saldoSubmitted >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoSubmitted)) }}</b>
|
|
</p>
|
|
|
|
<p>
|
|
Saldo:
|
|
<b>{{ (workingTimeInfo.summary.saldoApproved >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoApproved)) }}</b>
|
|
</p>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<div v-else-if="item.label === 'Bericht'">
|
|
<UButton
|
|
v-if="uri && !fileSaved"
|
|
icon="i-mdi-content-save"
|
|
color="primary"
|
|
class="w-full mb-3"
|
|
@click="saveFile"
|
|
>
|
|
Bericht speichern
|
|
</UButton>
|
|
|
|
<PDFViewer
|
|
v-if="showDocument"
|
|
:uri="uri"
|
|
location="show_time_evaluation"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</UTabs>
|
|
</template>
|
|
|
|
|
|
</template> |