Merge branch 'beta' into 'main'
2025.20.0 See merge request fedeo/software!28
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/nuxt-app
|
RUN mkdir -p /usr/src/nuxt-app
|
||||||
WORKDIR /usr/src/nuxt-app
|
WORKDIR /usr/src/nuxt-app
|
||||||
|
|||||||
@@ -110,12 +110,15 @@ setup()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
<div class="scroll" style="height: 70vh">
|
||||||
<EntityTable
|
<EntityTable
|
||||||
:type="type"
|
:type="type"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="props.item[type]"
|
:rows="props.item[type]"
|
||||||
style
|
style
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ const links = computed(() => {
|
|||||||
icon: "i-heroicons-megaphone",
|
icon: "i-heroicons-megaphone",
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
label: "Helpdesk",
|
||||||
|
to: "/helpdesk",
|
||||||
|
icon: "i-heroicons-chat-bubble-left-right"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "E-Mail",
|
label: "E-Mail",
|
||||||
to: "/email/new",
|
to: "/email/new",
|
||||||
@@ -137,21 +142,16 @@ const links = computed(() => {
|
|||||||
defaultOpen:false,
|
defaultOpen:false,
|
||||||
icon: "i-heroicons-user-group",
|
icon: "i-heroicons-user-group",
|
||||||
children: [
|
children: [
|
||||||
... true ? [{
|
|
||||||
label: "Projektzeiten",
|
|
||||||
to: "/times",
|
|
||||||
icon: "i-heroicons-clock"
|
|
||||||
}] : [],
|
|
||||||
... true ? [{
|
... true ? [{
|
||||||
label: "Anwesenheiten",
|
label: "Anwesenheiten",
|
||||||
to: "/workingtimes",
|
to: "/staff/time",
|
||||||
icon: "i-heroicons-clock"
|
icon: "i-heroicons-clock"
|
||||||
}] : [],
|
}] : [],
|
||||||
... has("absencerequests") ? [{
|
/*... has("absencerequests") ? [{
|
||||||
label: "Abwesenheiten",
|
label: "Abwesenheiten",
|
||||||
to: "/standardEntity/absencerequests",
|
to: "/standardEntity/absencerequests",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text"
|
||||||
}] : [],
|
}] : [],*/
|
||||||
/*{
|
/*{
|
||||||
label: "Fahrten",
|
label: "Fahrten",
|
||||||
to: "/trackingTrips",
|
to: "/trackingTrips",
|
||||||
@@ -244,7 +244,7 @@ const links = computed(() => {
|
|||||||
}] : [],
|
}] : [],
|
||||||
{
|
{
|
||||||
label: "Mitarbeiter",
|
label: "Mitarbeiter",
|
||||||
to: "/profiles",
|
to: "/staff/profiles",
|
||||||
icon: "i-heroicons-user-group"
|
icon: "i-heroicons-user-group"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -310,8 +310,7 @@ const links = computed(() => {
|
|||||||
},{
|
},{
|
||||||
label: "Bankkonten",
|
label: "Bankkonten",
|
||||||
to: "/settings/banking",
|
to: "/settings/banking",
|
||||||
icon: "i-heroicons-currency-euro",
|
icon: "i-heroicons-currency-euro"
|
||||||
disabled: true
|
|
||||||
},{
|
},{
|
||||||
label: "Textvorlagen",
|
label: "Textvorlagen",
|
||||||
to: "/settings/texttemplates",
|
to: "/settings/texttemplates",
|
||||||
|
|||||||
@@ -14,11 +14,18 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 1.2,
|
default: 1.2,
|
||||||
},
|
},
|
||||||
|
location: {
|
||||||
|
type: String,
|
||||||
|
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
useLicense(config.public.pdfLicense)
|
useLicense(config.public.pdfLicense)
|
||||||
|
|
||||||
|
const tempStore = useTempStore()
|
||||||
|
|
||||||
const pdfSrc = ref(null) // ObjectURL fürs Viewer
|
const pdfSrc = ref(null) // ObjectURL fürs Viewer
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
@@ -59,19 +66,26 @@ const currentScale = computed(() => {
|
|||||||
return zoomControl.value?.scale
|
return zoomControl.value?.scale
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleZoomTool = (type) => {
|
const handleZoomTool = (type, rawScale) => {
|
||||||
console.log(type)
|
console.log(type)
|
||||||
const zoomCtrl = unref(zoomControl)
|
const zoomCtrl = unref(zoomControl)
|
||||||
if (!zoomCtrl) return
|
if (!zoomCtrl) return
|
||||||
|
|
||||||
const scale = unref(currentScale)
|
const scale = unref(currentScale)
|
||||||
if (type === "in") {
|
if(!type ){
|
||||||
|
zoomCtrl.zoom(rawScale)
|
||||||
|
} else if (type === "in") {
|
||||||
scale && zoomCtrl.zoom(scale + 0.25)
|
scale && zoomCtrl.zoom(scale + 0.25)
|
||||||
} else if (type === "out") {
|
} else if (type === "out") {
|
||||||
scale && zoomCtrl.zoom(scale - 0.25)
|
scale && zoomCtrl.zoom(scale - 0.25)
|
||||||
} else {
|
} else {
|
||||||
zoomCtrl.zoom(type)
|
zoomCtrl.zoom(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(["in","out"].includes(type)){
|
||||||
|
tempStore.modifySettings(`pdfviewer-scale-${props.location}`,scale)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//Page Control
|
//Page Control
|
||||||
const pageControl = computed(() => vpvRef.value?.pageControl)
|
const pageControl = computed(() => vpvRef.value?.pageControl)
|
||||||
@@ -196,6 +210,7 @@ watch(downloadControl, (downloadCtrl) => {
|
|||||||
style="height: 78vh; width: 100%;"
|
style="height: 78vh; width: 100%;"
|
||||||
:toolbar-options="false"
|
:toolbar-options="false"
|
||||||
ref="vpvRef"
|
ref="vpvRef"
|
||||||
|
@loaded="handleZoomTool(null,tempStore.settings[`pdfviewer-scale-${props.location}`] || 1)"
|
||||||
/>
|
/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<UProgress
|
<UProgress
|
||||||
|
|||||||
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>
|
||||||
@@ -2,6 +2,8 @@ import {Capacitor} from "@capacitor/core";
|
|||||||
import {Device} from "@capacitor/device";
|
import {Device} from "@capacitor/device";
|
||||||
import {Network} from "@capacitor/network";
|
import {Network} from "@capacitor/network";
|
||||||
|
|
||||||
|
const override = false
|
||||||
|
|
||||||
export const useCapacitor = () => {
|
export const useCapacitor = () => {
|
||||||
const getPlatform = () => {
|
const getPlatform = () => {
|
||||||
return Capacitor.getPlatform()
|
return Capacitor.getPlatform()
|
||||||
@@ -14,11 +16,11 @@ export const useCapacitor = () => {
|
|||||||
const getIsPhone = async () => {
|
const getIsPhone = async () => {
|
||||||
let deviceInfo = await useCapacitor().getDeviceInfo()
|
let deviceInfo = await useCapacitor().getDeviceInfo()
|
||||||
|
|
||||||
return deviceInfo.model.toLowerCase().includes('iphone')
|
return override || deviceInfo.model.toLowerCase().includes('iphone')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIsNative = () => {
|
const getIsNative = () => {
|
||||||
return Capacitor.isNativePlatform()
|
return override || Capacitor.isNativePlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNetworkStatus = async () => {
|
const getNetworkStatus = async () => {
|
||||||
|
|||||||
@@ -36,6 +36,83 @@ export const useEntities = (
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const selectPaginated = async (options: {
|
||||||
|
select?: string
|
||||||
|
filters?: Record<string, any>
|
||||||
|
sort?: { field: string; direction?: 'asc' | 'desc' }[]
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
includeArchived?: boolean
|
||||||
|
noPagination?: boolean,
|
||||||
|
search?: string,
|
||||||
|
searchColumns?: string[],
|
||||||
|
distinctColumns?: string[],
|
||||||
|
}): Promise<{ data: any[]; meta: any }> => {
|
||||||
|
const {
|
||||||
|
select = '*',
|
||||||
|
filters = {},
|
||||||
|
sort = [],
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
includeArchived = false,
|
||||||
|
noPagination = false,
|
||||||
|
search,
|
||||||
|
searchColumns = [],
|
||||||
|
distinctColumns = [],
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const queryParams: Record<string, any> = {
|
||||||
|
select,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
noPagination: noPagination ? 'true' : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 🔍 Search-Parameter (optional) ---
|
||||||
|
if (search && search.trim().length > 0) {
|
||||||
|
queryParams.search = search.trim()
|
||||||
|
}
|
||||||
|
if (searchColumns.length > 0) queryParams.searchColumns = searchColumns.join(',')
|
||||||
|
if (distinctColumns.length > 0) queryParams.distinctColumns = distinctColumns.join(',')
|
||||||
|
|
||||||
|
|
||||||
|
// --- Sortierung ---
|
||||||
|
if (sort.length > 0) {
|
||||||
|
queryParams.sort = sort
|
||||||
|
.map(s => `${s.field}:${s.direction || 'asc'}`)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Filter ---
|
||||||
|
for (const [key, value] of Object.entries(filters)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
queryParams[`filter[${key}]`] = value.join(',')
|
||||||
|
} else {
|
||||||
|
queryParams[`filter[${key}]`] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await useNuxtApp().$api(`/api/resource/${relation}/paginated`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: queryParams
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return { data: [], meta: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = response.data || []
|
||||||
|
const meta = response.queryConfig || {}
|
||||||
|
|
||||||
|
// --- Optional: Archivierte ausblenden ---
|
||||||
|
if (!includeArchived) {
|
||||||
|
data = data.filter((i: any) => !i.archived)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, meta }
|
||||||
|
}
|
||||||
|
|
||||||
const selectSpecial = async (
|
const selectSpecial = async (
|
||||||
select: string = "*",
|
select: string = "*",
|
||||||
sortColumn: string | null = null,
|
sortColumn: string | null = null,
|
||||||
@@ -134,7 +211,7 @@ export const useEntities = (
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {select, create, update, archive, selectSingle, selectSpecial}
|
return {select, create, update, archive, selectSingle, selectSpecial, selectPaginated}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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")}`
|
||||||
|
}
|
||||||
@@ -6,25 +6,11 @@ const baseURL = /*"http://192.168.1.129:3333"*/ /*"http://localhost:3333"*/ "htt
|
|||||||
export const useFunctions = () => {
|
export const useFunctions = () => {
|
||||||
const supabase = useSupabaseClient()
|
const supabase = useSupabaseClient()
|
||||||
|
|
||||||
const getWorkingTimesEvaluationData = async (profileId, startDate, endDate) => {
|
const getWorkingTimesEvaluationData = async (user_id, startDate, endDate) => {
|
||||||
|
return (await useNuxtApp().$api(`/api/functions/timeevaluation/${user_id}?start_date=${startDate}&end_date=${endDate}`))
|
||||||
return (await useNuxtApp().$api(`/api/functions/workingtimeevaluation/${profileId}?start_date=${startDate}&end_date=${endDate}`))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useNextNumber = async (numberRange) => {
|
const useNextNumber = async (numberRange) => {
|
||||||
/*const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
|
||||||
|
|
||||||
return (await axios({
|
|
||||||
method: "POST",
|
|
||||||
url: `${baseURL}/functions/usenextnumber`,
|
|
||||||
data: {
|
|
||||||
numberRange: numberRange,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`
|
|
||||||
}
|
|
||||||
})).data.usedNumber*/
|
|
||||||
|
|
||||||
return (await useNuxtApp().$api(`/api/functions/usenextnumber/${numberRange}`,)).usedNumber
|
return (await useNuxtApp().$api(`/api/functions/usenextnumber/${numberRange}`,)).usedNumber
|
||||||
|
|
||||||
@@ -53,55 +39,35 @@ export const useFunctions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useBankingGenerateLink = async (institutionId) => {
|
const useBankingGenerateLink = async (institutionId) => {
|
||||||
const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
return (await useNuxtApp().$api(`/api/banking/link/${institutionId}`)).link
|
||||||
|
|
||||||
const {data} = await axios({
|
|
||||||
method: "POST",
|
|
||||||
url: `${baseURL}/functions/bankstatements/generatelink`,
|
|
||||||
data: {
|
|
||||||
institutionId
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
return data.link
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useCreatePDF = async (invoiceData,path) => {
|
const useCreatePDF = async (data,path,type) => {
|
||||||
//const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
const returnData = await useNuxtApp().$api(`/api/functions/pdf/${type}`, {
|
||||||
|
|
||||||
const data = await useNuxtApp().$api(`/api/functions/createinvoicepdf`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
invoiceData: invoiceData,
|
data: data,
|
||||||
backgroundPath: path,
|
backgroundPath: path,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/*const {data} = await axios({
|
console.log(returnData)
|
||||||
method: "POST",
|
|
||||||
url: `${baseURL}/functions/createpdf`,
|
|
||||||
data: {
|
|
||||||
invoiceData: invoiceData,
|
|
||||||
backgroundPath: path,
|
|
||||||
returnMode: "base64"
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
|
|
||||||
console.log(data)
|
return `data:${returnData.mimeType};base64,${returnData.base64}`
|
||||||
|
|
||||||
return `data:${data.mimeType};base64,${data.base64}`
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useZipCheck = async (zip) => {
|
||||||
|
const returnData = await useNuxtApp().$api(`/api/functions/check-zip/${zip}`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
|
||||||
|
return returnData
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const useGetInvoiceData = async (file) => {
|
const useGetInvoiceData = async (file) => {
|
||||||
const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
||||||
|
|
||||||
@@ -144,34 +110,16 @@ export const useFunctions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useBankingCheckInstitutions = async (bic) => {
|
const useBankingCheckInstitutions = async (bic) => {
|
||||||
const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
|
||||||
|
|
||||||
const {data} = await axios({
|
return await useNuxtApp().$api(`/api/banking/institutions/${bic}`)
|
||||||
method: "GET",
|
|
||||||
url: `${baseURL}/functions/bankstatements/checkinstitutions/${bic}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useBankingListRequisitions = async (reqId) => {
|
const useBankingListRequisitions = async (reqId) => {
|
||||||
const {data:{session:{access_token}}} = await supabase.auth.getSession()
|
|
||||||
|
|
||||||
const {data} = await axios({
|
return await useNuxtApp().$api(`/api/banking/requisitions/${reqId}`)
|
||||||
method: "GET",
|
|
||||||
url: `${baseURL}/functions/bankstatements/listrequisitions/${reqId}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {getWorkingTimesEvaluationData, useNextNumber, useCreateTicket, useBankingGenerateLink, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF,useGetInvoiceData, useSendTelegramNotification}
|
return {getWorkingTimesEvaluationData, useNextNumber, useCreateTicket, useBankingGenerateLink, useZipCheck, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF,useGetInvoiceData, useSendTelegramNotification}
|
||||||
}
|
}
|
||||||
110
composables/useHelpdesk.ts
Normal file
110
composables/useHelpdesk.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// composables/useHelpdeskApi.ts
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useNuxtApp } from '#app'
|
||||||
|
|
||||||
|
export function useHelpdeskApi() {
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const base = '/api/helpdesk'
|
||||||
|
|
||||||
|
// 🔹 Konversationen abrufen
|
||||||
|
async function getConversations(status?: string) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const query = status ? `?status=${status}` : ''
|
||||||
|
const data = await $api(`${base}/conversations${query}`)
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || 'Fehler beim Laden der Konversationen'
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Einzelne Konversation
|
||||||
|
async function getConversation(id: string) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/conversations/${id}`)
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Nachrichten einer Konversation
|
||||||
|
async function getMessages(conversationId: string) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/conversations/${conversationId}/messages`)
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Neue Nachricht senden
|
||||||
|
async function sendMessage(conversationId: string, text: string) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/conversations/${conversationId}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { text },
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replyMessage(conversationId: string, text: string) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/conversations/${conversationId}/reply`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { text },
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Neuen Kontakt (manuell) anlegen
|
||||||
|
async function createContact(payload: { email?: string; phone?: string; display_name?: string }) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/contacts`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Konversation-Status ändern
|
||||||
|
async function updateConversationStatus(conversationId: string, status: string) {
|
||||||
|
try {
|
||||||
|
return await $api(`${base}/conversations/${conversationId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { status },
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
getConversations,
|
||||||
|
getConversation,
|
||||||
|
getMessages,
|
||||||
|
sendMessage,
|
||||||
|
createContact,
|
||||||
|
updateConversationStatus,
|
||||||
|
replyMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
54
composables/useStaffTime.ts
Normal file
54
composables/useStaffTime.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
interface StaffTimeEntry {
|
||||||
|
id: string
|
||||||
|
started_at: string
|
||||||
|
stopped_at?: string | null
|
||||||
|
duration_minutes?: number | null
|
||||||
|
type: string
|
||||||
|
description?: string | null
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStaffTime() {
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function list(params?: { user_id?: string }) {
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
if (params?.user_id) query.append("user_id", params.user_id)
|
||||||
|
|
||||||
|
return await $api(`/api/staff/time${query.toString() ? `?${query}` : ''}`, { method: 'GET' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start(description?: string) {
|
||||||
|
return await $api<StaffTimeEntry>('/api/staff/time', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
type: 'work',
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop(id: string) {
|
||||||
|
return await $api<StaffTimeEntry>(`/api/staff/time/${id}/stop`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { stopped_at: new Date().toISOString() },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(id: string) {
|
||||||
|
return await $api<StaffTimeEntry>(`/api/staff/time/${id}`, { method: 'GET' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(data: Record<string, any>) {
|
||||||
|
return await $api('/api/staff/time', { method: 'POST', body: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(id: string, data: Record<string, any>) {
|
||||||
|
return await $api(`/api/staff/time/${id}`, { method: 'PUT', body: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { list, start, stop, get, create, update }
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,9 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ssr:false,
|
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
|
||||||
|
|
||||||
|
ssr: false,
|
||||||
|
|
||||||
imports: {
|
imports: {
|
||||||
dirs: ['stores']
|
dirs: ['stores']
|
||||||
@@ -25,7 +27,6 @@ export default defineNuxtConfig({
|
|||||||
transpile: ['@vuepic/vue-datepicker']
|
transpile: ['@vuepic/vue-datepicker']
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
|
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/printing': {ssr: false}
|
'/printing': {ssr: false}
|
||||||
@@ -35,7 +36,7 @@ export default defineNuxtConfig({
|
|||||||
supabase: {
|
supabase: {
|
||||||
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
|
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
|
||||||
url: "https://uwppvcxflrcsibuzsbil.supabase.co",
|
url: "https://uwppvcxflrcsibuzsbil.supabase.co",
|
||||||
redirect:false
|
redirect: false
|
||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
@@ -45,7 +46,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
icons: ['heroicons','mdi','simple-icons']
|
icons: ['heroicons', 'mdi', 'simple-icons']
|
||||||
},
|
},
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const setup = async () => {
|
|||||||
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
||||||
console.log(allocatedDocuments.value)
|
console.log(allocatedDocuments.value)
|
||||||
console.log(allocatedIncomingInvoices.value)
|
console.log(allocatedIncomingInvoices.value)
|
||||||
openIncomingInvoices.value = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")).filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
|
openIncomingInvoices.value = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")).filter(i => !i.archived && i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
|
||||||
//console.log(openIncomingInvoices.value)
|
//console.log(openIncomingInvoices.value)
|
||||||
|
|
||||||
// return incominginvoices.value.filter(i => bankstatements.value.filter(x => x.assignments.find(y => y.type === 'incomingInvoice' && y.id === i.id)).length === 0)
|
// return incominginvoices.value.filter(i => bankstatements.value.filter(x => x.assignments.find(y => y.type === 'incomingInvoice' && y.id === i.id)).length === 0)
|
||||||
@@ -181,14 +181,14 @@ const searchString = ref("")
|
|||||||
const filteredDocuments = computed(() => {
|
const filteredDocuments = computed(() => {
|
||||||
|
|
||||||
|
|
||||||
return useSearch(searchString.value, openDocuments.value)
|
return useSearch(searchString.value, openDocuments.value.filter(i => i.state === "Gebucht"))
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredIncomingInvoices = computed(() => {
|
const filteredIncomingInvoices = computed(() => {
|
||||||
|
|
||||||
|
|
||||||
return useSearch(searchString.value, openIncomingInvoices.value)
|
return useSearch(searchString.value, openIncomingInvoices.value.filter(i => i.state === "Gebucht"))
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1150,9 +1150,9 @@ const getDocumentData = async () => {
|
|||||||
label: "Ansprechpartner",
|
label: "Ansprechpartner",
|
||||||
content: contactPerson.full_name,
|
content: contactPerson.full_name,
|
||||||
},
|
},
|
||||||
...contactPerson.fixedTel || contactPerson.mobileTel ? [{
|
...itemInfo.value.contactTel || contactPerson.fixed_tel || contactPerson.mobile_tel ? [{
|
||||||
label: "Telefon",
|
label: "Telefon",
|
||||||
content: contactPerson.fixedTel || contactPerson.mobileTel,
|
content: itemInfo.value.contactTel || contactPerson.fixed_tel || contactPerson.mobile_tel,
|
||||||
}] : [],
|
}] : [],
|
||||||
...contactPerson.email ? [{
|
...contactPerson.email ? [{
|
||||||
label: "E-Mail",
|
label: "E-Mail",
|
||||||
@@ -1225,7 +1225,7 @@ const generateDocument = async () => {
|
|||||||
showDocument.value = false
|
showDocument.value = false
|
||||||
const path = letterheads.value.find(i => i.id === itemInfo.value.letterhead).path
|
const path = letterheads.value.find(i => i.id === itemInfo.value.letterhead).path
|
||||||
|
|
||||||
uri.value = await useFunctions().useCreatePDF(await getDocumentData(), path)
|
uri.value = await useFunctions().useCreatePDF(await getDocumentData(), path, "createdDocument")
|
||||||
/*uri.value = await useNuxtApp().$api("/api/functions/createinvoicepdf",{
|
/*uri.value = await useNuxtApp().$api("/api/functions/createinvoicepdf",{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
@@ -3108,24 +3108,11 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="item.label === 'Vorschau'">
|
<div v-else-if="item.label === 'Vorschau'">
|
||||||
|
|
||||||
<!-- <UButton
|
|
||||||
@click="generateDocument"
|
|
||||||
>
|
|
||||||
Show
|
|
||||||
</UButton>-->
|
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
v-if="showDocument"
|
v-if="showDocument"
|
||||||
:uri="uri"
|
:uri="uri"
|
||||||
|
location="edit_create_document"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- <object
|
|
||||||
:data="uri"
|
|
||||||
v-if="showDocument"
|
|
||||||
type="application/pdf"
|
|
||||||
class="w-full previewDocumentMobile"
|
|
||||||
/>-->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const openBankstatements = () => {
|
|||||||
:data="linkedDocument.url"
|
:data="linkedDocument.url"
|
||||||
class="w-full previewDocumentMobile"
|
class="w-full previewDocumentMobile"
|
||||||
/>-->
|
/>-->
|
||||||
<PDFViewer v-if="linkedDocument.id" :file-id="linkedDocument.id" />
|
<PDFViewer v-if="linkedDocument.id" :file-id="linkedDocument.id" location="show_create_document" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
325
pages/helpdesk/[[id]].vue
Normal file
325
pages/helpdesk/[[id]].vue
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted, watch} from 'vue'
|
||||||
|
import {format, isToday, formatDistanceToNow} from 'date-fns'
|
||||||
|
import {de as deLocale} from 'date-fns/locale'
|
||||||
|
|
||||||
|
const {getConversations, getMessages, sendMessage, replyMessage, updateConversationStatus} = useHelpdeskApi()
|
||||||
|
|
||||||
|
const conversations = ref<any[]>([])
|
||||||
|
const selectedConversation = ref<any>(null)
|
||||||
|
const messages = ref<any[]>([])
|
||||||
|
const messageText = ref('')
|
||||||
|
const filterStatus = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// Referenzen für Scroll + Shortcuts
|
||||||
|
const convRefs = ref<Element[]>([])
|
||||||
|
|
||||||
|
async function loadConversations() {
|
||||||
|
loading.value = true
|
||||||
|
conversations.value = await getConversations(filterStatus.value)
|
||||||
|
|
||||||
|
if(route.params.id){
|
||||||
|
await selectConversation(conversations.value.find(i => i.id === route.params.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectConversation(conv: any) {
|
||||||
|
selectedConversation.value = conv
|
||||||
|
messages.value = await getMessages(conv.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
if (!messageText.value || !selectedConversation.value) return
|
||||||
|
await replyMessage(selectedConversation.value.id, messageText.value)
|
||||||
|
messageText.value = ''
|
||||||
|
messages.value = await getMessages(selectedConversation.value.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
defineShortcuts({
|
||||||
|
arrowdown: () => {
|
||||||
|
const index = conversations.value.findIndex(c => c.id === selectedConversation.value?.id)
|
||||||
|
if (index === -1) selectedConversation.value = conversations.value[0]
|
||||||
|
else if (index < conversations.value.length - 1)
|
||||||
|
selectedConversation.value = conversations.value[index + 1]
|
||||||
|
},
|
||||||
|
arrowup: () => {
|
||||||
|
const index = conversations.value.findIndex(c => c.id === selectedConversation.value?.id)
|
||||||
|
if (index === -1) selectedConversation.value = conversations.value.at(-1)
|
||||||
|
else if (index > 0)
|
||||||
|
selectedConversation.value = conversations.value[index - 1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedConversation, () => {
|
||||||
|
if (!selectedConversation.value) return
|
||||||
|
const ref = convRefs.value[selectedConversation.value.id]
|
||||||
|
if (ref) ref.scrollIntoView({block: 'nearest'})
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(loadConversations)
|
||||||
|
watch(filterStatus, loadConversations)
|
||||||
|
|
||||||
|
// Gruppierung nach aufeinanderfolgenden gleichen Autoren
|
||||||
|
const groupedMessages = computed(() => {
|
||||||
|
if (!messages.value.length) return []
|
||||||
|
|
||||||
|
const groups: any[] = []
|
||||||
|
let current: any = null
|
||||||
|
|
||||||
|
for (const msg of messages.value) {
|
||||||
|
const authorKey = `${msg.direction}-${msg.author_user_id || msg.author_name || 'anon'}`
|
||||||
|
if (!current || current.key !== authorKey) {
|
||||||
|
current = {
|
||||||
|
key: authorKey,
|
||||||
|
direction: msg.direction,
|
||||||
|
author_name: msg.direction === 'outgoing' ? 'Du' : msg.author_name || 'Kunde',
|
||||||
|
author_avatar: msg.author_avatar || null,
|
||||||
|
messages: [msg],
|
||||||
|
latest_created_at: msg.created_at,
|
||||||
|
}
|
||||||
|
groups.push(current)
|
||||||
|
} else {
|
||||||
|
current.messages.push(msg)
|
||||||
|
current.latest_created_at = msg.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPage>
|
||||||
|
<!-- === NAVBAR === -->
|
||||||
|
<UDashboardNavbar>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="w-5 h-5 text-primary-600"/>
|
||||||
|
<span class="text-lg font-semibold">Helpdesk</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
color="gray"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
@click="loadConversations"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<!-- === TOOLBAR === -->
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<USelect
|
||||||
|
v-model="filterStatus"
|
||||||
|
size="sm"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Alle', value: '' },
|
||||||
|
{ label: 'Offen', value: 'open' },
|
||||||
|
{ label: 'In Bearbeitung', value: 'in_progress' },
|
||||||
|
{ label: 'Geschlossen', value: 'closed' }
|
||||||
|
]"
|
||||||
|
placeholder="Status filtern"
|
||||||
|
class="min-w-[180px]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
size="sm"
|
||||||
|
label="Konversation"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<!-- === CONTENT === -->
|
||||||
|
<div class="flex h-[calc(100vh-150px)] overflow-x-hidden">
|
||||||
|
<!-- 📬 Resizable Sidebar -->
|
||||||
|
<div
|
||||||
|
class="relative flex-shrink-0 border-r border-(--ui-border) bg-(--ui-bg) overflow-y-auto"
|
||||||
|
style="width: 340px; resize: horizontal; min-width: 260px; max-width: 600px;"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="p-4 space-y-2">
|
||||||
|
<USkeleton v-for="i in 6" :key="i" class="h-14"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="divide-y divide-(--ui-border)">
|
||||||
|
<div
|
||||||
|
v-for="(conv, index) in conversations"
|
||||||
|
:key="conv.id"
|
||||||
|
:ref="el => { convRefs[conv.id] = el as Element }"
|
||||||
|
class="p-4 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
|
||||||
|
:class="[
|
||||||
|
selectedConversation && selectedConversation.id === conv.id
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-(--ui-bg) hover:border-primary hover:bg-primary/5'
|
||||||
|
]"
|
||||||
|
@click="selectConversation(conv)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between font-medium">
|
||||||
|
<div class="flex items-center gap-2 truncate">
|
||||||
|
{{ conv.helpdesk_contacts?.display_name || 'Unbekannt' }}
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{
|
||||||
|
isToday(new Date(conv.last_message_at || conv.created_at))
|
||||||
|
? format(new Date(conv.last_message_at || conv.created_at), 'HH:mm')
|
||||||
|
: format(new Date(conv.last_message_at || conv.created_at), 'dd MMM')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="truncate text-sm font-semibold">
|
||||||
|
{{conv.ticket_number}} | {{ conv.subject || 'Ohne Betreff' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-dimmed line-clamp-1">
|
||||||
|
{{ conv.last_message_preview || '...' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 💬 Conversation Panel -->
|
||||||
|
<div class="flex-1 flex flex-col overflow-x-hidden" v-if="selectedConversation">
|
||||||
|
<UCard class="relative flex flex-col flex-1 rounded-none border-0 border-l border-(--ui-border)">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-semibold truncate text-gray-800 dark:text-gray-200">
|
||||||
|
{{selectedConversation.ticket_number}} | {{ selectedConversation.subject || 'Ohne Betreff' }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<USelect
|
||||||
|
v-model="selectedConversation.status"
|
||||||
|
size="xs"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Offen', value: 'open' },
|
||||||
|
{ label: 'In Bearbeitung', value: 'in_progress' },
|
||||||
|
{ label: 'Geschlossen', value: 'closed' }
|
||||||
|
]"
|
||||||
|
@update:model-value="val => updateConversationStatus(selectedConversation.id, val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kundenzuordnung -->
|
||||||
|
<div class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<UIcon
|
||||||
|
:name="selectConversation.customer_id?.isCompany ? 'i-heroicons-building-office-2' : 'i-heroicons-user'"
|
||||||
|
class="w-4 h-4 text-gray-400"/>
|
||||||
|
<span>
|
||||||
|
<strong>{{ selectedConversation.customer_id?.name || 'Kein Kunde zugeordnet' }}</strong>
|
||||||
|
</span>
|
||||||
|
<EntityModalButtons
|
||||||
|
type="customers"
|
||||||
|
v-if="selectedConversation?.customer_id?.id"
|
||||||
|
:id="selectedConversation?.customer_id?.id"
|
||||||
|
:button-edit="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="selectedConversation.contact_person">
|
||||||
|
<UIcon name="i-heroicons-user" class="w-4 h-4 text-gray-400 ml-3"/>
|
||||||
|
<span>{{ selectedConversation.contact_person.name }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Nachrichten -->
|
||||||
|
<div class="flex-1 overflow-y-auto space-y-4 p-4 pb-24">
|
||||||
|
<template v-for="(group, gIndex) in groupedMessages" :key="gIndex">
|
||||||
|
<!-- Avatar + Name + Zeit -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 mb-1"
|
||||||
|
:class="group.direction === 'outgoing' ? 'flex-row-reverse text-right' : 'flex-row text-left'"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ group.direction === 'outgoing' ? 'Du' : group.author_name || 'Kunde' }}
|
||||||
|
•
|
||||||
|
{{ formatDistanceToNow(new Date(group.latest_created_at), {addSuffix: true, locale: deLocale}) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nachrichten des Autors -->
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="msg in group.messages"
|
||||||
|
:key="msg.id"
|
||||||
|
class="flex"
|
||||||
|
:class="group.direction === 'outgoing' ? 'justify-end' : 'justify-start'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'inline-block px-3 py-2 rounded-xl max-w-[80%] text-sm whitespace-pre-wrap break-words',
|
||||||
|
msg.direction === 'outgoing'
|
||||||
|
? 'bg-primary-500 text-white ml-auto'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ msg.payload.text }}
|
||||||
|
|
||||||
|
<!-- Gelesen-Indikator (nur outgoing letzte Nachricht) -->
|
||||||
|
<span
|
||||||
|
v-if="group.direction === 'outgoing' && msg.id === group.messages.at(-1).id"
|
||||||
|
class="absolute -bottom-4 right-1 text-[10px] text-gray-400 flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
v-if="msg.read"
|
||||||
|
name="i-heroicons-check-double-16-solid"
|
||||||
|
class="text-primary-400 w-3 h-3"
|
||||||
|
/>
|
||||||
|
<UIcon
|
||||||
|
v-else
|
||||||
|
name="i-heroicons-check-16-solid"
|
||||||
|
class="text-gray-400 w-3 h-3"
|
||||||
|
/>
|
||||||
|
<span>{{ msg.read ? 'Gelesen' : 'Gesendet' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="!messages.length" class="text-center text-gray-500 text-sm mt-4">
|
||||||
|
Keine Nachrichten vorhanden
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Nachricht senden (jetzt sticky unten) -->
|
||||||
|
<form class="sticky bottom-0 border-t flex gap-2 p-3 bg-white dark:bg-gray-900" @submit.prevent="send">
|
||||||
|
<UInput
|
||||||
|
v-model="messageText"
|
||||||
|
placeholder="Nachricht eingeben..."
|
||||||
|
class="flex-1"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
:disabled="!messageText"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
label="Senden"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UPage>
|
||||||
|
</template>
|
||||||
@@ -68,11 +68,6 @@ definePageMeta({
|
|||||||
|
|
||||||
const { isNotificationsSlideoverOpen } = useDashboard()
|
const { isNotificationsSlideoverOpen } = useDashboard()
|
||||||
|
|
||||||
const setup = async () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setup()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
|
|
||||||
const itemInfo = ref({})
|
|
||||||
|
|
||||||
const createProfile = async () => {
|
|
||||||
let data = {
|
|
||||||
fullName: `${itemInfo.value.firstName} ${itemInfo.value.lastName}`,
|
|
||||||
...itemInfo.value
|
|
||||||
}
|
|
||||||
|
|
||||||
await dataStore.createNewItem("profiles", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UDashboardNavbar title="Mitarbeiter erstellen">
|
|
||||||
<template #right>
|
|
||||||
<UButton
|
|
||||||
color="rose"
|
|
||||||
@click="router.push(`/profiles`)"
|
|
||||||
>
|
|
||||||
Abbrechen
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
@click="createProfile"
|
|
||||||
>
|
|
||||||
Erstellen
|
|
||||||
</UButton>
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
|
||||||
|
|
||||||
<UForm
|
|
||||||
class="p-5"
|
|
||||||
>
|
|
||||||
<UFormGroup
|
|
||||||
label="Anrede"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
required
|
|
||||||
v-model="itemInfo.salutation"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Vorname"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
required
|
|
||||||
v-model="itemInfo.firstName"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Nachname"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
required
|
|
||||||
v-model="itemInfo.lastName"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Mitarbeiter Nummer"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.employeeNumber"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="E-Mail"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.email"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Handynummer"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.mobileTel"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Festnetznummer"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.fixedTel"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</UForm>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import axios from "axios"
|
|
||||||
import HistoryDisplay from "~/components/HistoryDisplay.vue";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
||||||
import isoWeek from "dayjs/plugin/isoWeek"
|
|
||||||
import isBetween from "dayjs/plugin/isBetween"
|
|
||||||
import DocumentList from "~/components/DocumentList.vue";
|
|
||||||
import DocumentUpload from "~/components/DocumentUpload.vue";
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
dayjs.extend(customParseFormat)
|
|
||||||
dayjs.extend(isoWeek)
|
|
||||||
dayjs.extend(isBetween)
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const supabase = useSupabaseClient()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const itemInfo = ref({
|
|
||||||
weeklyRegularWorkingHours: {
|
|
||||||
"1":0,
|
|
||||||
"2":0,
|
|
||||||
"3":0,
|
|
||||||
"4":0,
|
|
||||||
"5":0,
|
|
||||||
"6":0,
|
|
||||||
"7":0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const oldItemInfo = ref({})
|
|
||||||
|
|
||||||
const setupPage = async () => {
|
|
||||||
|
|
||||||
console.log(itemInfo.value)
|
|
||||||
|
|
||||||
if(route.params.id) {
|
|
||||||
itemInfo.value = await useSupabaseSelectSingle("profiles",route.params.id,"*, documents(*), checks(*)")
|
|
||||||
|
|
||||||
if(Object.keys(itemInfo.value.weeklyRegularWorkingHours).length === 0) {
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours = {
|
|
||||||
"1":0,
|
|
||||||
"2":0,
|
|
||||||
"3":0,
|
|
||||||
"4":0,
|
|
||||||
"5":0,
|
|
||||||
"6":0,
|
|
||||||
"7":0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!itemInfo.value.weeklyWorkingHours) itemInfo.value.weeklyWorkingHours = 0
|
|
||||||
if(!itemInfo.value.weeklyWorkingDays) itemInfo.value.weeklyWorkingDays = 0
|
|
||||||
if(!itemInfo.value.annualPaidLeaveDays) itemInfo.value.annualPaidLeaveDays = 0
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(itemInfo.value.id) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
setupPage()
|
|
||||||
|
|
||||||
const emailSignature = ref(profileStore.activeProfile.emailSignature)
|
|
||||||
const tiptapLoaded = ref(false)
|
|
||||||
const contentSaved = ref(true)
|
|
||||||
const contentChanged = async (content) => {
|
|
||||||
emailSignature.value = content.html
|
|
||||||
if(tiptapLoaded.value === true) {
|
|
||||||
contentSaved.value = false
|
|
||||||
} else {
|
|
||||||
tiptapLoaded.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const saveSignature = async () => {
|
|
||||||
const {data,error} = await supabase.from("profiles").update({emailSignature: emailSignature.value}).eq("id", profileStore.activeProfile.id)
|
|
||||||
|
|
||||||
if(error) {
|
|
||||||
toast.add({title: "Fehler beim Speichern der Signatur", color:"rose"})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
contentSaved.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const addToNewsletter = async () => {
|
|
||||||
const {data,error} = await axios({
|
|
||||||
url: "https://newsletter.fedeo.io/api/public/subscription",
|
|
||||||
method: "post",
|
|
||||||
data: {
|
|
||||||
email: profileStore.activeProfile.email,
|
|
||||||
name: profileStore.activeProfile.name,
|
|
||||||
list_uuids: ["b97453fd-14b2-4b25-8f9b-b83847317ea3"],
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcWeeklyWorkingHours = () => {
|
|
||||||
itemInfo.value.weeklyWorkingHours =
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[1] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[2] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[3] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[4] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[5] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[6] +
|
|
||||||
itemInfo.value.weeklyRegularWorkingHours[7]
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const isLight = computed({
|
|
||||||
get() {
|
|
||||||
return colorMode.value !== 'dark'
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const saveProfile = async () => {
|
|
||||||
let data = {...itemInfo.value, fullName: itemInfo.value.firstName + ' ' + itemInfo.value.lastName}
|
|
||||||
delete data.checks
|
|
||||||
delete data.documents
|
|
||||||
|
|
||||||
await dataStore.updateItem('profiles',data,oldItemInfo.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addAutomaticHourCorrection = async () => {
|
|
||||||
itemInfo.value.automaticHourCorrections.push({mode: 'correct', id: uuidv4(), weekday: 1, bookingTimeUTC: {hour: 0, minute: 0}, rangeStartUTC: {hour: 0, minute: 0}, rangeEndUTC: {hour: 0, minute: 0}})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UDashboardNavbar
|
|
||||||
:title="itemInfo.fullName"
|
|
||||||
>
|
|
||||||
<template #left>
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-chevron-left"
|
|
||||||
variant="outline"
|
|
||||||
@click="router.push(`/profiles`)"
|
|
||||||
>
|
|
||||||
Mitarbeiter
|
|
||||||
</UButton>
|
|
||||||
</template>
|
|
||||||
<template #center>
|
|
||||||
<h1
|
|
||||||
v-if="itemInfo"
|
|
||||||
:class="['text-xl','font-medium'/*, ... true ? ['text-primary'] : ['text-rose-500']*/]"
|
|
||||||
>{{itemInfo ? `Mitarbeiter: ${itemInfo.fullName}` : ''}}</h1>
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
|
||||||
<UTabs
|
|
||||||
v-if="itemInfo.id"
|
|
||||||
class="p-5"
|
|
||||||
:items="[
|
|
||||||
{
|
|
||||||
label: 'Informationen'
|
|
||||||
},{
|
|
||||||
label: 'Logbuch'
|
|
||||||
},{
|
|
||||||
label: 'Zeiterfassung'
|
|
||||||
},{
|
|
||||||
label: 'Vertragsdaten'
|
|
||||||
},{
|
|
||||||
label: 'Dokumente'
|
|
||||||
},{
|
|
||||||
label: 'Überprüfungen'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<template #item="{item}">
|
|
||||||
<UCard class="mt-5">
|
|
||||||
<div v-if="item.label === 'Informationen'">
|
|
||||||
<Toolbar>
|
|
||||||
<UButton
|
|
||||||
@click="saveProfile"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</UButton>
|
|
||||||
</Toolbar>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Anrede"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.salutation"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Vorname"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.firstName"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Nachname"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.lastName"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Mitarbeiternummer"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.employeeNumber"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="E-Mail"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.email"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Benutzername"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.username"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Festnetz Telefon"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.fixedTel"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Handy"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.mobileTel"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Geburtstag:"
|
|
||||||
>
|
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-calendar-days-20-solid"
|
|
||||||
:label="itemInfo.birthday ? dayjs(itemInfo.birthday).format('DD.MM.YYYY') : 'Datum auswählen'"
|
|
||||||
variant="outline"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #panel="{ close }">
|
|
||||||
<LazyDatePicker v-model="itemInfo.birthday" />
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Kleidergröße Oberteil"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.clothingSizeTop"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Kleidergröße Hose"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.clothingSizeBottom"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Schuhgröße"
|
|
||||||
class="w-60"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.clothingSizeShoe"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
<UDivider class="my-5" v-if="profileStore.activeProfile.id === itemInfo.id">
|
|
||||||
Helligkeitseinstellung
|
|
||||||
</UDivider>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
v-if="profileStore.activeProfile.id === itemInfo.id"
|
|
||||||
:icon="isLight ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
|
|
||||||
color="white"
|
|
||||||
variant="outline"
|
|
||||||
aria-label="Theme"
|
|
||||||
@click="isLight = !isLight"
|
|
||||||
>
|
|
||||||
{{!isLight ? "Hell" : "Dunkel"}}
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<UDivider class="my-5">
|
|
||||||
E-Mail Signatur
|
|
||||||
</UDivider>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
variant="outline"
|
|
||||||
v-if="!contentSaved"
|
|
||||||
@click="saveSignature"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<Tiptap
|
|
||||||
@updateContent="contentChanged"
|
|
||||||
:preloadedContent="emailSignature"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!--<UDivider>Newsletter</UDivider>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
@click="addToNewsletter"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
In Newsletter eintragen
|
|
||||||
</UButton>-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div v-if="item.label === 'Logbuch'">
|
|
||||||
<HistoryDisplay
|
|
||||||
type="profile"
|
|
||||||
v-if="itemInfo"
|
|
||||||
:element-id="itemInfo.id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.label === 'Vertragsdaten'">
|
|
||||||
<Toolbar>
|
|
||||||
<UButton
|
|
||||||
@click="saveProfile"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</UButton>
|
|
||||||
</Toolbar>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Eintrittsdatum:"
|
|
||||||
>
|
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-calendar-days-20-solid"
|
|
||||||
:label="itemInfo.entryDate ? dayjs(itemInfo.entryDate).format('DD.MM.YYYY') : 'Datum auswählen'"
|
|
||||||
variant="outline"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #panel="{ close }">
|
|
||||||
<LazyDatePicker v-model="itemInfo.entryDate" />
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Wöchentliche Arbeitszeit"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyWorkingHours"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<!-- <UFormGroup
|
|
||||||
label="Durchschnittliche Arbeitstage pro Woche"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyWorkingDays"
|
|
||||||
/>
|
|
||||||
</UFormGroup>-->
|
|
||||||
<UFormGroup
|
|
||||||
label="Urlaubstage"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.annualPaidLeaveDays"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
<UDivider class="my-3">Regelarbeitszeiten</UDivider>
|
|
||||||
<InputGroup class="w-full">
|
|
||||||
<UFormGroup
|
|
||||||
label="Montag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[1]"
|
|
||||||
type="number"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
|
|
||||||
/>
|
|
||||||
</UFormGroup><UFormGroup
|
|
||||||
label="Dienstag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[2]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Mittwoch"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[3]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Donnerstag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[4]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Freitag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[5]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Samstag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[6]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Sonntag"
|
|
||||||
class="flex-auto"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.weeklyRegularWorkingHours[7]"
|
|
||||||
type="number"
|
|
||||||
@change="calcWeeklyWorkingHours"
|
|
||||||
@keyup="calcWeeklyWorkingHours"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</InputGroup>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="item.label === 'Dokumente'">
|
|
||||||
<DocumentUpload
|
|
||||||
type="profile"
|
|
||||||
:element-id="itemInfo.id"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DocumentList :documents="dataStore.getDocumentsByProfileId(itemInfo.id)"/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="item.label === 'Überprüfungen'">
|
|
||||||
<UTable
|
|
||||||
:rows="itemInfo.checks"
|
|
||||||
:columns="[{key:'name',label: 'Name'},{key:'rhythm',label: 'Rhythmus'},{key:'description',label: 'Beschreibung'}]"
|
|
||||||
class="w-full"
|
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
|
||||||
@select="(i) => router.push(`/checks/show/${i.id}`) "
|
|
||||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Überprüfungen anzuzeigen' }"
|
|
||||||
>
|
|
||||||
<template #rhythm-data="{row}">
|
|
||||||
{{row.distance}}
|
|
||||||
<span v-if="row.distanceUnit === 'dayjs'">Tage</span>
|
|
||||||
<span v-if="row.distanceUnit === 'years'">Jahre</span>
|
|
||||||
</template>
|
|
||||||
</UTable>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="item.label === 'Zeiterfassung'">
|
|
||||||
<UButton
|
|
||||||
@click="addAutomaticHourCorrection"
|
|
||||||
>
|
|
||||||
+ Korrektur
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
@click="saveProfile"
|
|
||||||
class="ml-3"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<UDivider class="my-3" />
|
|
||||||
|
|
||||||
<InputGroup class="w-full" v-for="item in itemInfo.automaticHourCorrections">
|
|
||||||
<UFormGroup
|
|
||||||
label="Modus"
|
|
||||||
>
|
|
||||||
<USelectMenu
|
|
||||||
v-model="item.mode"
|
|
||||||
:options="[{key: 'correct', label: 'Korrigieren'},{key: 'automatic-checkout', label: 'Automatisches ausbuchen'}]"
|
|
||||||
option-attribute="label"
|
|
||||||
value-attribute="key"
|
|
||||||
class="w-80"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Wochentag"
|
|
||||||
>
|
|
||||||
<USelectMenu
|
|
||||||
v-model="item.weekday"
|
|
||||||
:options="[{key: 1, label: 'Montag'},{key: 2, label: 'Dienstag'},{key: 3, label: 'Mittwoch'},{key: 4, label: 'Donnerstag'},{key: 5, label: 'Freitag'},{key: 6, label: 'Samstag'},{key: 7, label: 'Sonntag'},]"
|
|
||||||
option-attribute="label"
|
|
||||||
value-attribute="key"
|
|
||||||
class="w-40"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Zeitraum Start"
|
|
||||||
v-if="item.mode === 'correct'"
|
|
||||||
>
|
|
||||||
<InputGroup>
|
|
||||||
<UInput
|
|
||||||
v-model="item.rangeStartUTC.hour"
|
|
||||||
placeholder="Stunde UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
<UInput
|
|
||||||
v-model="item.rangeStartUTC.minute"
|
|
||||||
placeholder="Minute UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Zeitraum Ende"
|
|
||||||
v-if="item.mode === 'correct'"
|
|
||||||
>
|
|
||||||
<InputGroup>
|
|
||||||
<UInput
|
|
||||||
v-model="item.rangeEndUTC.hour"
|
|
||||||
placeholder="Stunde UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
<UInput
|
|
||||||
v-model="item.rangeEndUTC.minute"
|
|
||||||
placeholder="Minute UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Korrigieren zu"
|
|
||||||
v-if="item.mode === 'correct'"
|
|
||||||
>
|
|
||||||
<InputGroup>
|
|
||||||
<UInput
|
|
||||||
v-model="item.bookingTimeUTC.hour"
|
|
||||||
placeholder="Stunde UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
<UInput
|
|
||||||
v-model="item.bookingTimeUTC.minute"
|
|
||||||
placeholder="Minute UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Ausbuchen"
|
|
||||||
v-if="item.mode === 'automatic-checkout'"
|
|
||||||
>
|
|
||||||
<InputGroup>
|
|
||||||
<UInput
|
|
||||||
v-model="item.bookingTimeUTC.hour"
|
|
||||||
placeholder="Stunde UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
<UInput
|
|
||||||
v-model="item.bookingTimeUTC.minute"
|
|
||||||
placeholder="Minute UTC"
|
|
||||||
class="w-20"
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup
|
|
||||||
label="Entfernen"
|
|
||||||
>
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-x-mark"
|
|
||||||
variant="outline"
|
|
||||||
color="red"
|
|
||||||
@click="itemInfo.automaticHourCorrections = itemInfo.automaticHourCorrections.filter(i => i.id !== item.id)"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</template>
|
|
||||||
</UTabs>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -15,12 +15,17 @@ const reqData = ref({})
|
|||||||
|
|
||||||
const bankaccounts = ref([])
|
const bankaccounts = ref([])
|
||||||
|
|
||||||
|
const showReqData = ref(false)
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
if(route.query.ref) {
|
if(route.query.ref) {
|
||||||
reqData.value = await useFunctions().useBankingListRequisitions(route.query.ref)
|
reqData.value = await useFunctions().useBankingListRequisitions(route.query.ref)
|
||||||
|
if(reqData.value.accounts.length > 0){
|
||||||
|
showReqData.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bankaccounts.value = await useSupabaseSelect("bankaccounts")
|
bankaccounts.value = await useEntities("bankaccounts").select()
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkBIC = async () => {
|
const checkBIC = async () => {
|
||||||
@@ -61,12 +66,17 @@ const addAccount = async (account) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateAccount = async (account) => {
|
const updateAccount = async (account) => {
|
||||||
const {data,error} = await supabase.from("bankaccounts").update({accountId: account.id, expired: false}).eq("iban",account.iban).select()
|
|
||||||
if(error) {
|
let bankaccountId = bankaccounts.value.find(i => i.iban === account.iban).id
|
||||||
|
|
||||||
|
const res = await useEntities("bankaccounts").update(bankaccountId, {accountId: account.id, expired: false})
|
||||||
|
|
||||||
|
if(!res) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
toast.add({title: "Es gab einen Fehler bei aktualisieren des Accounts", color:"rose"})
|
toast.add({title: "Es gab einen Fehler bei aktualisieren des Accounts", color:"rose"})
|
||||||
} else if(data) {
|
} else {
|
||||||
toast.add({title: "Account erfolgreich aktualisiert"})
|
toast.add({title: "Account erfolgreich aktualisiert"})
|
||||||
|
reqData.value = null
|
||||||
setupPage()
|
setupPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +143,11 @@ setupPage()
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UModal v-model="showReqData">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
Verfügbare Bankkonten
|
||||||
|
</template>
|
||||||
<div
|
<div
|
||||||
v-for="account in reqData.accounts"
|
v-for="account in reqData.accounts"
|
||||||
class="p-2 m-3 flex justify-between"
|
class="p-2 m-3 flex justify-between"
|
||||||
@@ -143,7 +158,7 @@ setupPage()
|
|||||||
@click="addAccount(account)"
|
@click="addAccount(account)"
|
||||||
v-if="!bankaccounts.find(i => i.iban === account.iban)"
|
v-if="!bankaccounts.find(i => i.iban === account.iban)"
|
||||||
>
|
>
|
||||||
+ Konto
|
Hinzufügen
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
@click="updateAccount(account)"
|
@click="updateAccount(account)"
|
||||||
@@ -152,6 +167,10 @@ setupPage()
|
|||||||
Aktualisieren
|
Aktualisieren
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <UButton @click="setupPage">Setup</UButton>
|
<!-- <UButton @click="setupPage">Setup</UButton>
|
||||||
<div v-if="route.query.reqId">
|
<div v-if="route.query.reqId">
|
||||||
|
|||||||
318
pages/staff/profiles/[id].vue
Normal file
318
pages/staff/profiles/[id].vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
|
const id = route.params.id as string
|
||||||
|
const profile = ref<any>(null)
|
||||||
|
const pending = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
/** Profil laden **/
|
||||||
|
async function fetchProfile() {
|
||||||
|
pending.value = true
|
||||||
|
try {
|
||||||
|
profile.value = await $api(`/api/profiles/${id}`)
|
||||||
|
ensureWorkingHoursStructure()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[fetchProfile]', err)
|
||||||
|
toast.add({
|
||||||
|
title: 'Fehler beim Laden',
|
||||||
|
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
|
||||||
|
color: 'red'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
pending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Profil speichern **/
|
||||||
|
async function saveProfile() {
|
||||||
|
if (saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $api(`/api/profiles/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: profile.value
|
||||||
|
})
|
||||||
|
toast.add({ title: 'Profil gespeichert', color: 'green' })
|
||||||
|
fetchProfile()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[saveProfile]', err)
|
||||||
|
toast.add({
|
||||||
|
title: 'Fehler beim Speichern',
|
||||||
|
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
|
||||||
|
color: 'red'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekdays = [
|
||||||
|
{ key: '1', label: 'Montag' },
|
||||||
|
{ key: '2', label: 'Dienstag' },
|
||||||
|
{ key: '3', label: 'Mittwoch' },
|
||||||
|
{ key: '4', label: 'Donnerstag' },
|
||||||
|
{ key: '5', label: 'Freitag' },
|
||||||
|
{ key: '6', label: 'Samstag' },
|
||||||
|
{ key: '7', label: 'Sonntag' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const bundeslaender = [
|
||||||
|
{ code: 'DE-BW', name: 'Baden-Württemberg' },
|
||||||
|
{ code: 'DE-BY', name: 'Bayern' },
|
||||||
|
{ code: 'DE-BE', name: 'Berlin' },
|
||||||
|
{ code: 'DE-BB', name: 'Brandenburg' },
|
||||||
|
{ code: 'DE-HB', name: 'Bremen' },
|
||||||
|
{ code: 'DE-HH', name: 'Hamburg' },
|
||||||
|
{ code: 'DE-HE', name: 'Hessen' },
|
||||||
|
{ code: 'DE-MV', name: 'Mecklenburg-Vorpommern' },
|
||||||
|
{ code: 'DE-NI', name: 'Niedersachsen' },
|
||||||
|
{ code: 'DE-NW', name: 'Nordrhein-Westfalen' },
|
||||||
|
{ code: 'DE-RP', name: 'Rheinland-Pfalz' },
|
||||||
|
{ code: 'DE-SL', name: 'Saarland' },
|
||||||
|
{ code: 'DE-SN', name: 'Sachsen' },
|
||||||
|
{ code: 'DE-ST', name: 'Sachsen-Anhalt' },
|
||||||
|
{ code: 'DE-SH', name: 'Schleswig-Holstein' },
|
||||||
|
{ code: 'DE-TH', name: 'Thüringen' }
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// Sicherstellen, dass das JSON-Feld existiert
|
||||||
|
function ensureWorkingHoursStructure() {
|
||||||
|
if (!profile.value.weekly_regular_working_hours) {
|
||||||
|
profile.value.weekly_regular_working_hours = {}
|
||||||
|
}
|
||||||
|
for (const { key } of weekdays) {
|
||||||
|
if (profile.value.weekly_regular_working_hours[key] == null) {
|
||||||
|
profile.value.weekly_regular_working_hours[key] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculateWeeklyHours() {
|
||||||
|
if (!profile.value?.weekly_regular_working_hours) return
|
||||||
|
|
||||||
|
const total = Object.values(profile.value.weekly_regular_working_hours).reduce(
|
||||||
|
(sum: number, val: any) => {
|
||||||
|
const num = parseFloat(val)
|
||||||
|
return sum + (isNaN(num) ? 0 : num)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
profile.value.weekly_working_hours = Number(total.toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkZip = async () => {
|
||||||
|
const zipData = await useFunctions().useZipCheck(profile.value.address_zip)
|
||||||
|
profile.value.address_city = zipData.short
|
||||||
|
profile.value.state_code = zipData.state_code
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchProfile)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Haupt-Navigation -->
|
||||||
|
<UDashboardNavbar title="Mitarbeiter">
|
||||||
|
<template #left>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
@click="navigateTo(`/staff/profiles`)"
|
||||||
|
icon="i-heroicons-chevron-left"
|
||||||
|
|
||||||
|
>
|
||||||
|
Mitarbeiter
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
<template #center>
|
||||||
|
<h1 class="text-xl font-medium truncate">
|
||||||
|
Mitarbeiter bearbeiten: {{ profile?.full_name || '' }}
|
||||||
|
</h1>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
icon="i-mdi-content-save"
|
||||||
|
color="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveProfile"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<!-- Inhalt -->
|
||||||
|
<UDashboardPanelContent>
|
||||||
|
<UCard v-if="!pending && profile">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<UAvatar size="xl" :alt="profile.full_name" />
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">{{ profile.full_name }}</h2>
|
||||||
|
<p class="text-sm text-gray-500">{{ profile.employee_number || '–' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UDivider label="Persönliche Daten" />
|
||||||
|
|
||||||
|
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||||
|
<UFormGroup label="Vorname">
|
||||||
|
<UInput v-model="profile.first_name" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Nachname">
|
||||||
|
<UInput v-model="profile.last_name" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="E-Mail">
|
||||||
|
<UInput v-model="profile.email" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Telefon (Mobil)">
|
||||||
|
<UInput v-model="profile.mobile_tel" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Telefon (Festnetz)">
|
||||||
|
<UInput v-model="profile.fixed_tel" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Geburtstag">
|
||||||
|
<UInput type="date" v-model="profile.birthday" />
|
||||||
|
</UFormGroup>
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
<UCard v-if="!pending && profile" class="mt-3">
|
||||||
|
<UDivider label="Vertragsinformationen" />
|
||||||
|
|
||||||
|
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||||
|
<UFormGroup label="Vertragsart">
|
||||||
|
<UInput v-model="profile.contract_type"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Status">
|
||||||
|
<UInput v-model="profile.status"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Position">
|
||||||
|
<UInput v-model="profile.position"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Qualifikation">
|
||||||
|
<UInput v-model="profile.qualification"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Eintrittsdatum">
|
||||||
|
<UInput type="date" v-model="profile.entry_date" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Wöchentliche Arbeitszeit (Std)">
|
||||||
|
<UInput type="number" v-model="profile.weekly_working_hours" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Bezahlte Urlaubstage (Jahr)">
|
||||||
|
<UInput type="number" v-model="profile.annual_paid_leave_days" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Aktiv">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UToggle v-model="profile.active" color="primary" />
|
||||||
|
<span class="text-sm text-gray-600">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</UFormGroup>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard v-if="!pending && profile" class="mt-3">
|
||||||
|
<UDivider label="Adresse & Standort" />
|
||||||
|
|
||||||
|
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||||
|
<UFormGroup label="Straße und Hausnummer">
|
||||||
|
<UInput v-model="profile.address_street"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="PLZ">
|
||||||
|
<UInput type="text" v-model="profile.address_zip" @focusout="checkZip"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Ort">
|
||||||
|
<UInput v-model="profile.address_city"/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Bundesland">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="profile.state_code"
|
||||||
|
:options="bundeslaender"
|
||||||
|
value-attribute="code"
|
||||||
|
option-attribute="name"
|
||||||
|
placeholder="Bundesland auswählen"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
|
||||||
|
<UCard v-if="!pending && profile" class="mt-3">
|
||||||
|
<UDivider label="Wöchentliche Arbeitsstunden" />
|
||||||
|
|
||||||
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="day in weekdays"
|
||||||
|
:key="day.key"
|
||||||
|
:class="[...profile.weekly_regular_working_hours[day.key] === 0 ? ['bg-gray-100'] : ['bg-gray-100','border-green-400'], 'flex items-center justify-between border rounded-lg p-3 bg-gray-50']"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-700">{{ day.label }}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInput
|
||||||
|
type="number"
|
||||||
|
size="sm"
|
||||||
|
min="0"
|
||||||
|
max="24"
|
||||||
|
step="0.25"
|
||||||
|
v-model.number="profile.weekly_regular_working_hours[day.key]"
|
||||||
|
placeholder="0"
|
||||||
|
class="w-24"
|
||||||
|
@change="recalculateWeeklyHours"
|
||||||
|
/>
|
||||||
|
<span class="text-gray-400 text-sm">Std</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard v-if="!pending && profile" class="mt-3">
|
||||||
|
<UDivider label="Sonstiges" />
|
||||||
|
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||||
|
<UFormGroup label="Kleidergröße (Oberteil)">
|
||||||
|
<UInput v-model="profile.clothing_size_top" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Kleidergröße (Hose)">
|
||||||
|
<UInput v-model="profile.clothing_size_bottom" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Schuhgröße">
|
||||||
|
<UInput v-model="profile.clothing_size_shoe" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Token-ID">
|
||||||
|
<UInput v-model="profile.token_id" />
|
||||||
|
</UFormGroup>
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<USkeleton v-if="pending" height="300px" />
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const dataStore = useDataStore()
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -40,10 +38,10 @@
|
|||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<UTable
|
<UTable
|
||||||
:rows="items"
|
:rows="items"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
@select="(i) => navigateTo(`/staff/profiles/${i.id}`)"
|
||||||
>
|
>
|
||||||
|
|
||||||
</UTable>
|
</UTable>
|
||||||
235
pages/staff/time/[id]/evaluate.vue
Normal file
235
pages/staff/time/[id]/evaluate.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<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"
|
||||||
|
location="show_time_evaluation"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
@@ -43,7 +43,12 @@ const setupPage = async (sort_column = null, sort_direction = null) => {
|
|||||||
console.log(item.value)
|
console.log(item.value)
|
||||||
} else if (mode.value === "list") {
|
} else if (mode.value === "list") {
|
||||||
//Load Data for List
|
//Load Data for List
|
||||||
items.value = await useEntities(type).select(dataType.supabaseSelectWithInformation, sort_column || dataType.supabaseSortColumn, sort_direction === "asc", true)
|
//items.value = await useEntities(type).select(dataType.supabaseSelectWithInformation, sort_column || dataType.supabaseSortColumn, sort_direction === "asc", true)
|
||||||
|
items.value = await useEntities(type).select({
|
||||||
|
filters: {},
|
||||||
|
sort: [],
|
||||||
|
page:1
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
449
pages/standardEntity/[type]/index.vue
Normal file
449
pages/standardEntity/[type]/index.vue
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useTempStore} from "~/stores/temp.js";
|
||||||
|
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
||||||
|
import EntityTable from "~/components/EntityTable.vue";
|
||||||
|
import EntityTableMobile from "~/components/EntityTableMobile.vue";
|
||||||
|
|
||||||
|
const { has } = usePermission()
|
||||||
|
|
||||||
|
defineShortcuts({
|
||||||
|
'/': () => {
|
||||||
|
//console.log(searchinput)
|
||||||
|
//searchinput.value.focus()
|
||||||
|
document.getElementById("searchinput").focus()
|
||||||
|
},
|
||||||
|
'+': () => {
|
||||||
|
router.push(`/standardEntity/${type}/create`)
|
||||||
|
},
|
||||||
|
'Enter': {
|
||||||
|
usingInput: true,
|
||||||
|
handler: () => {
|
||||||
|
router.push(`/standardEntity/${type}/show/${items.value[selectedItem.value].id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'arrowdown': () => {
|
||||||
|
if(selectedItem.value < items.value.length - 1) {
|
||||||
|
selectedItem.value += 1
|
||||||
|
} else {
|
||||||
|
selectedItem.value = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'arrowup': () => {
|
||||||
|
if(selectedItem.value === 0) {
|
||||||
|
selectedItem.value = items.value.length - 1
|
||||||
|
} else {
|
||||||
|
selectedItem.value -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
const tempStore = useTempStore()
|
||||||
|
|
||||||
|
//VARS
|
||||||
|
const type = route.params.type
|
||||||
|
|
||||||
|
const dataType = dataStore.dataTypes[type]
|
||||||
|
|
||||||
|
const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
|
||||||
|
const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.find(i => i.key === column.key)))
|
||||||
|
|
||||||
|
const searchString = ref(tempStore.searchStrings[type] ||'')
|
||||||
|
|
||||||
|
const items = ref([])
|
||||||
|
const itemsMeta = ref({})
|
||||||
|
const selectedItem = ref(0)
|
||||||
|
const loading = ref(true)
|
||||||
|
const initialSetupDone = ref(false)
|
||||||
|
const pageLimit = ref(15)
|
||||||
|
const page = ref(tempStore.pages[type] || 1)
|
||||||
|
|
||||||
|
const sort = ref({
|
||||||
|
column: dataType.supabaseSortColumn || "created_at",
|
||||||
|
direction: 'desc'
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnsToFilter = ref({})
|
||||||
|
|
||||||
|
//Functions
|
||||||
|
|
||||||
|
const clearSearchString = () => {
|
||||||
|
tempStore.clearSearchString(type)
|
||||||
|
searchString.value = ''
|
||||||
|
setupPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const performSearch = async () => {
|
||||||
|
tempStore.modifySearchString(type,searchString)
|
||||||
|
setupPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changePage = (number) => {
|
||||||
|
page.value = number
|
||||||
|
tempStore.modifyPages(type, number)
|
||||||
|
setupPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetColum = (column) => {
|
||||||
|
columnsToFilter.value[column] = itemsMeta.value.distinctValues[column]
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSort = (column) => {
|
||||||
|
if(sort.value.column === column) {
|
||||||
|
sort.value.direction = sort.value.direction === 'desc' ? 'asc' : 'desc'
|
||||||
|
} else {
|
||||||
|
sort.value.direction = "asc"
|
||||||
|
sort.value.column = column
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
//SETUP
|
||||||
|
|
||||||
|
const setupPage = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
archived:false
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(columnsToFilter.value).forEach((column) => {
|
||||||
|
if(columnsToFilter.value[column].length !== itemsMeta.value.distinctValues[column].length) {
|
||||||
|
filters[column] = columnsToFilter.value[column]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const {data,meta} = await useEntities(type).selectPaginated({
|
||||||
|
select: dataType.supabaseSelectWithInformation || "*",
|
||||||
|
filters: filters,
|
||||||
|
sort: [{field: sort.value.column, direction: sort.value.direction}],
|
||||||
|
page: page.value,
|
||||||
|
limit: pageLimit.value,
|
||||||
|
search: searchString.value,
|
||||||
|
searchColumns: columns.value.map((i) => i.key),
|
||||||
|
distinctColumns: dataType.templateColumns.filter(i => i.distinct).map(i => i.key),
|
||||||
|
})
|
||||||
|
|
||||||
|
items.value = data
|
||||||
|
itemsMeta.value = meta
|
||||||
|
if(!initialSetupDone.value){
|
||||||
|
Object.keys(itemsMeta.value.distinctValues).forEach(distinctValue => {
|
||||||
|
columnsToFilter.value[distinctValue] = itemsMeta.value.distinctValues[distinctValue]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
initialSetupDone.value = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPage()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <FloatingActionButton
|
||||||
|
:label="`+ ${dataType.labelSingle}`"
|
||||||
|
variant="outline"
|
||||||
|
v-if="platform === 'mobile'"
|
||||||
|
@click="router.push(`/standardEntity/${type}/create`)"
|
||||||
|
/>-->
|
||||||
|
<UDashboardNavbar :title="dataType.label" :badge="itemsMeta.total">
|
||||||
|
<template #toggle>
|
||||||
|
<div v-if="platform === 'mobile'"></div>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<UTooltip :text="`${dataType.label} durchsuchen`">
|
||||||
|
<UInput
|
||||||
|
id="searchinput"
|
||||||
|
v-model="searchString"
|
||||||
|
icon="i-heroicons-funnel"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Suche..."
|
||||||
|
class="hidden lg:block"
|
||||||
|
@keydown.esc="$event.target.blur()"
|
||||||
|
@keyup="performSearch"
|
||||||
|
@change="performSearch"
|
||||||
|
>
|
||||||
|
<template #trailing>
|
||||||
|
<UKbd value="/" />
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Suche löschen" v-if="searchString.length > 0">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
variant="outline"
|
||||||
|
color="rose"
|
||||||
|
@click="clearSearchString()"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
|
||||||
|
<UTooltip :text="`${dataType.labelSingle} erstellen`">
|
||||||
|
<UButton
|
||||||
|
v-if="platform !== 'mobile' && has(`${type}-create`)/*&& useRole().checkRight(`${type}-create`)*/"
|
||||||
|
@click="router.push(`/standardEntity/${type}/create`)"
|
||||||
|
class="ml-3"
|
||||||
|
>+ {{dataType.labelSingle}}</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UTooltip :text="`${dataType.label} pro Seite`">
|
||||||
|
<USelectMenu
|
||||||
|
:options="[10,15,25,50,100,250]"
|
||||||
|
v-model="pageLimit"
|
||||||
|
@change="setupPage"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<!-- <UTooltip text="Erste Seite">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changePage(1)"
|
||||||
|
icon="i-heroicons-chevron-double-left"
|
||||||
|
:disabled="page <= 1"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Eine Seite nach vorne">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changePage(page-1)"
|
||||||
|
icon="i-heroicons-chevron-left"
|
||||||
|
:disabled="page <= 1"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
v-for="pageNumber in itemsMeta.totalPages"
|
||||||
|
:text="`Zu Seite ${pageNumber} wechseln`"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
:variant="page === pageNumber ? 'solid' : 'outline'"
|
||||||
|
@click="changePage(pageNumber)"
|
||||||
|
>
|
||||||
|
{{pageNumber}}
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Eine Seite nach hinten">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changePage(page+1)"
|
||||||
|
icon="i-heroicons-chevron-right"
|
||||||
|
:disabled="page >= itemsMeta.totalPages"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Letzte Seite">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changePage(itemsMeta.totalPages)"
|
||||||
|
icon="i-heroicons-chevron-double-right"
|
||||||
|
:disabled="page >= itemsMeta.totalPages"
|
||||||
|
/>
|
||||||
|
</UTooltip>-->
|
||||||
|
<UPagination
|
||||||
|
v-if="initialSetupDone && items.length > 0"
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="page"
|
||||||
|
:page-count="pageLimit"
|
||||||
|
:total="itemsMeta.total"
|
||||||
|
@update:modelValue="(i) => changePage(i)"
|
||||||
|
show-first
|
||||||
|
show-last
|
||||||
|
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
|
||||||
|
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UTooltip text="Angezeigte Spalten auswählen">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="selectedColumns"
|
||||||
|
icon="i-heroicons-adjustments-horizontal-solid"
|
||||||
|
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||||
|
multiple
|
||||||
|
class="hidden lg:block"
|
||||||
|
by="key"
|
||||||
|
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||||
|
:ui-menu="{ width: 'min-w-max' }"
|
||||||
|
@change="tempStore.modifyColumns(type,selectedColumns)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
Spalten
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<UTable
|
||||||
|
:loading="loading"
|
||||||
|
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||||
|
sort-mode="manual"
|
||||||
|
v-model:sort="sort"
|
||||||
|
@update:sort="setupPage"
|
||||||
|
v-if="dataType && columns && items.length > 0"
|
||||||
|
:rows="items"
|
||||||
|
:columns="columns"
|
||||||
|
class="w-full"
|
||||||
|
style="height: 85dvh"
|
||||||
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
|
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
|
||||||
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||||
|
v-slot:[`${column.key}-header`]="{row}">
|
||||||
|
<InputGroup>
|
||||||
|
<UTooltip v-if="column.sortable">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changeSort(column.key)"
|
||||||
|
:color="sort.column === column.key ? 'primary' : 'white'"
|
||||||
|
:icon="sort.column === column.key ? (sort.direction === 'asc' ? 'i-heroicons-arrow-up' : 'i-heroicons-arrow-down') : 'i-heroicons-arrows-up-down'"
|
||||||
|
>
|
||||||
|
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
v-if="column.distinct"
|
||||||
|
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
|
||||||
|
>
|
||||||
|
<USelectMenu
|
||||||
|
:options="itemsMeta?.distinctValues?.[column.key]"
|
||||||
|
v-model="columnsToFilter[column.key]"
|
||||||
|
multiple
|
||||||
|
@change="setupPage"
|
||||||
|
searchable
|
||||||
|
searchable-placeholder="Suche..."
|
||||||
|
:search-attributes="[column.key]"
|
||||||
|
:ui-menu="{ width: 'min-w-max' }"
|
||||||
|
clear-search-on-close
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Keine Einträge in der Spalte {{column.label}}
|
||||||
|
</template>
|
||||||
|
<template #default="{open}">
|
||||||
|
<UButton
|
||||||
|
:disabled="!columnsToFilter[column.key]?.length > 0"
|
||||||
|
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
|
||||||
|
:color="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'primary' : 'white'"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ column.label }}</span>
|
||||||
|
|
||||||
|
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
</USelectMenu>
|
||||||
|
</UTooltip>
|
||||||
|
<UButton
|
||||||
|
variant="solid"
|
||||||
|
color="white"
|
||||||
|
v-else
|
||||||
|
class="mr-2 truncate"
|
||||||
|
>{{column.label}}</UButton>
|
||||||
|
|
||||||
|
|
||||||
|
<UTooltip
|
||||||
|
text="Filter zurücksetzen"
|
||||||
|
v-if="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length && column.distinct"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
@click="resetColum(column.key)"
|
||||||
|
variant="outline"
|
||||||
|
color="rose"
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<template #name-data="{row}">
|
||||||
|
<span
|
||||||
|
v-if="row.id === items[selectedItem].id"
|
||||||
|
class="text-primary-500 font-bold">
|
||||||
|
<UTooltip
|
||||||
|
:text="row.name"
|
||||||
|
>
|
||||||
|
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
||||||
|
</UTooltip> </span>
|
||||||
|
<span v-else>
|
||||||
|
<UTooltip
|
||||||
|
:text="row.name"
|
||||||
|
>
|
||||||
|
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
||||||
|
</UTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #fullName-data="{row}">
|
||||||
|
<span
|
||||||
|
v-if="row.id === items[selectedItem].id"
|
||||||
|
class="text-primary-500 font-bold">{{row.fullName}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{row.fullName}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #licensePlate-data="{row}">
|
||||||
|
<span
|
||||||
|
v-if="row.id === items[selectedItem].id"
|
||||||
|
class="text-primary-500 font-bold">{{row.licensePlate}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{row.licensePlate}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
|
||||||
|
v-slot:[`${column.key}-data`]="{row}">
|
||||||
|
<component v-if="column.component" :is="column.component" :row="row"></component>
|
||||||
|
<span v-else-if="row[column.key]">
|
||||||
|
<UTooltip :text="row[column.key]">
|
||||||
|
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
|
||||||
|
</UTooltip>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
<UCard
|
||||||
|
class="w-1/3 mx-auto mt-10"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col text-center"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
class="mx-auto w-10 h-10 mb-5"
|
||||||
|
name="i-heroicons-circle-stack-20-solid"/>
|
||||||
|
<span class="font-bold">Keine {{dataType.label}} anzuzeigen</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -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>
|
|
||||||
14
plugins/dayjs.ts
Normal file
14
plugins/dayjs.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import duration from 'dayjs/plugin/duration'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
||||||
|
import 'dayjs/locale/de'
|
||||||
|
|
||||||
|
dayjs.extend(duration)
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.extend(localizedFormat)
|
||||||
|
dayjs.locale('de')
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
return { provide: { dayjs } }
|
||||||
|
})
|
||||||
@@ -17,6 +17,10 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
async init(token) {
|
async init(token) {
|
||||||
await this.fetchMe(token)
|
await this.fetchMe(token)
|
||||||
|
|
||||||
|
const tempStore = useTempStore()
|
||||||
|
|
||||||
|
if(this.profile.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
|
||||||
|
|
||||||
if(useCapacitor().getIsNative()) {
|
if(useCapacitor().getIsNative()) {
|
||||||
navigateTo("/mobile")
|
navigateTo("/mobile")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import space from "~/components/columnRenderings/space.vue"
|
|||||||
import driver from "~/components/columnRenderings/driver.vue"
|
import driver from "~/components/columnRenderings/driver.vue"
|
||||||
|
|
||||||
import quantity from "~/components/helpRenderings/quantity.vue"
|
import quantity from "~/components/helpRenderings/quantity.vue"
|
||||||
import {useZipCheck} from "~/composables/useZipCheck.js";
|
|
||||||
import {useFunctions} from "~/composables/useFunctions.js";
|
import {useFunctions} from "~/composables/useFunctions.js";
|
||||||
import signDate from "~/components/columnRenderings/signDate.vue";
|
import signDate from "~/components/columnRenderings/signDate.vue";
|
||||||
import sepaDate from "~/components/columnRenderings/sepaDate.vue";
|
import sepaDate from "~/components/columnRenderings/sepaDate.vue";
|
||||||
@@ -214,7 +213,8 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
},
|
},
|
||||||
inputColumn: "Allgemeines",
|
inputColumn: "Allgemeines",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
maxLength: 20
|
maxLength: 20,
|
||||||
|
distinct: true,
|
||||||
}, {
|
}, {
|
||||||
key: "nameAddition",
|
key: "nameAddition",
|
||||||
label: "Firmenname Zusatz",
|
label: "Firmenname Zusatz",
|
||||||
@@ -226,7 +226,8 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
return item.isCompany
|
return item.isCompany
|
||||||
},
|
},
|
||||||
inputColumn: "Allgemeines",
|
inputColumn: "Allgemeines",
|
||||||
maxLength: 20
|
maxLength: 20,
|
||||||
|
distinct: true,
|
||||||
},{
|
},{
|
||||||
key: "salutation",
|
key: "salutation",
|
||||||
label: "Anrede",
|
label: "Anrede",
|
||||||
@@ -253,7 +254,8 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
return !item.isCompany
|
return !item.isCompany
|
||||||
},
|
},
|
||||||
inputColumn: "Allgemeines",
|
inputColumn: "Allgemeines",
|
||||||
sortable: true
|
sortable: true,
|
||||||
|
distinct: true
|
||||||
},{
|
},{
|
||||||
key: "title",
|
key: "title",
|
||||||
label: "Titel",
|
label: "Titel",
|
||||||
@@ -340,7 +342,8 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
component: active,
|
component: active,
|
||||||
inputType: "bool",
|
inputType: "bool",
|
||||||
inputColumn: "Allgemeines",
|
inputColumn: "Allgemeines",
|
||||||
sortable: true
|
sortable: true,
|
||||||
|
distinct: true
|
||||||
}, {
|
}, {
|
||||||
key: "customPaymentDays",
|
key: "customPaymentDays",
|
||||||
label: "Zahlungsziel in Tagen",
|
label: "Zahlungsziel in Tagen",
|
||||||
@@ -385,7 +388,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputType: "number",
|
inputType: "number",
|
||||||
inputChangeFunction: async function (row) {
|
inputChangeFunction: async function (row) {
|
||||||
if(row.infoData.zip) {
|
if(row.infoData.zip) {
|
||||||
row.infoData.city = (await useZipCheck(row.infoData.zip))
|
row.infoData.city = (await useFunctions().useZipCheck(row.infoData.zip)).short
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
@@ -1331,7 +1334,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
inputChangeFunction: async function (row) {
|
inputChangeFunction: async function (row) {
|
||||||
if(row.infoData.zip) {
|
if(row.infoData.zip) {
|
||||||
row.infoData.city = (await useZipCheck(row.infoData.zip))
|
row.infoData.city = (await useFunctions().useZipCheck(row.infoData.zip)).short
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1513,7 +1516,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputColumn: "Ort",
|
inputColumn: "Ort",
|
||||||
inputChangeFunction: async function (row) {
|
inputChangeFunction: async function (row) {
|
||||||
if(row.infoData.zip) {
|
if(row.infoData.zip) {
|
||||||
row.infoData.city = (await useZipCheck(row.infoData.zip))
|
row.infoData.city = (await useFunctions().useZipCheck(row.infoData.zip)).short
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2580,6 +2583,10 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
],
|
],
|
||||||
showTabs: [{label: 'Informationen'},{label: 'Buchungen'}]
|
showTabs: [{label: 'Informationen'},{label: 'Buchungen'}]
|
||||||
},
|
},
|
||||||
|
bankaccounts: {
|
||||||
|
label: "Bankkonten",
|
||||||
|
labelSingle: "Bankkonto",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentTypesForCreation = ref({
|
const documentTypesForCreation = ref({
|
||||||
|
|||||||
@@ -3,28 +3,68 @@ import {defineStore} from 'pinia'
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const useTempStore = defineStore('temp', () => {
|
export const useTempStore = defineStore('temp', () => {
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const searchStrings = ref({})
|
const searchStrings = ref({})
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const columns = ref({})
|
const columns = ref({})
|
||||||
|
const pages = ref({})
|
||||||
|
const settings = ref({})
|
||||||
|
|
||||||
|
const storeTempConfig = async () => {
|
||||||
|
const config = {
|
||||||
|
searchStrings: searchStrings.value,
|
||||||
|
columns: columns.value,
|
||||||
|
pages: pages.value,
|
||||||
|
settings: settings.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
await useNuxtApp().$api(`/api/profiles/${auth.profile.id}`,{
|
||||||
|
method: 'PUT',
|
||||||
|
body: {temp_config: config}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStoredTempConfig (config) {
|
||||||
|
searchStrings.value = config.searchStrings
|
||||||
|
columns.value = config.columns
|
||||||
|
pages.value = config.pages
|
||||||
|
settings.value = config.settings
|
||||||
|
}
|
||||||
|
|
||||||
function modifySearchString(type,input) {
|
function modifySearchString(type,input) {
|
||||||
searchStrings.value[type] = input
|
searchStrings.value[type] = input
|
||||||
|
storeTempConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearchString(type) {
|
function clearSearchString(type) {
|
||||||
searchStrings.value[type] = ""
|
searchStrings.value[type] = ""
|
||||||
|
storeTempConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyFilter(type,input) {
|
function modifyFilter(type,input) {
|
||||||
filters.value[type] = input
|
filters.value[type] = input
|
||||||
|
storeTempConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyColumns(type,input) {
|
function modifyColumns(type,input) {
|
||||||
columns.value[type] = input
|
columns.value[type] = input
|
||||||
|
storeTempConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyPages(type,input) {
|
||||||
|
pages.value[type] = input
|
||||||
|
storeTempConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifySettings(type,input) {
|
||||||
|
settings.value[type] = input
|
||||||
|
storeTempConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
setStoredTempConfig,
|
||||||
searchStrings,
|
searchStrings,
|
||||||
modifySearchString,
|
modifySearchString,
|
||||||
clearSearchString,
|
clearSearchString,
|
||||||
@@ -32,6 +72,10 @@ export const useTempStore = defineStore('temp', () => {
|
|||||||
modifyFilter,
|
modifyFilter,
|
||||||
columns,
|
columns,
|
||||||
modifyColumns,
|
modifyColumns,
|
||||||
|
modifyPages,
|
||||||
|
pages,
|
||||||
|
modifySettings,
|
||||||
|
settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user