diff --git a/composables/useFunctions.js b/composables/useFunctions.js index a56ac3c..e475b50 100644 --- a/composables/useFunctions.js +++ b/composables/useFunctions.js @@ -7,7 +7,20 @@ export const useFunctions = () => { const supabase = useSupabaseClient() const getWorkingTimesEvaluationData = async (user_id, startDate, endDate) => { - return (await useNuxtApp().$api(`/api/functions/timeevaluation/${user_id}?start_date=${startDate}&end_date=${endDate}`)) + // Der neue Endpunkt ist /staff/time/evaluation und erwartet die Benutzer-ID als targetUserId Query-Parameter. + + // Wir bauen den Query-String zusammen. + const queryParams = new URLSearchParams({ + from: startDate, + to: endDate, + targetUserId: user_id, // Die ID wird als targetUserId übergeben + }); + + // Der neue API-Pfad verwendet nur noch den Basis-Endpunkt. + const url = `/api/staff/time/evaluation?${queryParams.toString()}`; + + // Annahme: useNuxtApp().$api führt den GET-Request aus und liefert die Daten zurück. + return (await useNuxtApp().$api(url)); } const useNextNumber = async (numberRange) => { diff --git a/composables/useStaffTime.ts b/composables/useStaffTime.ts index 4db7d36..b19145d 100644 --- a/composables/useStaffTime.ts +++ b/composables/useStaffTime.ts @@ -1,77 +1,160 @@ -interface StaffTimeEntry { - id: string - started_at: string - stopped_at?: string | null - duration_minutes?: number | null - type: string - description?: string | null - created_at?: string -} +import { defineStore } from 'pinia' +import { useAuthStore } from '~/stores/auth' -export function useStaffTime() { - const { $api } = useNuxtApp() +export const useStaffTime = () => { + const { $api, $dayjs } = useNuxtApp() const auth = useAuthStore() + /** + * Lädt die Zeitspannen (Spans) für die Liste. + * Nutzt jetzt GET /api/staff/time/spans + */ + const list = async (filter?: { user_id?: string, from?: string, to?: string }) => { + // Standard: Aktueller Monat + const from = filter?.from || $dayjs().startOf('month').format('YYYY-MM-DD') + const to = filter?.to || $dayjs().endOf('month').format('YYYY-MM-DD') + // Ziel-User: Entweder aus Filter (Admin-Ansicht) oder eigener User + const targetUserId = filter?.user_id || auth.user.id - 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('/api/staff/time', { - method: 'POST', - body: { - started_at: new Date().toISOString(), - type: 'work', - description, - }, + const params = new URLSearchParams({ + from, + to, + // Der Endpoint erwartet targetUserId, wenn man Daten eines anderen Users will + targetUserId }) + + try { + // 💡 UPDATE: Prefix /api hinzugefügt + const spans = await $api(`/api/staff/time/spans?${params.toString()}`) + + return (spans || []).map((span: any) => { + const start = $dayjs(span.startedAt) + // Wenn endedAt null ist, läuft die Zeit noch -> Dauer bis "jetzt" berechnen für Anzeige + const end = span.endedAt ? $dayjs(span.endedAt) : $dayjs() + const duration = end.diff(start, 'minute') + + return { + // ID: Wir nehmen die erste Event-ID (Start-Event) als Referenz für Aktionen + id: span.sourceEventIds && span.sourceEventIds.length > 0 ? span.sourceEventIds[0] : null, + + // Mapping Backend-Status -> Frontend-State + state: span.status, + + // Zeitstempel + started_at: span.startedAt, + stopped_at: span.endedAt, + + duration_minutes: duration, + + // Da der Endpoint nur die Spans zurückgibt, setzen wir die UserID + // auf den User, den wir angefragt haben. + user_id: targetUserId, + + type: span.type, + + // Payload/Description falls vorhanden + description: span.payload?.description || '' + } + }).sort((a: any, b: any) => $dayjs(b.started_at).valueOf() - $dayjs(a.started_at).valueOf()) // Sortierung: Neueste oben + + } catch (error) { + console.error("Fehler beim Laden der Zeiten:", error) + return [] + } } - async function stop(id: string) { - return await $api(`/api/staff/time/${id}/stop`, { - method: 'PUT', - body: { stopped_at: new Date().toISOString() }, - }) + /** + * Startet einen neuen Zeiteintrag. + * POST /api/staff/time/event + */ + const start = async (description = "Arbeitszeit") => { + try { + // 💡 UPDATE: Prefix /api hinzugefügt + await $api('/api/staff/time/event', { + method: 'POST', + body: { + eventtype: 'work_start', + eventtime: new Date().toISOString(), + payload: { description } + } + }) + } catch (error) { + console.error("Fehler beim Starten:", error) + throw error + } } - async function submit(id: string) { - return await $api(`/api/staff/time/${id}`, { - method: 'PUT', - body: { state: 'submitted' }, - }) + /** + * Stoppt die aktuelle Zeit. + * POST /api/staff/time/event + */ + const stop = async (entryId?: string) => { + try { + // 💡 UPDATE: Prefix /api hinzugefügt + await $api('/api/staff/time/event', { + method: 'POST', + body: { + eventtype: 'work_end', + eventtime: new Date().toISOString() + } + }) + } catch (error) { + console.error("Fehler beim Stoppen:", error) + throw error + } } - async function approve(id: string) { - const auth = useAuthStore() - const now = useNuxtApp().$dayjs().toISOString() - - return await $api(`/api/staff/time/${id}`, { - method: 'PUT', - body: { - state: 'approved', - //@ts-ignore - approved_by: auth.user.id, - approved_at: now, - }, - }) + /** + * Reicht einen Eintrag ein. + * POST /api/staff/time/submit + */ + const submit = async (entryId: string) => { + if (!entryId) return + try { + // 💡 UPDATE: Prefix /api hinzugefügt + await $api('/api/staff/time/submit', { + method: 'POST', + body: { + eventIds: [entryId] + } + }) + } catch (error) { + console.error("Fehler beim Einreichen:", error) + throw error + } } - async function get(id: string) { - return await $api(`/api/staff/time/${id}`, { method: 'GET' }) + /** + * Genehmigt einen Eintrag. + * POST /api/staff/time/approve + */ + const approve = async (entry: any) => { + if (!entry || !entry.id || !entry.user_id) { + console.error("Ungültiger Eintrag für Genehmigung (ID oder UserID fehlt)", entry) + return + } + + try { + // 💡 UPDATE: Prefix /api hinzugefügt + await $api('/api/staff/time/approve', { + method: 'POST', + body: { + eventIds: [entry.id], + employeeUserId: entry.user_id + } + }) + } catch (error) { + console.error("Fehler beim Genehmigen:", error) + throw error + } } - async function create(data: Record) { - return await $api('/api/staff/time', { method: 'POST', body: data }) + return { + list, + start, + stop, + submit, + approve } - - async function update(id: string, data: Record) { - return await $api(`/api/staff/time/${id}`, { method: 'PUT', body: data }) - } - - return { list, start, stop,submit,approve, get, create, update } -} +} \ No newline at end of file diff --git a/pages/staff/time/[id]/evaluate.vue b/pages/staff/time/[id]/evaluate.vue index 38ced35..8f75226 100644 --- a/pages/staff/time/[id]/evaluate.vue +++ b/pages/staff/time/[id]/evaluate.vue @@ -4,10 +4,25 @@ const router = useRouter() const route = useRoute() const auth = useAuthStore() const toast = useToast() + // 🔹 State -const workingtimes = ref([]) -const absencerequests = ref([]) -const workingTimeInfo = ref(null) +const workingTimeInfo = ref<{ + userId: string; + spans: any[]; // Neue Struktur für die Detailansicht/Tabelle + summary: { + sumWorkingMinutesSubmitted: number; + sumWorkingMinutesApproved: number; + sumWorkingMinutesRecreationDays: number; + sumRecreationDays: number; + sumWorkingMinutesVacationDays: number; + sumVacationDays: number; + sumWorkingMinutesSickDays: number; + sumSickDays: number; + timeSpanWorkingMinutes: number; + saldoApproved: number; + saldoSubmitted: number; + } | null; // Neue Struktur für die Zusammenfassung +} | null>(null) const platformIsNative = ref(useCapacitor().getIsNative()) @@ -20,12 +35,38 @@ const showDocument = ref(false) const uri = ref("") const itemInfo = ref({}) +const profile = ref(null) + +// 💡 Die ID des Benutzers, dessen Daten wir abrufen (aus der Route) +const evaluatedUserId = computed(() => route.params.id as string) + +/** + * Konvertiert Minuten in das Format HH:MM h + */ function formatMinutesToHHMM(minutes = 0) { const h = Math.floor(minutes / 60) const m = Math.floor(minutes % 60) return `${h}:${String(m).padStart(2, "0")} h` } +/** + * Berechnet die Dauer zwischen startedAt und endedAt in Minuten. + */ +function calculateDurationMinutes(start: string, end: string): number { + const startTime = $dayjs(start); + const endTime = $dayjs(end); + return endTime.diff(startTime, 'minute'); +} + +/** + * Formatiert die Dauer (in Minuten) in HH:MM h + */ +function formatSpanDuration(start: string, end: string): string { + const minutes = calculateDurationMinutes(start, end); + return formatMinutesToHHMM(minutes); +} + + // 📅 Zeitraumumschaltung function changeRange() { const rangeMap = { @@ -56,41 +97,67 @@ function changeRange() { loadWorkingTimeInfo() } -const profile = ref(null) - -// 📊 Daten laden +// 📊 Daten laden (Initialisierung) async function setupPage() { await changeRange() - profile.value = (await useNuxtApp().$api("/api/tenant/profiles")).data.find(i => i.user_id === route.params.id) + // Lade das Profil des Benutzers, der ausgewertet wird (route.params.id) + try { + const response = await useNuxtApp().$api(`/api/tenant/profiles`); + // Findet das Profil des Benutzers, dessen ID in der Route steht + profile.value = response.data.find(i => i.user_id === evaluatedUserId.value); + } catch (error) { + console.error("Fehler beim Laden des Profils:", error); + } console.log(profile.value) setPageLayout(platformIsNative.value ? 'mobile' : 'default') - } +// 💡 ANGEPASST: Ruft den neuen Endpunkt ab und speichert das gesamte Payload-Objekt async function loadWorkingTimeInfo() { - workingTimeInfo.value = await useFunctions().getWorkingTimesEvaluationData( - route.params.id, - selectedStartDay.value, - selectedEndDay.value - ) + + // Erstellt Query-Parameter für den neuen Backend-Endpunkt + const queryParams = new URLSearchParams({ + from: selectedStartDay.value, + to: selectedEndDay.value, + targetUserId: evaluatedUserId.value, + }); + + const url = `/api/staff/time/evaluation?${queryParams.toString()}`; + + // Führt den GET-Request zum neuen Endpunkt aus und speichert das gesamte Payload-Objekt { userId, spans, summary } + const data = await useNuxtApp().$api(url); + + workingTimeInfo.value = data; + openTab.value = 0 } // 📄 PDF generieren +// Frontend (index.vue) async function generateDocument() { - const path = (await useEntities("letterheads").select("*"))[0].path // TODO SELECT + if (!workingTimeInfo.value || !workingTimeInfo.value.summary) return; + + const path = (await useEntities("letterheads").select("*"))[0].path uri.value = await useFunctions().useCreatePDF({ full_name: profile.value.full_name, - employee_number: profile.value.employee_number ? profile.value.employee_number : "-", - ...workingTimeInfo.value}, path, "timesheet") + employee_number: profile.value.employee_number || "-", + // Wir übergeben das summary-Objekt flach (für Header-Daten) + ...workingTimeInfo.value.summary, + + // UND wir müssen die Spans explizit übergeben, damit die Tabelle generiert werden kann + spans: workingTimeInfo.value.spans + }, path, "timesheet") showDocument.value = true } + const fileSaved = ref(false) + +// 💾 Datei speichern async function saveFile() { try { let fileData = { @@ -107,9 +174,6 @@ async function saveFile() { } catch (error) { toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"}) } - - - } async function onTabChange(index: number) { @@ -118,7 +182,6 @@ async function onTabChange(index: number) { // Initialisierung await setupPage() -changeRange()