Add workdays routes to backend and frontend; update routing and UI components for workdays tracking

This commit is contained in:
Torsten Schulz (local)
2025-10-17 21:37:47 +02:00
parent 2868d64e37
commit a58504a93e
7 changed files with 423 additions and 0 deletions

View File

@@ -70,6 +70,7 @@ const pageTitle = computed(() => {
'timefix': 'Zeitkorrekturen',
'vacation': 'Urlaub',
'sick': 'Krankheit',
'workdays': 'Arbeitstage',
'entries': 'Einträge',
'stats': 'Statistiken'
}

View File

@@ -13,6 +13,7 @@ import WeekOverview from '../views/WeekOverview.vue'
import Timefix from '../views/Timefix.vue'
import Vacation from '../views/Vacation.vue'
import Sick from '../views/Sick.vue'
import Workdays from '../views/Workdays.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -77,6 +78,12 @@ const router = createRouter({
component: Sick,
meta: { requiresAuth: true }
},
{
path: '/bookings/workdays',
name: 'workdays',
component: Workdays,
meta: { requiresAuth: true }
},
{
path: '/entries',
name: 'entries',

View File

@@ -0,0 +1,181 @@
<template>
<div class="workdays-page">
<div class="card">
<!-- Jahr-Auswahl -->
<div class="year-selector">
<input
type="number"
v-model.number="selectedYear"
@change="loadStatistics"
min="2000"
max="2100"
class="year-input"
>
</div>
<!-- Statistik-Tabelle -->
<table class="stats-table" v-if="statistics">
<tbody>
<tr>
<th>Werktage</th>
<td>{{ statistics.workdays }}</td>
</tr>
<tr>
<th>Feiertage</th>
<td>{{ statistics.holidays }}</td>
</tr>
<tr>
<th>Krankheitstage</th>
<td>{{ statistics.sickDays }} ({{ statistics.sickPercentage }} %)</td>
</tr>
<tr>
<th>Urlaubstage</th>
<td>{{ formatVacationDays(statistics.vacationDays) }}</td>
</tr>
<tr>
<th>Gearbeitete Tage</th>
<td>{{ statistics.workedDays }}</td>
</tr>
</tbody>
</table>
<div v-if="loading" class="loading">Lade Statistiken...</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/authStore'
const authStore = useAuthStore()
const selectedYear = ref(new Date().getFullYear())
const statistics = ref(null)
const loading = ref(false)
const error = ref(null)
// Lade Statistiken für das ausgewählte Jahr
async function loadStatistics() {
try {
loading.value = true
error.value = null
const response = await fetch(`http://localhost:3010/api/workdays?year=${selectedYear.value}`, {
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (!response.ok) {
throw new Error('Fehler beim Laden der Statistiken')
}
statistics.value = await response.json()
} catch (err) {
console.error('Fehler beim Laden der Statistiken:', err)
error.value = err.message
} finally {
loading.value = false
}
}
// Formatiere Urlaubstage (zeige halbe Tage korrekt)
function formatVacationDays(days) {
if (days === Math.floor(days)) {
return days.toString()
}
return days.toFixed(1)
}
// Initiales Laden
onMounted(() => {
loadStatistics()
})
</script>
<style scoped>
.workdays-page {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.year-selector {
margin-bottom: 24px;
display: flex;
justify-content: center;
}
.year-input {
width: 120px;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
text-align: center;
font-family: inherit;
}
.year-input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.stats-table {
width: 100%;
border-collapse: collapse;
}
.stats-table tr {
border-bottom: 1px solid #eee;
}
.stats-table tr:last-child {
border-bottom: none;
}
.stats-table th {
text-align: left;
padding: 12px 16px;
font-weight: 600;
color: #555;
font-size: 14px;
width: 70%;
white-space: nowrap;
}
.stats-table td {
text-align: right;
padding: 12px 16px;
font-size: 15px;
font-weight: 500;
color: #333;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
.error {
text-align: center;
padding: 20px;
color: #f44336;
background: #ffebee;
border-radius: 4px;
margin-top: 20px;
}
</style>