Fixed Times
This commit is contained in:
@@ -4,10 +4,25 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 🔹 State
|
||||
const workingtimes = ref([])
|
||||
const absencerequests = ref([])
|
||||
const workingTimeInfo = ref(null)
|
||||
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(useCapacitor().getIsNative())
|
||||
|
||||
@@ -20,12 +35,38 @@ 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 = {
|
||||
@@ -56,41 +97,67 @@ function changeRange() {
|
||||
loadWorkingTimeInfo()
|
||||
}
|
||||
|
||||
const profile = ref(null)
|
||||
|
||||
// 📊 Daten laden
|
||||
// 📊 Daten laden (Initialisierung)
|
||||
async function setupPage() {
|
||||
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)
|
||||
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
||||
|
||||
}
|
||||
|
||||
// 💡 ANGEPASST: Ruft den neuen Endpunkt ab und speichert das gesamte Payload-Objekt
|
||||
async function loadWorkingTimeInfo() {
|
||||
workingTimeInfo.value = await useFunctions().getWorkingTimesEvaluationData(
|
||||
route.params.id,
|
||||
selectedStartDay.value,
|
||||
selectedEndDay.value
|
||||
)
|
||||
|
||||
// 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() {
|
||||
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({
|
||||
full_name: profile.value.full_name,
|
||||
employee_number: profile.value.employee_number ? profile.value.employee_number : "-",
|
||||
...workingTimeInfo.value}, path, "timesheet")
|
||||
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 = {
|
||||
@@ -107,9 +174,6 @@ async function saveFile() {
|
||||
} catch (error) {
|
||||
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function onTabChange(index: number) {
|
||||
@@ -118,7 +182,6 @@ async function onTabChange(index: number) {
|
||||
|
||||
// Initialisierung
|
||||
await setupPage()
|
||||
changeRange()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -206,22 +269,24 @@ changeRange()
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div v-if="item.label === 'Information'">
|
||||
<UCard v-if="workingTimeInfo" class="my-5">
|
||||
|
||||
<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.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>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.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
||||
Inoffizielles Saldo: <b>{{ (workingTimeInfo.summary.saldoSubmitted >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoSubmitted)) }}</b>
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
</UCard>
|
||||
@@ -229,33 +294,39 @@ changeRange()
|
||||
<UDashboardPanel>
|
||||
<UTable
|
||||
v-if="workingTimeInfo"
|
||||
:rows="workingTimeInfo.times"
|
||||
:rows="workingTimeInfo.spans"
|
||||
: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: 'status', label: 'Status' },
|
||||
{ key: 'startedAt', label: 'Start' },
|
||||
{ key: 'endedAt', label: 'Ende' },
|
||||
{ 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}">
|
||||
<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 #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 #start-data="{ row }">
|
||||
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
<template #startedAt-data="{ row }">
|
||||
{{ $dayjs(row.startedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<template #end-data="{ row }">
|
||||
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
<template #endedAt-data="{ row }">
|
||||
{{ $dayjs(row.endedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</UTable>
|
||||
</UDashboardPanel>
|
||||
@@ -273,12 +344,8 @@ changeRange()
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- 📱 MOBILE ANSICHT -->
|
||||
<!-- ====================== -->
|
||||
<template v-else>
|
||||
|
||||
<!-- 🔙 Navigation -->
|
||||
<UDashboardNavbar title="Auswertung">
|
||||
<template #toggle><div></div></template>
|
||||
<template #left>
|
||||
@@ -290,9 +357,7 @@ changeRange()
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<!-- 📌 Mobile Zeitraumwahl -->
|
||||
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
||||
<!-- Predefined Ranges -->
|
||||
<USelectMenu
|
||||
v-model="selectedPresetRange"
|
||||
:options="[
|
||||
@@ -309,7 +374,6 @@ changeRange()
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<!-- Start/End Datum -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">Start</p>
|
||||
@@ -341,7 +405,6 @@ changeRange()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 📑 Mobile Tabs -->
|
||||
<UTabs
|
||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||
v-model="openTab"
|
||||
@@ -350,58 +413,50 @@ changeRange()
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- TAB 1 — INFORMATION -->
|
||||
<!-- ====================== -->
|
||||
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||
|
||||
<!-- Summary Card -->
|
||||
<UCard v-if="workingTimeInfo" class="mt-3">
|
||||
<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.sumWorkingMinutesEingereicht) }}</b></p>
|
||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
||||
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSubmitted) }}</b></p>
|
||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesApproved) }}</b></p>
|
||||
|
||||
<p>
|
||||
Feiertagsausgleich:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumRecreationDays }} Tage
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesRecreationDays) }}</b>
|
||||
/ {{ workingTimeInfo.summary.sumRecreationDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Urlaubs-/Berufsschule:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumVacationDays }} Tage
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesVacationDays) }}</b>
|
||||
/ {{ workingTimeInfo.summary.sumVacationDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Krankheitsausgleich:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumSickDays }} Tage
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.summary.sumWorkingMinutesSickDays) }}</b>
|
||||
/ {{ workingTimeInfo.summary.sumSickDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
||||
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.summary.timeSpanWorkingMinutes) }}</b></p>
|
||||
|
||||
<p>
|
||||
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>
|
||||
Saldo:
|
||||
<b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
||||
<b>{{ (workingTimeInfo.summary.saldoApproved >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.summary.saldoApproved)) }}</b>
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- TAB 2 — BERICHT -->
|
||||
<!-- ====================== -->
|
||||
<div v-else-if="item.label === 'Bericht'">
|
||||
<UButton
|
||||
v-if="uri && !fileSaved"
|
||||
@@ -424,4 +479,4 @@ changeRange()
|
||||
</template>
|
||||
|
||||
|
||||
</template>
|
||||
</template>
|
||||
Reference in New Issue
Block a user