582 lines
18 KiB
Vue
582 lines
18 KiB
Vue
<template>
|
|
<div class="status-box" v-if="isReady">
|
|
<div class="status-actions">
|
|
<!-- Linker Button -->
|
|
<button
|
|
v-if="leftButton"
|
|
class="btn btn-small"
|
|
:class="leftButton.class"
|
|
@click="handleAction(leftButton.action)"
|
|
:disabled="loading"
|
|
>
|
|
{{ leftButton.label }}
|
|
</button>
|
|
|
|
<!-- Rechter Button -->
|
|
<button
|
|
v-if="rightButton"
|
|
class="btn btn-small btn-secondary"
|
|
@click="handleAction(rightButton.action)"
|
|
:disabled="loading"
|
|
>
|
|
{{ rightButton.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="status-grid">
|
|
<div class="row" v-for="(value, key) in displayRows" :key="key" :class="{ 'heading-row': value === null }">
|
|
<span class="label">{{ key }}{{ value === null ? '' : ':' }}</span>
|
|
<span class="value" v-if="value !== null">{{ value || '—' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="status-box loading" v-else>
|
|
Lädt…
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
|
|
import { useTimeStore } from '../stores/timeStore'
|
|
import { useAuthStore } from '../stores/authStore'
|
|
import { API_BASE_URL } from '@/config/api'
|
|
|
|
const API_URL = API_BASE_URL
|
|
const timeStore = useTimeStore()
|
|
const authStore = useAuthStore()
|
|
const stats = ref({})
|
|
const currentState = ref(null) // 'null', 'start work', 'start pause', 'stop pause'
|
|
const isReady = ref(false)
|
|
const loading = ref(false)
|
|
const workStartTime = ref(null) // Timestamp wann die Arbeit begonnen hat
|
|
const lastPauseStartTime = ref(null) // Timestamp der aktuellen Pause
|
|
const pauseDurations = ref([]) // Array von Pausen-Dauern in Millisekunden
|
|
const currentlyWorkedTime = ref('—') // Berechnete Arbeitszeit
|
|
const openTime = ref('—') // Berechnete offene Zeit
|
|
const regularEndTime = ref('—') // Berechnetes normales Arbeitsende
|
|
const serverWorkedTime = ref(null) // Vom Server berechnete Zeit
|
|
const serverOpenTime = ref(null) // Vom Server berechnete offene Zeit
|
|
const serverTimestamp = ref(null) // Wann die Server-Daten berechnet wurden
|
|
const missingBreakMinutes = ref(0) // Fehlende Pausenminuten
|
|
let dataFetchInterval = null // Daten vom Server laden
|
|
let displayUpdateInterval = null // Anzeige aktualisieren
|
|
|
|
const fetchStats = async () => {
|
|
const data = await timeStore.fetchStats()
|
|
stats.value = data || {}
|
|
|
|
// Hole vom Server berechnete Arbeitszeit, offene Zeit und Timestamp
|
|
if (data?.currentlyWorked) {
|
|
serverWorkedTime.value = data.currentlyWorked
|
|
} else {
|
|
serverWorkedTime.value = null
|
|
}
|
|
|
|
if (data?.open) {
|
|
serverOpenTime.value = data.open
|
|
} else {
|
|
serverOpenTime.value = null
|
|
}
|
|
|
|
if (data?.timestamp) {
|
|
serverTimestamp.value = new Date(data.timestamp).getTime()
|
|
}
|
|
|
|
// Hole fehlende Pausenminuten
|
|
if (data?.missingBreakMinutes !== undefined) {
|
|
missingBreakMinutes.value = data.missingBreakMinutes
|
|
} else {
|
|
missingBreakMinutes.value = 0
|
|
}
|
|
|
|
isReady.value = true
|
|
}
|
|
|
|
const fetchCurrentState = async () => {
|
|
try {
|
|
const response = await fetch(`${API_URL}/time-entries/current-state`, {
|
|
headers: authStore.getAuthHeaders()
|
|
})
|
|
|
|
if (response.ok) {
|
|
const result = await response.json()
|
|
currentState.value = result.state || null
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden des aktuellen Zustands:', error)
|
|
}
|
|
}
|
|
|
|
// Lade die aktuellen Worklog-Daten (nur einmal pro Minute)
|
|
const fetchWorklogData = async () => {
|
|
try {
|
|
const response = await fetch(`${API_URL}/time-entries/running`, {
|
|
headers: authStore.getAuthHeaders()
|
|
})
|
|
|
|
if (response.ok) {
|
|
const result = await response.json()
|
|
|
|
// Das Backend gibt direkt das Entry-Objekt zurück, nicht { entry: ... }
|
|
if (result && result.startTime && (currentState.value === 'start work' || currentState.value === 'stop pause')) {
|
|
// Arbeit läuft
|
|
workStartTime.value = new Date(result.startTime).getTime()
|
|
pauseDurations.value = result.pauses || []
|
|
lastPauseStartTime.value = null
|
|
} else if (result && result.startTime && currentState.value === 'start pause') {
|
|
// In Pause
|
|
workStartTime.value = new Date(result.startTime).getTime()
|
|
pauseDurations.value = result.pauses || []
|
|
// Hole letzten Pause-Start
|
|
if (result.currentPauseStart) {
|
|
lastPauseStartTime.value = new Date(result.currentPauseStart).getTime()
|
|
}
|
|
} else {
|
|
// Nicht am Arbeiten
|
|
workStartTime.value = null
|
|
lastPauseStartTime.value = null
|
|
pauseDurations.value = []
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Worklog-Daten:', error)
|
|
}
|
|
}
|
|
|
|
// Berechne die aktuell gearbeitete Zeit (2x pro Sekunde)
|
|
const updateCurrentlyWorkedTime = () => {
|
|
// Wenn nicht am Arbeiten
|
|
if (currentState.value === null || currentState.value === 'stop work') {
|
|
currentlyWorkedTime.value = '—'
|
|
return
|
|
}
|
|
|
|
// Verwende Server-Werte nur, wenn NICHT gearbeitet wird (oder kein laufender Start vorhanden)
|
|
const shouldUseServerSummary = !workStartTime.value
|
|
|
|
if (shouldUseServerSummary && serverWorkedTime.value && serverTimestamp.value) {
|
|
// Parse Server-Zeit (HH:MM:SS)
|
|
const parts = serverWorkedTime.value.split(':')
|
|
const serverSeconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2])
|
|
|
|
// Berechne vergangene Zeit seit Server-Timestamp
|
|
const now = Date.now()
|
|
const elapsedMs = now - serverTimestamp.value
|
|
let elapsedSeconds = Math.floor(elapsedMs / 1000)
|
|
|
|
// Wenn in Pause, zähle die Zeit nicht hoch
|
|
if (currentState.value === 'start pause') {
|
|
elapsedSeconds = 0
|
|
}
|
|
|
|
// Addiere vergangene Zeit
|
|
const totalSeconds = serverSeconds + elapsedSeconds
|
|
const hours = Math.floor(totalSeconds / 3600)
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
const seconds = totalSeconds % 60
|
|
|
|
currentlyWorkedTime.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
} else {
|
|
// Fallback/Standard: Aus Startzeit berechnen (bevorzugt bei laufender Arbeit)
|
|
if (!workStartTime.value) {
|
|
currentlyWorkedTime.value = '—'
|
|
return
|
|
}
|
|
|
|
const now = Date.now()
|
|
let totalWorkedMs = now - workStartTime.value
|
|
|
|
// Ziehe abgeschlossene Pausen ab
|
|
const totalPauseMs = pauseDurations.value.reduce((sum, duration) => sum + duration, 0)
|
|
totalWorkedMs -= totalPauseMs
|
|
|
|
// Wenn aktuell in Pause, ziehe die laufende Pause ab
|
|
if (currentState.value === 'start pause' && lastPauseStartTime.value) {
|
|
const currentPauseMs = now - lastPauseStartTime.value
|
|
totalWorkedMs -= currentPauseMs
|
|
}
|
|
|
|
// Formatiere als HH:MM:SS
|
|
const totalSeconds = Math.floor(totalWorkedMs / 1000)
|
|
const hours = Math.floor(totalSeconds / 3600)
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
const seconds = totalSeconds % 60
|
|
|
|
currentlyWorkedTime.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
}
|
|
}
|
|
|
|
// Berechne die offene Zeit (2x pro Sekunde)
|
|
const updateOpenTime = () => {
|
|
// Wenn nicht am Arbeiten
|
|
if (currentState.value === null || currentState.value === 'stop work') {
|
|
openTime.value = '—'
|
|
regularEndTime.value = '—'
|
|
return
|
|
}
|
|
|
|
// Wenn wir einen Server-Wert haben, nutze diesen als Basis
|
|
if (serverOpenTime.value && serverTimestamp.value) {
|
|
// Parse Server-Zeit (HH:MM:SS)
|
|
const parts = serverOpenTime.value.split(':')
|
|
const serverSeconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2])
|
|
|
|
// Berechne vergangene Zeit seit Server-Timestamp
|
|
const now = Date.now()
|
|
const elapsedMs = now - serverTimestamp.value
|
|
let elapsedSeconds = Math.floor(elapsedMs / 1000)
|
|
|
|
// Wenn in Pause, zähle die Zeit nicht runter (bleibt gleich)
|
|
if (currentState.value === 'start pause') {
|
|
elapsedSeconds = 0
|
|
}
|
|
|
|
// Subtrahiere vergangene Zeit (Offen wird weniger)
|
|
const remainingSeconds = serverSeconds - elapsedSeconds
|
|
|
|
if (remainingSeconds > 0) {
|
|
const hours = Math.floor(remainingSeconds / 3600)
|
|
const minutes = Math.floor((remainingSeconds % 3600) / 60)
|
|
const seconds = remainingSeconds % 60
|
|
|
|
openTime.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
|
|
// Berechne "Normales Arbeitsende" = Jetzt + Offen (Pausen sind bereits in Offen enthalten)
|
|
const totalRemainingSeconds = remainingSeconds
|
|
const endTimestamp = now + (totalRemainingSeconds * 1000)
|
|
const endDate = new Date(endTimestamp)
|
|
const endHours = endDate.getHours()
|
|
const endMinutes = endDate.getMinutes()
|
|
const endSeconds = endDate.getSeconds()
|
|
|
|
// Ausgabe ohne zusätzlichen Pausen-Hinweis; Zeit stammt bereits inkl. Pausen aus Backend
|
|
regularEndTime.value = `${endHours.toString().padStart(2, '0')}:${endMinutes.toString().padStart(2, '0')}:${endSeconds.toString().padStart(2, '0')} Uhr`
|
|
|
|
// Override: Berechne Normales Arbeitsende aus Server "open" Zeit
|
|
// Nur überschreiben wenn wir Server-Daten haben
|
|
if (serverOpenTime.value && serverTimestamp.value) {
|
|
// Parse Server-Offen-Zeit
|
|
const openParts = serverOpenTime.value.split(':')
|
|
const openSeconds = parseInt(openParts[0]) * 3600 + parseInt(openParts[1]) * 60 + parseInt(openParts[2] || 0)
|
|
|
|
// Berechne vergangene Zeit seit Server-Timestamp
|
|
const now = Date.now()
|
|
const elapsedMs = now - serverTimestamp.value
|
|
let elapsedSeconds = Math.floor(elapsedMs / 1000)
|
|
|
|
// Wenn in Pause, zähle die Zeit nicht
|
|
if (currentState.value === 'start pause') {
|
|
elapsedSeconds = 0
|
|
}
|
|
|
|
const remainingSeconds = openSeconds - elapsedSeconds
|
|
|
|
if (remainingSeconds > 0) {
|
|
const endTs = now + (remainingSeconds * 1000)
|
|
const endDt = new Date(endTs)
|
|
const eh = endDt.getHours()
|
|
const em = endDt.getMinutes()
|
|
const es = endDt.getSeconds()
|
|
regularEndTime.value = `${eh.toString().padStart(2, '0')}:${em.toString().padStart(2, '0')}:${es.toString().padStart(2, '0')} Uhr`
|
|
} else {
|
|
regularEndTime.value = 'Erreicht'
|
|
}
|
|
}
|
|
} else {
|
|
openTime.value = 'Arbeitsende erreicht'
|
|
|
|
// Auch wenn Arbeitsende erreicht ist, können noch Pausen fehlen
|
|
if (missingBreakMinutes.value > 0) {
|
|
regularEndTime.value = `Erreicht (+${missingBreakMinutes.value}min Pause)`
|
|
} else {
|
|
regularEndTime.value = 'Erreicht'
|
|
}
|
|
}
|
|
} else {
|
|
openTime.value = '—'
|
|
regularEndTime.value = '—'
|
|
}
|
|
}
|
|
|
|
const handleAction = async (action) => {
|
|
try {
|
|
loading.value = true
|
|
|
|
const response = await fetch(`${API_URL}/time-entries/clock`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...authStore.getAuthHeaders(),
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ action })
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
alert(error.error || 'Fehler beim Stempeln')
|
|
return
|
|
}
|
|
|
|
// Aktualisiere Status und Worklog-Daten sofort
|
|
await fetchCurrentState()
|
|
await fetchWorklogData()
|
|
await fetchStats()
|
|
|
|
// Event auslösen für andere Komponenten (z.B. WeekOverview)
|
|
window.dispatchEvent(new CustomEvent('worklog-updated'))
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Stempeln:', error)
|
|
alert('Fehler beim Stempeln')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// Button-Konfiguration basierend auf dem aktuellen Zustand
|
|
const leftButton = computed(() => {
|
|
switch (currentState.value) {
|
|
case null:
|
|
case 'stop work':
|
|
return { label: 'Arbeit beginnen', action: 'start work', class: 'btn-success' }
|
|
case 'start work':
|
|
case 'stop pause':
|
|
return { label: 'Arbeit beenden', action: 'stop work', class: 'btn-danger' }
|
|
case 'start pause':
|
|
return null // Nicht sichtbar
|
|
default:
|
|
return null
|
|
}
|
|
})
|
|
|
|
const rightButton = computed(() => {
|
|
switch (currentState.value) {
|
|
case null:
|
|
case 'stop work':
|
|
return null // Nicht sichtbar
|
|
case 'start work':
|
|
case 'stop pause':
|
|
return { label: 'Pause beginnen', action: 'start pause' }
|
|
case 'start pause':
|
|
return { label: 'Pause beenden', action: 'stop pause' }
|
|
default:
|
|
return null
|
|
}
|
|
})
|
|
|
|
// Event-Handler für Login
|
|
const handleLoginCompleted = async () => {
|
|
await fetchCurrentState()
|
|
await fetchWorklogData()
|
|
await fetchStats()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// Initiales Laden
|
|
await fetchCurrentState()
|
|
await fetchWorklogData()
|
|
await fetchStats()
|
|
|
|
// Server-Daten alle 60 Sekunden neu laden
|
|
dataFetchInterval = setInterval(async () => {
|
|
await fetchCurrentState()
|
|
await fetchWorklogData()
|
|
await fetchStats()
|
|
}, 60000)
|
|
|
|
// Anzeige 2x pro Sekunde aktualisieren (nur Berechnung, keine Server-Requests)
|
|
displayUpdateInterval = setInterval(() => {
|
|
updateCurrentlyWorkedTime()
|
|
updateOpenTime()
|
|
}, 500)
|
|
|
|
// Event-Listener für Login
|
|
window.addEventListener('login-completed', handleLoginCompleted)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (dataFetchInterval) clearInterval(dataFetchInterval)
|
|
if (displayUpdateInterval) clearInterval(displayUpdateInterval)
|
|
window.removeEventListener('login-completed', handleLoginCompleted)
|
|
})
|
|
|
|
const displayRows = computed(() => {
|
|
// Verwende Server-Wert für "Derzeit gearbeitet" wenn verfügbar (zeigt gesamte Tagesarbeitszeit)
|
|
const workedDisplay = (stats.value?.currentlyWorked && stats.value.currentlyWorked !== '—')
|
|
? stats.value.currentlyWorked + ' h'
|
|
: (currentlyWorkedTime.value === '—' ? currentlyWorkedTime.value : currentlyWorkedTime.value + ' h');
|
|
|
|
const rows = {
|
|
'Derzeit gearbeitet': workedDisplay,
|
|
'Offen': openTime.value === '—' || openTime.value.includes('erreicht') ? openTime.value : openTime.value + ' h',
|
|
'Normales Arbeitsende': regularEndTime.value // Verwende berechneten Wert (hat bereits "Uhr")
|
|
}
|
|
|
|
// Füge andere Stats hinzu
|
|
const map = [
|
|
['overtime', 'overtime'], // Label wird dynamisch gesetzt
|
|
['totalOvertime', 'totalOvertime'], // Label wird dynamisch gesetzt
|
|
['Wochenarbeitszeit', 'weekWorktime'],
|
|
['Arbeitsfreie Stunden', 'nonWorkingHours'],
|
|
['Offen für Woche', 'openForWeek'],
|
|
['Bereinigtes Arbeitsende (heute)', null], // Nur Überschrift, kein Wert
|
|
['- Generell', 'adjustedEndTodayGeneral'],
|
|
['- Woche', 'adjustedEndTodayWeek']
|
|
]
|
|
|
|
for (const [label, key] of map) {
|
|
// Spezialbehandlung für Überschrift ohne Wert
|
|
if (key === null) {
|
|
rows[label] = null // Überschrift ohne Wert (wird im Template speziell behandelt)
|
|
continue
|
|
}
|
|
|
|
const val = stats.value?.[key]
|
|
|
|
if (val !== undefined && val !== null && val !== '') {
|
|
// Spezialbehandlung für Überstunden/Fehlzeit Labels
|
|
if (key === 'overtime') {
|
|
const isNegative = val.startsWith('-');
|
|
const displayLabel = isNegative ? 'Fehlzeit (Woche)' : 'Überstunden (Woche)';
|
|
const displayValue = isNegative ? val.substring(1) + ' h' : val + ' h'; // Entferne Minus-Zeichen, füge " h" hinzu
|
|
rows[displayLabel] = displayValue;
|
|
} else if (key === 'totalOvertime') {
|
|
const isNegative = val.startsWith('-');
|
|
const displayLabel = isNegative ? 'Fehlzeit (Gesamt)' : 'Überstunden (Gesamt)';
|
|
const displayValue = isNegative ? val.substring(1) + ' h' : val + ' h'; // Entferne Minus-Zeichen, füge " h" hinzu
|
|
rows[displayLabel] = displayValue;
|
|
} else if (key === 'adjustedEndTodayGeneral' || key === 'adjustedEndTodayWeek') {
|
|
// Füge " Uhr" zu Uhrzeiten hinzu (außer bei "Arbeitsende erreicht")
|
|
rows[label] = val.includes('erreicht') ? val : val + ' Uhr';
|
|
} else if (key === 'weekWorktime' || key === 'openForWeek' || key === 'nonWorkingHours') {
|
|
// Füge " h" zu Zeiten hinzu (außer bei "—")
|
|
rows[label] = val === '—' ? val : val + ' h';
|
|
} else {
|
|
rows[label] = val;
|
|
}
|
|
} else {
|
|
rows[label] = '—'
|
|
}
|
|
}
|
|
|
|
return rows
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.status-box {
|
|
background: #ffffff;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
padding: 8px 10px;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
|
|
position: fixed;
|
|
top: 58px;
|
|
right: 24px;
|
|
max-width: 560px;
|
|
min-width: 280px;
|
|
width: auto;
|
|
height: auto;
|
|
box-sizing: border-box;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.status-box.loading {
|
|
min-width: 120px;
|
|
text-align: center;
|
|
}
|
|
|
|
.status-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.btn {
|
|
border: 1px solid #d0d0d0;
|
|
background: #f8f8f8;
|
|
color: #333;
|
|
border-radius: 4px;
|
|
padding: 6px 12px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
flex: 1;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
background: #e8e8e8;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-success {
|
|
background: #d4edda;
|
|
border-color: #c3e6cb;
|
|
color: #155724;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-success:hover:not(:disabled) {
|
|
background: #c3e6cb;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #f8d7da;
|
|
border-color: #f5c6cb;
|
|
color: #721c24;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-danger:hover:not(:disabled) {
|
|
background: #f5c6cb;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #e7f1ff;
|
|
border-color: #b8daff;
|
|
color: #004085;
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: #b8daff;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
}
|
|
|
|
.status-grid {
|
|
font-size: 12px;
|
|
column-gap: 16px;
|
|
}
|
|
|
|
.row {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
align-items: baseline;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.row.heading-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.row.heading-row .label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.label {
|
|
color: #555;
|
|
}
|
|
|
|
.value {
|
|
color: #000;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|