Initial commit: TimeClock v3 - Node.js/Vue.js Zeiterfassung
Features: - Backend: Node.js/Express mit MySQL/MariaDB - Frontend: Vue.js 3 mit Composition API - UTC-Zeithandling für korrekte Zeiterfassung - Timewish-basierte Überstundenberechnung - Wochenübersicht mit Urlaubs-/Krankheits-/Feiertagshandling - Bereinigtes Arbeitsende (Generell/Woche) - Überstunden-Offset für historische Daten - Fixed Layout mit scrollbarem Content - Kompakte UI mit grünem Theme
This commit is contained in:
338
frontend/src/stores/authStore.js
Normal file
338
frontend/src/stores/authStore.js
Normal file
@@ -0,0 +1,338 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const API_URL = 'http://localhost:3010/api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
/**
|
||||
* Token in localStorage speichern
|
||||
*/
|
||||
const saveToken = (newToken) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('timeclock_token', newToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Token aus localStorage laden
|
||||
*/
|
||||
const loadToken = () => {
|
||||
const savedToken = localStorage.getItem('timeclock_token')
|
||||
if (savedToken) {
|
||||
token.value = savedToken
|
||||
return savedToken
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Token und Benutzerdaten löschen
|
||||
*/
|
||||
const clearAuth = () => {
|
||||
token.value = null
|
||||
user.value = null
|
||||
isAuthenticated.value = false
|
||||
localStorage.removeItem('timeclock_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrierung
|
||||
*/
|
||||
const register = async (userData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Registrierung fehlgeschlagen')
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Registrierungsfehler:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*/
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credentials)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Login fehlgeschlagen')
|
||||
}
|
||||
|
||||
// Token speichern
|
||||
saveToken(data.token)
|
||||
|
||||
// Benutzerdaten setzen
|
||||
user.value = data.user
|
||||
isAuthenticated.value = true
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Login-Fehler:', error)
|
||||
clearAuth()
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
if (token.value) {
|
||||
await fetch(`${API_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout-Fehler:', error)
|
||||
} finally {
|
||||
clearAuth()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuellen Benutzer laden (für Session-Wiederherstellung)
|
||||
*/
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const savedToken = loadToken()
|
||||
|
||||
if (!savedToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${savedToken}`
|
||||
}
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
// Token ungültig - ausloggen
|
||||
clearAuth()
|
||||
return false
|
||||
}
|
||||
|
||||
// Session wiederherstellen
|
||||
user.value = data.user
|
||||
isAuthenticated.value = true
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Benutzers:', error)
|
||||
clearAuth()
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token validieren
|
||||
*/
|
||||
const validateToken = async () => {
|
||||
try {
|
||||
const savedToken = loadToken()
|
||||
|
||||
if (!savedToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/validate`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${savedToken}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
clearAuth()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Token-Validierungsfehler:', error)
|
||||
clearAuth()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort-Reset anfordern
|
||||
*/
|
||||
const requestPasswordReset = async (email) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/request-reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Anfrage fehlgeschlagen')
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Passwort-Reset-Anfrage-Fehler:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort zurücksetzen
|
||||
*/
|
||||
const resetPassword = async (resetToken, password) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: resetToken,
|
||||
password
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Passwort-Reset fehlgeschlagen')
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Passwort-Reset-Fehler:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort ändern (eingeloggter Benutzer)
|
||||
*/
|
||||
const changePassword = async (oldPassword, newPassword) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
oldPassword,
|
||||
newPassword
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Passwort-Änderung fehlgeschlagen')
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Passwort-Änderungs-Fehler:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP-Header mit Authorization zurückgeben
|
||||
*/
|
||||
const getAuthHeaders = () => {
|
||||
const savedToken = token.value || loadToken()
|
||||
|
||||
if (savedToken) {
|
||||
return {
|
||||
'Authorization': `Bearer ${savedToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
user,
|
||||
token,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
|
||||
// Actions
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
fetchCurrentUser,
|
||||
validateToken,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
changePassword,
|
||||
getAuthHeaders,
|
||||
loadToken,
|
||||
saveToken,
|
||||
clearAuth
|
||||
}
|
||||
})
|
||||
|
||||
125
frontend/src/stores/timeStore.js
Normal file
125
frontend/src/stores/timeStore.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
const API_URL = 'http://localhost:3010/api'
|
||||
|
||||
export const useTimeStore = defineStore('time', () => {
|
||||
const entries = ref([])
|
||||
const currentEntry = ref(null)
|
||||
|
||||
/**
|
||||
* API-Request mit Auth-Header
|
||||
*/
|
||||
const fetchWithAuth = async (url, options = {}) => {
|
||||
const authStore = useAuthStore()
|
||||
const headers = authStore.getAuthHeaders()
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
// Bei 401 Unauthorized -> Logout
|
||||
if (response.status === 401) {
|
||||
authStore.clearAuth()
|
||||
window.location.href = '/login'
|
||||
throw new Error('Session abgelaufen')
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
const fetchEntries = async () => {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/time-entries`)
|
||||
const data = await response.json()
|
||||
entries.value = data
|
||||
|
||||
// Finde laufenden Eintrag
|
||||
currentEntry.value = data.find(entry => entry.isRunning) || null
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Einträge:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const startTimer = async (entryData) => {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/time-entries`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(entryData)
|
||||
})
|
||||
|
||||
const newEntry = await response.json()
|
||||
currentEntry.value = newEntry
|
||||
entries.value.unshift(newEntry)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Starten des Timers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const stopTimer = async () => {
|
||||
if (!currentEntry.value) return
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/time-entries/${currentEntry.value.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
endTime: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
const updatedEntry = await response.json()
|
||||
|
||||
// Aktualisiere Eintrag in Liste
|
||||
const index = entries.value.findIndex(e => e.id === updatedEntry.id)
|
||||
if (index !== -1) {
|
||||
entries.value[index] = updatedEntry
|
||||
}
|
||||
|
||||
currentEntry.value = null
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Stoppen des Timers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEntry = async (id) => {
|
||||
try {
|
||||
await fetchWithAuth(`${API_URL}/time-entries/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
entries.value = entries.value.filter(entry => entry.id !== id)
|
||||
|
||||
if (currentEntry.value && currentEntry.value.id === id) {
|
||||
currentEntry.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Eintrags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/time-entries/stats/summary`)
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Statistiken:', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entries,
|
||||
currentEntry,
|
||||
fetchEntries,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
deleteEntry,
|
||||
fetchStats
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user