Redone Times
This commit is contained in:
116
components/StaffTimeEntryModal.vue
Normal file
116
components/StaffTimeEntryModal.vue
Normal 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
8
composables/useFormat.ts
Normal 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
@@ -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
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ const exports = ref([])
|
||||
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const setupPage = async () => {
|
||||
exports.value = await useNuxtApp().$api("/api/exports",{
|
||||
@@ -56,7 +57,10 @@ const createExport = async () => {
|
||||
<template #right>
|
||||
<UButton
|
||||
@click="showCreateExportModal = true"
|
||||
>+ Export</UButton>
|
||||
>+ DATEV</UButton>
|
||||
<UButton
|
||||
@click="router.push('/export/create/sepa')"
|
||||
>+ SEPA</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<UTable
|
||||
234
pages/staff/time/[id]/evaluate.vue
Normal file
234
pages/staff/time/[id]/evaluate.vue
Normal 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
173
pages/staff/time/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user