Leer lassen, wenn die Zeit noch läuft.
+
+
-
-
+
+
+
-
-
+
\ No newline at end of file
diff --git a/composables/useStaffTime.ts b/composables/useStaffTime.ts
index b19145d..791c0a0 100644
--- a/composables/useStaffTime.ts
+++ b/composables/useStaffTime.ts
@@ -5,156 +5,115 @@ 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
- */
+ // ... (list Funktion bleibt gleich) ...
const list = async (filter?: { user_id?: string, from?: string, to?: string }) => {
- // Standard: Aktueller Monat
+ // ... (Code wie zuvor)
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
-
- const params = new URLSearchParams({
- from,
- to,
- // Der Endpoint erwartet targetUserId, wenn man Daten eines anderen Users will
- targetUserId
- })
-
+ const params = new URLSearchParams({ from, to, 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
+ eventIds: span.sourceEventIds || [],
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.
+ duration_minutes: end.diff(start, 'minute'),
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
-
+ }).sort((a: any, b: any) => $dayjs(b.started_at).valueOf() - $dayjs(a.started_at).valueOf())
} catch (error) {
- console.error("Fehler beim Laden der Zeiten:", error)
+ console.error("Fehler beim Laden:", error)
return []
}
}
/**
- * Startet einen neuen Zeiteintrag.
- * POST /api/staff/time/event
+ * Startet "jetzt" (Live-Modus).
+ * Kann optional eine Zeit empfangen (für manuelle Korrekturen),
+ * aber wir nutzen dafür besser die createEntry Funktion unten.
*/
- 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
- }
+ const start = async (description = "Arbeitszeit", time?: string) => {
+ await $api('/api/staff/time/event', {
+ method: 'POST',
+ body: {
+ eventtype: 'work_start',
+ eventtime: time || new Date().toISOString(), // 💡 Fix: Zeit akzeptieren
+ payload: { description }
+ }
+ })
}
- /**
- * 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
- }
+ const stop = async () => {
+ await $api('/api/staff/time/event', { method: 'POST', body: { eventtype: 'work_end', eventtime: new Date().toISOString() } })
}
- /**
- * 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
- }
+ const submit = async (entry: any) => {
+ const ids = entry.eventIds || (entry.id ? [entry.id] : [entry]);
+ if (!ids || ids.length === 0) return
+ await $api('/api/staff/time/submit', { method: 'POST', body: { eventIds: ids } })
}
- /**
- * 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
- }
+ if (!entry?.user_id) return
+ const ids = entry.eventIds || [entry.id];
+ await $api('/api/staff/time/approve', { method: 'POST', body: { eventIds: ids, employeeUserId: entry.user_id } })
+ }
- try {
- // 💡 UPDATE: Prefix /api hinzugefügt
- await $api('/api/staff/time/approve', {
+ const reject = async (entry: any, reason = "Abgelehnt") => {
+ if (!entry?.user_id) return
+ const ids = entry.eventIds || [entry.id];
+ await $api('/api/staff/time/reject', { method: 'POST', body: { eventIds: ids, employeeUserId: entry.user_id, reason } })
+ }
+
+ const update = async (entry: any, newData: { start: string, end: string | null, type: string, description: string }) => {
+ if (!entry || !entry.eventIds || entry.eventIds.length === 0) {
+ throw new Error("Bearbeiten fehlgeschlagen: Keine IDs.")
+ }
+ await $api('/api/staff/time/edit', {
+ method: 'POST',
+ body: {
+ originalEventIds: entry.eventIds,
+ newStart: newData.start,
+ newEnd: newData.end,
+ newType: newData.type,
+ description: newData.description,
+ reason: "Manuelle Bearbeitung"
+ }
+ })
+ }
+
+ // 🆕 NEU: Manuellen Eintrag erstellen (Vergangenheit oder Zeitraum)
+ const createEntry = async (data: { start: string, end: string | null, type: string, description: string }) => {
+ // 1. Start Event senden
+ // Wir nutzen den dynamischen Typ (work_start, vacation_start etc.)
+ await $api('/api/staff/time/event', {
+ method: 'POST',
+ body: {
+ eventtype: `${data.type}_start`,
+ eventtime: data.start,
+ payload: { description: data.description }
+ }
+ })
+
+ // 2. End Event senden (falls vorhanden)
+ if (data.end) {
+ await $api('/api/staff/time/event', {
method: 'POST',
body: {
- eventIds: [entry.id],
- employeeUserId: entry.user_id
+ eventtype: `${data.type}_end`,
+ eventtime: data.end
}
})
- } catch (error) {
- console.error("Fehler beim Genehmigen:", error)
- throw error
}
}
- return {
- list,
- start,
- stop,
- submit,
- approve
- }
+ return { list, start, stop, submit, approve, reject, update, createEntry }
}
\ No newline at end of file
diff --git a/pages/staff/time/index.vue b/pages/staff/time/index.vue
index c6c9d17..a546533 100644
--- a/pages/staff/time/index.vue
+++ b/pages/staff/time/index.vue
@@ -7,35 +7,49 @@ definePageMeta({
layout: "default",
})
-const { list, start, stop, submit, approve } = useStaffTime()
+const { list, start, stop, submit, approve, reject } = useStaffTime()
const auth = useAuthStore()
const router = useRouter()
+const toast = useToast()
+const { $dayjs } = useNuxtApp()
+
// MOBILE DETECTION
const platformIsNative = useCapacitor().getIsNative()
// STATE
const loading = ref(false)
-const showModal = ref(false)
-const editEntry = ref(null)
+const view = ref('list') // 'list' | 'timeline'
-// 👥 Nutzer-Filter
+// MODAL STATES
+const showEditModal = ref(false)
+const entryToEdit = ref(null)
+
+const showRejectModal = ref(false)
+const entryToReject = ref(null)
+const rejectReason = ref("")
+
+// FILTER & USER
const users = ref([])
-// WICHTIG: Standardmäßig IMMER den aktuellen User wählen (kein null mehr)
const selectedUser = ref(auth.user.id)
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
-// LIST + ACTIVE
+// DATA
const entries = ref([])
-
-// Active berechnet sich jetzt aus den geladenen Einträgen.
-// Da wir immer nur EINEN User laden, ist ein laufender Eintrag hier
-// automatisch der aktive Eintrag des angezeigten Users.
const active = computed(() => entries.value.find(e => !e.stopped_at))
-
-// Prüft, ob wir gerade das eigene Profil ansehen (für Start/Stop Buttons)
const isViewingSelf = computed(() => selectedUser.value === auth.user.id)
+// GROUPING
+const groupedEntries = computed(() => {
+ const groups = {}
+ entries.value.forEach(entry => {
+ const dateKey = $dayjs(entry.started_at).format('YYYY-MM-DD')
+ if (!groups[dateKey]) groups[dateKey] = []
+ groups[dateKey].push(entry)
+ })
+ return groups
+})
+// CONFIG
const typeLabel = {
work: "Arbeitszeit",
vacation: "Urlaub",
@@ -52,26 +66,20 @@ const typeColor = {
other: "gray"
}
-
+// ACTIONS
async function loadUsers() {
if (!canViewAll.value) return
const res = (await useNuxtApp().$api("/api/tenant/profiles")).data
users.value = res
}
-// LOAD ENTRIES (Erzwingt jetzt immer eine User-ID)
async function load() {
if (!selectedUser.value) return
-
- entries.value = await list({
- user_id: selectedUser.value // 👈 Hier wird der Filter erzwungen
- })
+ entries.value = await list({ user_id: selectedUser.value })
}
async function handleStart() {
- // Sicherheitshalber: Starten nur erlauben, wenn man sich selbst ansieht
if (!isViewingSelf.value) return
-
loading.value = true
await start("Arbeitszeit gestartet")
await load()
@@ -80,9 +88,6 @@ async function handleStart() {
async function handleStop() {
if (!active.value) return
- // Stoppen darf man ggf. auch andere (als Admin), daher hier kein isViewingSelf Check
- // oder je nach Logik anpassen. Hier erlauben wir das Stoppen des angezeigten Timers.
-
loading.value = true
await stop(active.value.id)
await load()
@@ -90,28 +95,53 @@ async function handleStop() {
}
function handleEdit(entry) {
- editEntry.value = entry
- showModal.value = true
+ entryToEdit.value = entry
+ showEditModal.value = true
}
async function handleSubmit(entry) {
+ loading.value = true
await submit(entry)
await load()
+ loading.value = false
+ toast.add({ title: 'Zeit eingereicht', color: 'green' })
}
async function handleApprove(entry) {
+ loading.value = true
await approve(entry)
await load()
+ loading.value = false
+ toast.add({ title: 'Zeit genehmigt', color: 'green' })
}
-// Watcher: Wenn der User im Dropdown gewechselt wird, neu laden
-watch(selectedUser, () => {
- load()
-})
+function openRejectModal(entry) {
+ entryToReject.value = entry
+ rejectReason.value = ""
+ showRejectModal.value = true
+}
+
+async function confirmReject() {
+ if (!entryToReject.value) return
+ loading.value = true
+ try {
+ await reject(entryToReject.value, rejectReason.value || "Vom Administrator abgelehnt")
+ toast.add({ title: 'Zeit abgelehnt', color: 'green' })
+ showRejectModal.value = false
+ await load()
+ } catch (e) {
+ toast.add({ title: 'Fehler beim Ablehnen', description: e.message, color: 'red' })
+ } finally {
+ loading.value = false
+ entryToReject.value = null
+ }
+}
+
+watch(selectedUser, () => { load() })
onMounted(async () => {
await loadUsers()
- await load() // Lädt initial den eingeloggten User
+ await load()
setPageLayout(platformIsNative ? 'mobile' : 'default')
})
@@ -120,302 +150,324 @@ onMounted(async () => {
-
-
-
-
-
-
-
-
-
-
-
+ { entryToEdit = null; showEditModal = true }" />
+
-
-
-
- Genehmigt
- Eingereicht
- Entwurf
-
-
-
- {{ typeLabel[row.type] || row.type }}
-
-
-
-
-
- {{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
-
-
- {{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
-
-
-
-
-
- läuft...
-
-
- {{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
-
-
- {{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
-
-
-
-
-
-
-
+
+
+
+ Genehmigt
+ Eingereicht
+ Abgelehnt
+ Entwurf
+
+
+ {{ typeLabel[row.type] || row.type }}
+
+
+ {{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
+ {{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
+
+
+ läuft...
+ {{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
+ {{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
+
+
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{row.vacation_reason}}
+ {{row.sick_reason}}
+ {{row.description}}
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
- {{row.vacation_reason}}
- {{row.sick_reason}}
- {{row.description}}
-
-
+
+
+
+
+
+ {{ useNuxtApp().$dayjs(date).format('dddd, DD. MMMM') }}
+
+
+ {{ group.length }} Einträge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ useNuxtApp().$dayjs(entry.started_at).format('HH:mm') }}
+ bis
+ {{ useNuxtApp().$dayjs(entry.stopped_at).format('HH:mm') }}
+ Läuft
+
+
+
{{ typeLabel[entry.type] }}
+
+
+
+
+ {{ useFormatDuration(entry.duration_minutes) }}
+
+
+ Genehmigt
+ Eingereicht
+ Abgelehnt
+ Entwurf
+
+
+
+
+
+ {{ entry.description || 'Keine Beschreibung angegeben.' }}
+
+
+ Grund: {{ entry.vacation_reason }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Keine Einträge im gewählten Zeitraum.
+
+
+
-
-
Aktive Zeit
-
+
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
Keine aktive Zeit
-
-
-
+
+
+
+
+
+
-
-
+
-
-
-
+
- {{ row.description || 'Keine Beschreibung' }}
-
-
- {{ typeLabel[row.type] }}
-
+ {{ row.description || 'Keine Beschreibung' }}
+ {{ typeLabel[row.type] }}
-
-
- {{ { approved: 'Genehmigt', submitted: 'Eingereicht', draft: 'Entwurf' }[row.state] || row.state }}
-
+
Genehmigt
+
Eingereicht
+
Abgelehnt
+
Entwurf
-
-
- Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
-
-
-
- Ende:
-
- {{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
-
- läuft...
-
-
-
- Dauer:
- {{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
-
-
-
-
-
+
Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
+
Ende: {{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
+
+
+
-
-
{ editEntry = null; showModal = true }"
- />
-
+ { entryToEdit = null; showEditModal = true }" />
+
+
+
+
+
+
+ Zeiteintrag ablehnen
+
+
+
+
+
+
+ Der Eintrag wird als "Rejected" markiert und nicht mehr zur Arbeitszeit gezählt.
+
+
+
+
+
+
+
+ Abbrechen
+ Bestätigen
+
+
+
+
\ No newline at end of file