From 888336dd0462aea1694cc8b99ac353b5eb84bd62 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 8 Dec 2025 14:40:21 +0100 Subject: [PATCH] BETA for new DB --- components/MainNav.vue | 10 -- components/StaffTimeEntryModal.vue | 150 ++++++++++++++++----- composables/useEntities.ts | 2 +- composables/useFormat.ts | 17 +++ composables/useStaffTime.ts | 13 +- pages/accounts/index.vue | 18 ++- pages/banking/statements/[mode]/[[id]].vue | 16 +-- pages/staff/time/index.vue | 98 ++++++++++++-- 8 files changed, 256 insertions(+), 68 deletions(-) diff --git a/components/MainNav.vue b/components/MainNav.vue index 38d5cae..dc00730 100644 --- a/components/MainNav.vue +++ b/components/MainNav.vue @@ -155,7 +155,6 @@ const links = computed(() => { label: "Anwesenheiten", to: "/staff/time", icon: "i-heroicons-clock", - disabled: true }] : [], /*... has("absencerequests") ? [{ label: "Abwesenheiten", @@ -186,7 +185,6 @@ const links = computed(() => { label: "Eingangsbelege", to: "/incomingInvoices", icon: "i-heroicons-document-text", - disabled: true },{ label: "Kostenstellen", to: "/standardEntity/costcentres", @@ -195,7 +193,6 @@ const links = computed(() => { label: "Buchungskonten", to: "/accounts", icon: "i-heroicons-document-text", - disabled: true },{ label: "zusätzliche Buchungskonten", to: "/standardEntity/ownaccounts", @@ -205,7 +202,6 @@ const links = computed(() => { label: "Bank", to: "/banking", icon: "i-heroicons-document-text", - disabled: true }, ] }], @@ -312,7 +308,6 @@ const links = computed(() => { label: "Nummernkreise", to: "/settings/numberRanges", icon: "i-heroicons-clipboard-document-list", - disabled: true },/*{ label: "Rollen", to: "/roles", @@ -321,17 +316,14 @@ const links = computed(() => { label: "E-Mail Konten", to: "/settings/emailaccounts", icon: "i-heroicons-envelope", - disabled: true },{ label: "Bankkonten", to: "/settings/banking", icon: "i-heroicons-currency-euro", - disabled: true },{ label: "Textvorlagen", to: "/settings/texttemplates", icon: "i-heroicons-clipboard-document-list", - disabled: true },/*{ label: "Eigene Felder", to: "/settings/ownfields", @@ -340,12 +332,10 @@ const links = computed(() => { label: "Firmeneinstellungen", to: "/settings/tenant", icon: "i-heroicons-building-office", - disabled: true },{ label: "Projekttypen", to: "/projecttypes", icon: "i-heroicons-clipboard-document-list", - disabled: true },{ label: "Export", to: "/export", diff --git a/components/StaffTimeEntryModal.vue b/components/StaffTimeEntryModal.vue index f1f5c33..e08ff26 100644 --- a/components/StaffTimeEntryModal.vue +++ b/components/StaffTimeEntryModal.vue @@ -3,55 +3,75 @@ import dayjs from "dayjs"; const props = defineProps<{ modelValue: boolean; - entry?: null; + entry?: any | null; + users: any[]; + canSelectUser: boolean; + defaultUserId: string; }>(); 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; -}>({ +// 🌈 Typen +const typeOptions = [ + { label: "Arbeitszeit", value: "work" }, + { label: "Urlaub", value: "vacation" }, + { label: "Krankheit", value: "sick" }, + { label: "Feiertag", value: "holiday" }, +]; + +// Lokaler State +const local = reactive({ id: "", + user_id: "", // 👈 Mitarbeiter description: "", - started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"), - stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"), + started_at: "", + stopped_at: "", type: "work", + vacation_reason: "", + sick_reason: "", }); -// 📡 Wenn das Modal geöffnet wird, Entry-Daten übernehmen +// 📡 ENTRY —> LOCAL watch( () => props.entry, (val) => { if (val) { Object.assign(local, { id: val.id, + user_id: val.user_id, // 👈 Mitarbeiter vorbelegen 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", + + started_at: + val.type === "vacation" + ? dayjs(val.started_at).format("YYYY-MM-DD") + : dayjs(val.started_at).format("YYYY-MM-DDTHH:mm"), + + stopped_at: + val.type === "vacation" + ? dayjs(val.stopped_at).format("YYYY-MM-DD") + : dayjs(val.stopped_at).format("YYYY-MM-DDTHH:mm"), + + vacation_reason: val.vacation_reason || "", + sick_reason: val.sick_reason || "", }); } else { Object.assign(local, { id: "", + user_id: props.defaultUserId, // 👈 Neuer Eintrag → aktueller Nutzer description: "", - started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"), - stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"), type: "work", + started_at: dayjs().format("YYYY-MM-DDTHH:mm"), + stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"), + vacation_reason: "", + sick_reason: "", }); } }, @@ -63,13 +83,27 @@ 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, + const payload: any = { + user_id: local.user_id, // 👈 immer senden type: local.type, }; + if (local.type === "vacation") { + payload.started_at = dayjs(local.started_at).startOf("day").toISOString(); + payload.stopped_at = dayjs(local.stopped_at).endOf("day").toISOString(); + payload.vacation_reason = local.vacation_reason; + } else { + payload.started_at = dayjs(local.started_at).toISOString(); + payload.stopped_at = local.stopped_at + ? dayjs(local.stopped_at).toISOString() + : null; + payload.description = local.description; + + if (local.type === "sick") { + payload.sick_reason = local.sick_reason; + } + } + if (local.id) { await update(local.id, payload); } else { @@ -84,6 +118,7 @@ async function handleSubmit() { } + - - + + + + - - + + + - - - + + + + + + + + + +
@@ -114,3 +203,4 @@ async function handleSubmit() { + diff --git a/composables/useEntities.ts b/composables/useEntities.ts index 1a92bb4..fbf0459 100644 --- a/composables/useEntities.ts +++ b/composables/useEntities.ts @@ -138,7 +138,7 @@ export const useEntities = ( ) => { if (!idToEq) return null - const res = await useNuxtApp().$api(withInformation ? `/api/resource/${relation}/${idToEq}/${withInformation}` : `/api/resource/${relation}/${idToEq}`, { + const res = await useNuxtApp().$api(withInformation ? `/api/resource/${relation}/${idToEq}` : `/api/resource/${relation}/${idToEq}`, { method: "GET", params: { select } }) diff --git a/composables/useFormat.ts b/composables/useFormat.ts index c908968..6182b99 100644 --- a/composables/useFormat.ts +++ b/composables/useFormat.ts @@ -5,4 +5,21 @@ export const useFormatDuration = (durationInMinutes:number,) => { const mins = Math.floor(durationInMinutes % 60) return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}` +} + +export const useFormatDurationDays = (start,end) => { + const startDate = useNuxtApp().$dayjs(start); + const endDate = useNuxtApp().$dayjs(end); + + if(startDate.isBefore(endDate)){ + // inkl. beider Tage → +1 + const days = endDate.diff(startDate, "day") + 1; + + return days + " Tag" + (days > 1 ? "e" : ""); + } else { + const days = startDate.diff(endDate, "day") + 1; + + return days + " Tag" + (days > 1 ? "e" : ""); + } + } \ No newline at end of file diff --git a/composables/useStaffTime.ts b/composables/useStaffTime.ts index e495d6a..4db7d36 100644 --- a/composables/useStaffTime.ts +++ b/composables/useStaffTime.ts @@ -10,6 +10,7 @@ interface StaffTimeEntry { export function useStaffTime() { const { $api } = useNuxtApp() + const auth = useAuthStore() @@ -46,9 +47,17 @@ export function useStaffTime() { } async function approve(id: string) { - return await $api(`/api/staff/time/${id}`, { + const auth = useAuthStore() + const now = useNuxtApp().$dayjs().toISOString() + + return await $api(`/api/staff/time/${id}`, { method: 'PUT', - body: { state: 'approved' }, + body: { + state: 'approved', + //@ts-ignore + approved_by: auth.user.id, + approved_at: now, + }, }) } diff --git a/pages/accounts/index.vue b/pages/accounts/index.vue index e955732..3472214 100644 --- a/pages/accounts/index.vue +++ b/pages/accounts/index.vue @@ -16,9 +16,15 @@ const router = useRouter() const items = ref([]) const dataLoaded = ref(false) +const statementallocations = ref([]) +const incominginvoices = ref([]) + const setupPage = async () => { items.value = await useEntities("accounts").selectSpecial() + statementallocations.value = (await useEntities("statementallocations").select("*, bs_id(*)")) + incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)")) + items.value = await Promise.all(items.value.map(async (i) => { let renderedAllocationsTemp = await renderedAllocations(i.id) let saldo = getSaldo(renderedAllocationsTemp) @@ -37,22 +43,22 @@ const setupPage = async () => { const renderedAllocations = async (account) => { - let statementallocations = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === account) - let incominginvoices = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === account)) + let statementallocationslocal = statementallocations.value.filter(i => i.account === account) + let incominginvoiceslocal = incominginvoices.value.filter(i => i.accounts.find(x => x.account === account)) - let tempstatementallocations = statementallocations.map(i => { + let tempstatementallocations = statementallocationslocal.map(i => { return { ...i, type: "statementallocation", - date: i.bs_id.date, - partner: i.bs_id ? (i.bs_id.debName ? i.bs_id.debName : (i.bs_id.credName ? i.bs_id.credName : '')) : '' + date: i.bankstatement.date, + partner: i.bankstatement ? (i.bankstatement.debName ? i.bankstatement.debName : (i.bankstatement.credName ? i.bankstatement.credName : '')) : '' } }) let incominginvoicesallocations = [] - incominginvoices.forEach(i => { + incominginvoiceslocal.forEach(i => { incominginvoicesallocations.push(...i.accounts.filter(x => x.account === account).map(x => { return { diff --git a/pages/banking/statements/[mode]/[[id]].vue b/pages/banking/statements/[mode]/[[id]].vue index 37f5db9..5f90098 100644 --- a/pages/banking/statements/[mode]/[[id]].vue +++ b/pages/banking/statements/[mode]/[[id]].vue @@ -65,8 +65,8 @@ const setup = async () => { console.log(openDocuments.value) - allocatedDocuments.value = documents.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)) + allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id)) + allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id)) console.log(allocatedDocuments.value) console.log(allocatedIncomingInvoices.value) 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)) @@ -611,7 +611,7 @@ const archiveStatement = async () => { variant="outline" icon="i-heroicons-check" :disabled="!accountToSave" - @click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })" + @click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })" /> { variant="outline" icon="i-heroicons-check" :disabled="!ownAccountToSave" - @click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave, description: allocationDescription })" + @click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave, description: allocationDescription })" /> { variant="outline" icon="i-heroicons-check" :disabled="!customerAccountToSave" - @click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, customer: customerAccountToSave, description: allocationDescription })" + @click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, customer: customerAccountToSave, description: allocationDescription })" /> { variant="outline" icon="i-heroicons-check" :disabled="!vendorAccountToSave" - @click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, vendor: vendorAccountToSave, description: allocationDescription })" + @click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, vendor: vendorAccountToSave, description: allocationDescription })" /> { variant="outline" class="mr-3" v-if="!itemInfo.statementallocations.find(i => i.cd_id === document.id)" - @click="saveAllocation({cd_id: document.id, bs_id: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription})" + @click="saveAllocation({cd_id: document.id, bankstatement: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription})" /> { variant="outline" class="mr-3" v-if="!itemInfo.statementallocations.find(i => i.ii_id === item.id)" - @click="saveAllocation({ii_id: item.id, bs_id: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})" + @click="saveAllocation({ii_id: item.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})" /> auth.permissions.includes('staff.time.read_all')) +const typeLabel = { + work: "Arbeitszeit", + vacation: "Urlaub", + sick: "Krankheit", + holiday: "Feiertag", + other: "Sonstiges" +} + +const typeColor = { + work: "gray", + vacation: "yellow", + sick: "rose", + holiday: "blue", + other: "gray" +} + + async function loadUsers() { if (!canViewAll.value) return // Beispiel: User aus Supabase holen @@ -171,28 +188,65 @@ onMounted(async () => { { key: 'started_at', label: 'Start' }, { key: 'stopped_at', label: 'Ende' }, { key: 'duration_minutes', label: 'Dauer' }, + { key: 'user', label: 'Mitarbeiter' }, + { key: 'type', label: 'Typ' }, { key: 'description', label: 'Beschreibung' }, - ]" + ]" > - - @@ -284,9 +346,16 @@ onMounted(async () => { @click="handleEdit(row)" >
-

- {{ row.description || 'Keine Beschreibung' }} -

+
+ {{ row.description || 'Keine Beschreibung' }} + + + {{ typeLabel[row.type] }} + +
+