Add workdays routes to backend and frontend; update routing and UI components for workdays tracking
This commit is contained in:
@@ -70,6 +70,7 @@ const pageTitle = computed(() => {
|
||||
'timefix': 'Zeitkorrekturen',
|
||||
'vacation': 'Urlaub',
|
||||
'sick': 'Krankheit',
|
||||
'workdays': 'Arbeitstage',
|
||||
'entries': 'Einträge',
|
||||
'stats': 'Statistiken'
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
181
frontend/src/views/Workdays.vue
Normal file
181
frontend/src/views/Workdays.vue
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user