Add timefix and vacation routes to backend; update frontend for new routes and page titles

This commit is contained in:
Torsten Schulz (local)
2025-10-17 15:54:30 +02:00
parent e95bb4cb76
commit b65a13d815
16 changed files with 1913 additions and 13 deletions

View File

@@ -6,17 +6,20 @@
<h1 class="brand">
<RouterLink to="/">Stechuhr</RouterLink>
</h1>
<div class="nav-collapse">
<SideMenu />
<ul class="pull-right navbar-nav nav">
<li class="user-info">
<span class="user-name">{{ authStore.user?.full_name }}</span>
</li>
<li>
<button @click="handleLogout" class="btn-logout">Abmelden</button>
</li>
</ul>
<div class="nav-title-menu">
<h2 class="page-title" v-if="pageTitle">{{ pageTitle }}</h2>
<div class="nav-collapse">
<SideMenu />
</div>
</div>
<ul class="pull-right navbar-nav nav">
<li class="user-info">
<span class="user-name">{{ authStore.user?.full_name }}</span>
</li>
<li>
<button @click="handleLogout" class="btn-logout">Abmelden</button>
</li>
</ul>
</div>
</div>
</div>
@@ -44,19 +47,33 @@
</template>
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { useAuthStore } from './stores/authStore'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import StatusBox from './components/StatusBox.vue'
import SideMenu from './components/SideMenu.vue'
const authStore = useAuthStore()
const router = useRouter()
const route = useRoute()
const handleLogout = async () => {
await authStore.logout()
router.push('/login')
}
// Seitentitel basierend auf der aktuellen Route
const pageTitle = computed(() => {
const titles = {
'week-overview': 'Wochenübersicht',
'timefix': 'Zeitkorrekturen',
'vacation': 'Urlaub',
'entries': 'Einträge',
'stats': 'Statistiken'
}
return titles[route.name] || ''
})
</script>
<style scoped>
@@ -77,6 +94,21 @@ const handleLogout = async () => {
border-bottom: 1px solid #e0ffe0;
}
.nav-title-menu {
display: flex;
flex-direction: column;
flex: 1;
align-items: flex-start;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin: 0;
padding: 0 15px;
}
.container {
max-width: 100%;
margin: 0 auto;
@@ -84,6 +116,7 @@ const handleLogout = async () => {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
}
.brand {

View File

@@ -0,0 +1,216 @@
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="show" class="modal-overlay" @click.self="onCancel">
<div class="modal-container">
<div class="modal-header">
<h3 class="modal-title">{{ title }}</h3>
<button class="modal-close" @click="onCancel" aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<p>{{ message }}</p>
</div>
<div class="modal-footer">
<button
v-if="type === 'confirm'"
class="btn btn-secondary"
@click="onCancel"
>
{{ cancelText }}
</button>
<button
class="btn"
:class="type === 'confirm' ? 'btn-danger' : 'btn-primary'"
@click="onConfirm"
>
{{ confirmText }}
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
defineProps({
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: 'Hinweis'
},
message: {
type: String,
required: true
},
type: {
type: String,
default: 'alert', // 'alert' oder 'confirm'
validator: (value) => ['alert', 'confirm'].includes(value)
},
confirmText: {
type: String,
default: 'OK'
},
cancelText: {
type: String,
default: 'Abbrechen'
}
})
const emit = defineEmits(['confirm', 'cancel'])
const onConfirm = () => {
emit('confirm')
}
const onCancel = () => {
emit('cancel')
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: linear-gradient(135deg, #f0ffec, #e8f5e0);
border-bottom: 2px solid #d0f0c0;
border-radius: 8px 8px 0 0;
}
.modal-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #2c5e1a;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
line-height: 1;
color: #999;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.modal-close:hover {
background: #f0f0f0;
color: #666;
}
.modal-body {
padding: 24px;
color: #555;
line-height: 1.6;
}
.modal-body p {
margin: 0;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
}
.btn-secondary {
background: #ecf0f1;
color: #555;
}
.btn-secondary:hover {
background: #d5dbdd;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
/* Transition */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-active .modal-container,
.modal-leave-active .modal-container {
transition: transform 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
transform: scale(0.9);
}
</style>

View File

@@ -0,0 +1,67 @@
import { ref } from 'vue'
export function useModal() {
const showModal = ref(false)
const modalConfig = ref({
title: 'Hinweis',
message: '',
type: 'alert',
confirmText: 'OK',
cancelText: 'Abbrechen'
})
const resolvePromise = ref(null)
const alert = (message, title = 'Hinweis') => {
return new Promise((resolve) => {
modalConfig.value = {
title,
message,
type: 'alert',
confirmText: 'OK',
cancelText: 'Abbrechen'
}
showModal.value = true
resolvePromise.value = resolve
})
}
const confirm = (message, title = 'Bestätigung') => {
return new Promise((resolve) => {
modalConfig.value = {
title,
message,
type: 'confirm',
confirmText: 'Ja',
cancelText: 'Abbrechen'
}
showModal.value = true
resolvePromise.value = resolve
})
}
const onConfirm = () => {
showModal.value = false
if (resolvePromise.value) {
resolvePromise.value(true)
resolvePromise.value = null
}
}
const onCancel = () => {
showModal.value = false
if (resolvePromise.value) {
resolvePromise.value(false)
resolvePromise.value = null
}
}
return {
showModal,
modalConfig,
alert,
confirm,
onConfirm,
onCancel
}
}

View File

@@ -10,6 +10,8 @@ import PasswordForgot from '../views/PasswordForgot.vue'
import PasswordReset from '../views/PasswordReset.vue'
import OAuthCallback from '../views/OAuthCallback.vue'
import WeekOverview from '../views/WeekOverview.vue'
import Timefix from '../views/Timefix.vue'
import Vacation from '../views/Vacation.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -56,6 +58,18 @@ const router = createRouter({
component: WeekOverview,
meta: { requiresAuth: true }
},
{
path: '/bookings/timefix',
name: 'timefix',
component: Timefix,
meta: { requiresAuth: true }
},
{
path: '/bookings/vacation',
name: 'vacation',
component: Vacation,
meta: { requiresAuth: true }
},
{
path: '/entries',
name: 'entries',

View File

@@ -0,0 +1,513 @@
<template>
<div class="timefix-container">
<Modal
:show="showModal"
:title="modalConfig.title"
:message="modalConfig.message"
:type="modalConfig.type"
:confirmText="modalConfig.confirmText"
:cancelText="modalConfig.cancelText"
@confirm="onConfirm"
@cancel="onCancel"
/>
<!-- Formular für neue Zeitkorrektur -->
<div class="card form-card">
<h2>Neue Zeitkorrektur</h2>
<form @submit.prevent="createTimefix">
<!-- Erste Zeile: Original-Eintrag -->
<div class="form-row">
<div class="form-group">
<label for="originalDate">Datum des Original-Eintrags</label>
<input
type="date"
id="originalDate"
v-model="form.originalDate"
@change="loadWorklogEntries"
required
/>
</div>
<div class="form-group">
<label for="worklogEntry">Eintrag</label>
<select
id="worklogEntry"
v-model="form.worklogId"
@change="onEntrySelected"
required
:disabled="!availableEntries.length"
>
<option value="">{{ availableEntries.length === 0 ? '-- Keine Einträge für dieses Datum --' : '-- Bitte wählen --' }}</option>
<option
v-for="entry in availableEntries"
:key="entry.id"
:value="entry.id"
>
{{ entry.time }} - {{ formatAction(entry.action) }}
</option>
</select>
</div>
</div>
<!-- Zweite Zeile: Neue Werte -->
<div class="form-row">
<div class="form-group">
<label for="newDate">Neues Datum</label>
<input
type="date"
id="newDate"
v-model="form.newDate"
required
/>
</div>
<div class="form-group">
<label for="newTime">Neue Uhrzeit</label>
<input
type="time"
id="newTime"
v-model="form.newTime"
required
/>
</div>
<div class="form-group">
<label for="newAction">Neuer Eintrags-Typ</label>
<select
id="newAction"
v-model="form.newAction"
required
>
<option value="">-- Bitte wählen --</option>
<option value="start work">Arbeit beginnen</option>
<option value="stop work">Arbeit beenden</option>
<option value="start pause">Pause beginnen</option>
<option value="stop pause">Pause beenden</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
{{ loading ? 'Wird gespeichert...' : 'Korrektur speichern' }}
</button>
<button type="button" class="btn btn-secondary" @click="resetForm">
Zurücksetzen
</button>
</div>
</form>
</div>
<!-- Tabelle der heutigen Zeitkorrekturen -->
<div class="card table-card">
<h2>Zeitkorrekturen von heute</h2>
<div v-if="loading && timefixes.length === 0" class="loading">
Lade Zeitkorrekturen...
</div>
<div v-else-if="timefixes.length === 0" class="empty-state">
Keine Zeitkorrekturen für heute vorhanden.
</div>
<table v-else class="timefix-table">
<thead>
<tr>
<th colspan="3">Neue Werte</th>
<th colspan="3">Originalwerte</th>
<th>Aktionen</th>
</tr>
<tr>
<th>Zeit</th>
<th>Datum</th>
<th>Aktion</th>
<th>Zeit</th>
<th>Datum</th>
<th>Aktion</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="timefix in timefixes" :key="timefix.id">
<td>{{ timefix.newTime }}</td>
<td>{{ formatDate(timefix.newDate) }}</td>
<td>{{ formatAction(timefix.newAction) }}</td>
<td>{{ timefix.originalTime }}</td>
<td>{{ formatDate(timefix.originalDate) }}</td>
<td>{{ formatAction(timefix.originalAction) }}</td>
<td class="actions">
<button
@click="deleteTimefix(timefix.id)"
class="btn btn-danger btn-small"
title="Korrektur rückgängig machen"
>
Löschen
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/authStore'
import { useModal } from '../composables/useModal'
import Modal from '../components/Modal.vue'
const authStore = useAuthStore()
const timefixes = ref([])
const availableEntries = ref([])
const loading = ref(false)
const { showModal, modalConfig, alert, confirm, onConfirm, onCancel } = useModal()
const form = ref({
originalDate: new Date().toISOString().split('T')[0],
worklogId: '',
newDate: new Date().toISOString().split('T')[0],
newTime: '',
newAction: ''
})
// Lade Worklog-Einträge für das ausgewählte Datum
async function loadWorklogEntries() {
try {
const date = form.value.originalDate
if (!date) return
// Hole alle Worklog-Einträge für das Datum vom Backend
const response = await fetch(`http://localhost:3010/api/timefix/worklog-entries?date=${date}`, {
headers: authStore.getAuthHeaders()
})
if (!response.ok) throw new Error('Fehler beim Laden der Einträge')
const entries = await response.json()
// Formatiere die Einträge für das Dropdown
availableEntries.value = entries.map(entry => ({
id: entry.id,
time: entry.time,
action: entry.action,
tstamp: entry.tstamp
}))
// Wenn keine Einträge gefunden wurden
if (availableEntries.value.length === 0) {
console.log('Keine Worklog-Einträge für dieses Datum gefunden')
}
// Reset worklogId wenn Datum geändert wird
form.value.worklogId = ''
} catch (error) {
console.error('Fehler beim Laden der Worklog-Einträge:', error)
availableEntries.value = []
}
}
// Wenn ein Eintrag ausgewählt wird, fülle die Felder vor
function onEntrySelected() {
const selectedEntry = availableEntries.value.find(e => e.id === form.value.worklogId)
if (selectedEntry) {
// Fülle Datum und Aktion vor (aber nicht die Uhrzeit)
form.value.newDate = form.value.originalDate
form.value.newAction = selectedEntry.action
// form.value.newTime bleibt leer
}
}
// Lade alle Zeitkorrekturen für heute
async function loadTimefixes() {
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/timefix', {
headers: authStore.getAuthHeaders()
})
if (!response.ok) throw new Error('Fehler beim Laden der Zeitkorrekturen')
timefixes.value = await response.json()
} catch (error) {
console.error('Fehler beim Laden der Zeitkorrekturen:', error)
alert('Fehler beim Laden der Zeitkorrekturen')
} finally {
loading.value = false
}
}
// Erstelle neue Zeitkorrektur
async function createTimefix() {
if (!form.value.worklogId || !form.value.newDate || !form.value.newTime || !form.value.newAction) {
await alert('Bitte füllen Sie alle Felder aus')
return
}
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/timefix', {
method: 'POST',
headers: {
...authStore.getAuthHeaders(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
worklogId: form.value.worklogId,
newDate: form.value.newDate,
newTime: form.value.newTime,
newAction: form.value.newAction
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Fehler beim Erstellen der Zeitkorrektur')
}
resetForm()
await Promise.all([
loadTimefixes(),
loadWorklogEntries() // Lade auch die Dropdown-Liste neu
])
} catch (error) {
console.error('Fehler beim Erstellen der Zeitkorrektur:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Lösche Zeitkorrektur
async function deleteTimefix(id) {
const confirmed = await confirm('Möchten Sie diese Zeitkorrektur wirklich löschen?', 'Zeitkorrektur löschen')
if (!confirmed) {
return
}
try {
loading.value = true
const response = await fetch(`http://localhost:3010/api/timefix/${id}`, {
method: 'DELETE',
headers: authStore.getAuthHeaders()
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Fehler beim Löschen der Zeitkorrektur')
}
await loadTimefixes()
} catch (error) {
console.error('Fehler beim Löschen der Zeitkorrektur:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Formatiere Datum
function formatDate(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
// Formatiere Aktion
function formatAction(action) {
const actions = {
'start work': 'Arbeit beginnen',
'stop work': 'Arbeit beenden',
'start pause': 'Pause beginnen',
'stop pause': 'Pause beenden'
}
return actions[action] || action
}
// Formular zurücksetzen
function resetForm() {
form.value = {
originalDate: new Date().toISOString().split('T')[0],
worklogId: '',
newDate: new Date().toISOString().split('T')[0],
newTime: '',
newAction: ''
}
availableEntries.value = []
}
onMounted(() => {
loadTimefixes()
loadWorklogEntries()
})
</script>
<style scoped>
.timefix-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
font-size: 28px;
margin-bottom: 8px;
color: #2c3e50;
}
.subtitle {
color: #7f8c8d;
margin-bottom: 24px;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
h2 {
font-size: 20px;
margin-bottom: 20px;
color: #34495e;
}
/* Formular */
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #555;
}
input, select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus, select:focus {
outline: none;
border-color: #4CAF50;
}
input:disabled, select:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #45a049;
}
.btn-secondary {
background: #ecf0f1;
color: #555;
}
.btn-secondary:hover {
background: #d5dbdd;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Tabelle */
.timefix-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.timefix-table thead {
background: #f8f9fa;
}
.timefix-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #ddd;
}
.timefix-table td {
padding: 12px;
border-bottom: 1px solid #eee;
}
.timefix-table tbody tr:hover {
background: #f8f9fa;
}
.timefix-table .actions {
text-align: center;
}
.btn-small {
padding: 6px 12px;
font-size: 13px;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.loading, .empty-state {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,384 @@
<template>
<div class="vacation-container">
<Modal
:show="showModal"
:title="modalConfig.title"
:message="modalConfig.message"
:type="modalConfig.type"
:confirmText="modalConfig.confirmText"
:cancelText="modalConfig.cancelText"
@confirm="onConfirm"
@cancel="onCancel"
/>
<!-- Formular für neuen Urlaubseintrag -->
<div class="card form-card">
<form @submit.prevent="createVacation" class="vacation-form">
<div class="form-group">
<label for="vacationType">Umfang</label>
<select
id="vacationType"
v-model="form.vacationType"
@change="onTypeChange"
required
>
<option :value="0">Zeitraum</option>
<option :value="1">Halber Tag</option>
</select>
</div>
<div class="form-group">
<label for="startDate">Urlaubsbeginn</label>
<input
type="date"
id="startDate"
v-model="form.startDate"
@change="onStartDateChange"
required
/>
</div>
<div class="form-group">
<label for="endDate">Urlaubsende</label>
<input
type="date"
id="endDate"
v-model="form.endDate"
:disabled="form.vacationType === 1"
:required="form.vacationType === 0"
/>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" :disabled="loading">
{{ loading ? 'Wird gespeichert...' : 'Urlaub eintragen' }}
</button>
</div>
</form>
</div>
<hr class="separator" />
<!-- Tabelle der Urlaubseinträge -->
<div class="card table-card">
<div v-if="loading && vacations.length === 0" class="loading">
Lade Urlaubseinträge...
</div>
<div v-else-if="vacations.length === 0" class="empty-state">
Keine Urlaubseinträge vorhanden.
</div>
<table v-else class="vacation-table">
<thead>
<tr>
<th>Umfang</th>
<th>Urlaubsbeginn</th>
<th>Urlaubsende</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="vacation in vacations" :key="vacation.id">
<td>{{ vacation.type }}</td>
<td>{{ formatDate(vacation.startDate) }}</td>
<td>{{ formatDate(vacation.endDate) }}</td>
<td class="actions">
<button
@click="deleteVacation(vacation.id)"
class="btn btn-danger btn-small"
title="Urlaubseintrag löschen"
>
Löschen
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/authStore'
import { useModal } from '../composables/useModal'
import Modal from '../components/Modal.vue'
const authStore = useAuthStore()
const vacations = ref([])
const loading = ref(false)
const { showModal, modalConfig, alert, confirm, onConfirm, onCancel } = useModal()
const form = ref({
vacationType: 0, // 0 = Zeitraum, 1 = Halber Tag
startDate: '',
endDate: ''
})
// Wenn Typ geändert wird
function onTypeChange() {
if (form.value.vacationType === 1) {
// Halber Tag: endDate = startDate
form.value.endDate = form.value.startDate
}
}
// Wenn Startdatum geändert wird bei "Halber Tag"
function onStartDateChange() {
if (form.value.vacationType === 1) {
form.value.endDate = form.value.startDate
}
}
// Lade alle Urlaubseinträge
async function loadVacations() {
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/vacation', {
headers: authStore.getAuthHeaders()
})
if (!response.ok) throw new Error('Fehler beim Laden der Urlaubseinträge')
vacations.value = await response.json()
} catch (error) {
console.error('Fehler beim Laden der Urlaubseinträge:', error)
await alert('Fehler beim Laden der Urlaubseinträge', 'Fehler')
} finally {
loading.value = false
}
}
// Erstelle neuen Urlaubseintrag
async function createVacation() {
if (!form.value.startDate || (form.value.vacationType === 0 && !form.value.endDate)) {
await alert('Bitte füllen Sie alle Felder aus')
return
}
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/vacation', {
method: 'POST',
headers: {
...authStore.getAuthHeaders(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
vacationType: form.value.vacationType,
startDate: form.value.startDate,
endDate: form.value.endDate
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Fehler beim Erstellen des Urlaubseintrags')
}
resetForm()
await loadVacations()
} catch (error) {
console.error('Fehler beim Erstellen des Urlaubseintrags:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Lösche Urlaubseintrag
async function deleteVacation(id) {
const confirmed = await confirm('Möchten Sie diesen Urlaubseintrag wirklich löschen?', 'Urlaubseintrag löschen')
if (!confirmed) {
return
}
try {
loading.value = true
const response = await fetch(`http://localhost:3010/api/vacation/${id}`, {
method: 'DELETE',
headers: authStore.getAuthHeaders()
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Fehler beim Löschen des Urlaubseintrags')
}
await loadVacations()
} catch (error) {
console.error('Fehler beim Löschen des Urlaubseintrags:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Formatiere Datum
function formatDate(dateString) {
if (!dateString) return '-'
const date = new Date(dateString + 'T00:00:00')
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
// Formular zurücksetzen
function resetForm() {
form.value = {
vacationType: 0,
startDate: '',
endDate: ''
}
}
onMounted(() => {
loadVacations()
})
</script>
<style scoped>
.vacation-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.separator {
border: none;
border-top: 1px solid #e0e0e0;
margin: 24px 0;
}
/* Formular */
.vacation-form {
display: flex;
gap: 16px;
align-items: flex-end;
}
.form-group {
display: flex;
flex-direction: column;
flex: 1;
}
.form-group:last-child {
flex: 0;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #555;
}
input, select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus, select:focus {
outline: none;
border-color: #4CAF50;
}
input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #45a049;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Tabelle */
.vacation-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.vacation-table thead {
background: #f8f9fa;
}
.vacation-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #ddd;
}
.vacation-table td {
padding: 12px;
border-bottom: 1px solid #eee;
}
.vacation-table tbody tr:hover {
background: #f8f9fa;
}
.vacation-table .actions {
text-align: center;
width: 120px;
}
.btn-small {
padding: 6px 12px;
font-size: 13px;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.loading, .empty-state {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
</style>

View File

@@ -1,8 +1,6 @@
<template>
<div class="week-overview">
<div class="card">
<h2>Wochenübersicht</h2>
<p class="subtitle">Übersicht Ihrer Arbeitszeiten für die aktuelle Woche</p>
<div v-if="loading" class="loading">
Lade Daten...