Add calendar routes to backend and frontend; update routing and UI components for calendar feature
This commit is contained in:
38
backend/src/controllers/CalendarController.js
Normal file
38
backend/src/controllers/CalendarController.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const CalendarService = require('../services/CalendarService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller für Kalender
|
||||||
|
* Verarbeitet HTTP-Requests und delegiert an CalendarService
|
||||||
|
*/
|
||||||
|
class CalendarController {
|
||||||
|
/**
|
||||||
|
* Holt Kalenderdaten für einen Monat
|
||||||
|
*/
|
||||||
|
async getMonth(req, res) {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id || 1;
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
const month = parseInt(req.query.month) || (new Date().getMonth() + 1);
|
||||||
|
|
||||||
|
if (isNaN(year) || year < 1900 || year > 2100) {
|
||||||
|
return res.status(400).json({ message: 'Ungültiges Jahr' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(month) || month < 1 || month > 12) {
|
||||||
|
return res.status(400).json({ message: 'Ungültiger Monat' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarData = await CalendarService.getCalendarMonth(userId, year, month);
|
||||||
|
res.json(calendarData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Kalenderdaten:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'Fehler beim Abrufen der Kalenderdaten',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new CalendarController();
|
||||||
|
|
||||||
@@ -82,6 +82,10 @@ app.use('/api/sick', authenticateToken, sickRouter);
|
|||||||
const workdaysRouter = require('./routes/workdays');
|
const workdaysRouter = require('./routes/workdays');
|
||||||
app.use('/api/workdays', authenticateToken, workdaysRouter);
|
app.use('/api/workdays', authenticateToken, workdaysRouter);
|
||||||
|
|
||||||
|
// Calendar routes (geschützt) - MIT ID-Hashing
|
||||||
|
const calendarRouter = require('./routes/calendar');
|
||||||
|
app.use('/api/calendar', authenticateToken, calendarRouter);
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
|
|||||||
13
backend/src/routes/calendar.js
Normal file
13
backend/src/routes/calendar.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const CalendarController = require('../controllers/CalendarController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routen für Kalender
|
||||||
|
*/
|
||||||
|
|
||||||
|
// GET /api/calendar?year=2025&month=10 - Kalenderdaten für einen Monat abrufen
|
||||||
|
router.get('/', CalendarController.getMonth.bind(CalendarController));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
283
backend/src/services/CalendarService.js
Normal file
283
backend/src/services/CalendarService.js
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
const database = require('../config/database');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service-Klasse für Kalender-Daten
|
||||||
|
* Lädt alle Informationen für einen Monat
|
||||||
|
*/
|
||||||
|
class CalendarService {
|
||||||
|
constructor() {
|
||||||
|
this.defaultUserId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt Kalenderdaten für einen Monat
|
||||||
|
* @param {number} userId - Benutzer-ID
|
||||||
|
* @param {number} year - Jahr
|
||||||
|
* @param {number} month - Monat (1-12)
|
||||||
|
* @returns {Promise<Object>} Kalenderdaten
|
||||||
|
*/
|
||||||
|
async getCalendarMonth(userId, year, month) {
|
||||||
|
const { Holiday, Sick, Vacation } = database.getModels();
|
||||||
|
const sequelize = database.sequelize;
|
||||||
|
|
||||||
|
// Berechne Start- und End-Datum des Monats
|
||||||
|
const firstDay = new Date(year, month - 1, 1);
|
||||||
|
const lastDay = new Date(year, month, 0);
|
||||||
|
|
||||||
|
// Erweitere den Bereich um die Tage aus dem vorherigen/nächsten Monat
|
||||||
|
const startDate = new Date(firstDay);
|
||||||
|
startDate.setDate(startDate.getDate() - ((startDate.getDay() + 6) % 7)); // Montag der ersten Woche
|
||||||
|
|
||||||
|
const endDate = new Date(lastDay);
|
||||||
|
const daysToAdd = (7 - ((endDate.getDay() + 6) % 7)) % 7;
|
||||||
|
endDate.setDate(endDate.getDate() + daysToAdd); // Sonntag der letzten Woche
|
||||||
|
|
||||||
|
const startDateStr = this._formatDate(startDate);
|
||||||
|
const endDateStr = this._formatDate(endDate);
|
||||||
|
|
||||||
|
// Hole alle Feiertage
|
||||||
|
const holidays = await Holiday.findAll({
|
||||||
|
where: {
|
||||||
|
date: {
|
||||||
|
[Op.between]: [startDateStr, endDateStr]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const holidayMap = new Map();
|
||||||
|
holidays.forEach(h => {
|
||||||
|
holidayMap.set(h.date, h.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hole alle Krankheitstage
|
||||||
|
const sickEntries = await Sick.findAll({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
first_day: {
|
||||||
|
[Op.between]: [startDateStr, endDateStr]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
last_day: {
|
||||||
|
[Op.between]: [startDateStr, endDateStr]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Op.and]: [
|
||||||
|
{ first_day: { [Op.lte]: startDateStr } },
|
||||||
|
{ last_day: { [Op.gte]: endDateStr } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstelle Set mit allen Krankheitstagen
|
||||||
|
const sickDays = new Set();
|
||||||
|
sickEntries.forEach(sick => {
|
||||||
|
const start = new Date(Math.max(new Date(sick.first_day + 'T00:00:00'), new Date(startDateStr + 'T00:00:00')));
|
||||||
|
const end = new Date(Math.min(new Date(sick.last_day + 'T00:00:00'), new Date(endDateStr + 'T00:00:00')));
|
||||||
|
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
sickDays.add(this._formatDate(d));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hole alle Urlaubstage
|
||||||
|
const vacationEntries = await Vacation.findAll({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
first_day: {
|
||||||
|
[Op.between]: [startDateStr, endDateStr]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
last_day: {
|
||||||
|
[Op.between]: [startDateStr, endDateStr]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Op.and]: [
|
||||||
|
{ first_day: { [Op.lte]: startDateStr } },
|
||||||
|
{ last_day: { [Op.gte]: endDateStr } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstelle Map mit Urlaubstagen (und ob es halbe Tage sind)
|
||||||
|
const vacationDays = new Map();
|
||||||
|
vacationEntries.forEach(vac => {
|
||||||
|
const start = new Date(Math.max(new Date(vac.first_day + 'T00:00:00'), new Date(startDateStr + 'T00:00:00')));
|
||||||
|
const end = new Date(Math.min(new Date(vac.last_day + 'T00:00:00'), new Date(endDateStr + 'T00:00:00')));
|
||||||
|
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
vacationDays.set(this._formatDate(d), vac.half_day === 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hole Arbeitszeiten über den TimeEntryService (ähnlich wie WeekOverview)
|
||||||
|
// Wir verwenden eine vereinfachte Query die alle worklog-Einträge holt
|
||||||
|
const workQuery = `
|
||||||
|
SELECT DATE(tstamp) as work_date, tstamp, state, relatedTo_id, id
|
||||||
|
FROM worklog
|
||||||
|
WHERE user_id = :userId
|
||||||
|
AND DATE(tstamp) BETWEEN :startDate AND :endDate
|
||||||
|
ORDER BY tstamp ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const workEntries = await sequelize.query(workQuery, {
|
||||||
|
replacements: {
|
||||||
|
userId,
|
||||||
|
startDate: startDateStr,
|
||||||
|
endDate: endDateStr
|
||||||
|
},
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gruppiere nach Datum und berechne Arbeitszeit
|
||||||
|
const workMap = new Map();
|
||||||
|
const entriesByDate = new Map();
|
||||||
|
|
||||||
|
workEntries.forEach(entry => {
|
||||||
|
if (!entriesByDate.has(entry.work_date)) {
|
||||||
|
entriesByDate.set(entry.work_date, []);
|
||||||
|
}
|
||||||
|
entriesByDate.get(entry.work_date).push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Berechne für jeden Tag die Arbeitszeit (Nettoarbeitszeit mit Pausenabzug)
|
||||||
|
entriesByDate.forEach((entries, date) => {
|
||||||
|
let totalWorkMinutes = 0;
|
||||||
|
let totalPauseMinutes = 0;
|
||||||
|
|
||||||
|
// Parse state für alle Einträge
|
||||||
|
const parsedEntries = entries.map(entry => {
|
||||||
|
let state = entry.state;
|
||||||
|
if (typeof state === 'string' && state.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
state = JSON.parse(state);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const action = state?.action || state;
|
||||||
|
return { ...entry, action };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finde alle start work -> stop work Paare und berechne Bruttoarbeitszeit
|
||||||
|
parsedEntries.forEach(entry => {
|
||||||
|
if (entry.action === 'stop work') {
|
||||||
|
// Finde das zugehörige start work über relatedTo_id
|
||||||
|
const startEntry = parsedEntries.find(e => e.id === entry.relatedTo_id && e.action === 'start work');
|
||||||
|
|
||||||
|
if (startEntry) {
|
||||||
|
const start = new Date(startEntry.tstamp);
|
||||||
|
const end = new Date(entry.tstamp);
|
||||||
|
const minutes = (end - start) / (1000 * 60);
|
||||||
|
totalWorkMinutes += minutes;
|
||||||
|
|
||||||
|
// Berechne Pausen innerhalb dieses Arbeitsblocks
|
||||||
|
const pauseEntries = parsedEntries.filter(e =>
|
||||||
|
new Date(e.tstamp) > start &&
|
||||||
|
new Date(e.tstamp) < end &&
|
||||||
|
(e.action === 'start pause' || e.action === 'stop pause')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paare start pause -> stop pause
|
||||||
|
pauseEntries.forEach(pauseEntry => {
|
||||||
|
if (pauseEntry.action === 'stop pause') {
|
||||||
|
const pauseStart = pauseEntries.find(pe =>
|
||||||
|
pe.id === pauseEntry.relatedTo_id &&
|
||||||
|
pe.action === 'start pause'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pauseStart) {
|
||||||
|
const pStart = new Date(pauseStart.tstamp);
|
||||||
|
const pEnd = new Date(pauseEntry.tstamp);
|
||||||
|
const pauseMins = (pEnd - pStart) / (1000 * 60);
|
||||||
|
totalPauseMinutes += pauseMins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const netMinutes = totalWorkMinutes - totalPauseMinutes;
|
||||||
|
|
||||||
|
if (netMinutes > 0) {
|
||||||
|
workMap.set(date, netMinutes / 60); // Convert to hours
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstelle Kalendertage
|
||||||
|
const days = [];
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
|
||||||
|
while (currentDate <= endDate) {
|
||||||
|
const dateStr = this._formatDate(currentDate);
|
||||||
|
const day = currentDate.getDate();
|
||||||
|
const isCurrentMonth = currentDate.getMonth() === (month - 1);
|
||||||
|
const isToday = this._isToday(currentDate);
|
||||||
|
|
||||||
|
const dayData = {
|
||||||
|
date: dateStr,
|
||||||
|
day,
|
||||||
|
isCurrentMonth,
|
||||||
|
isToday,
|
||||||
|
holiday: holidayMap.get(dateStr) || null,
|
||||||
|
sick: sickDays.has(dateStr),
|
||||||
|
vacation: vacationDays.has(dateStr) ? (vacationDays.get(dateStr) ? 'half' : 'full') : null,
|
||||||
|
workedHours: workMap.get(dateStr) || null
|
||||||
|
};
|
||||||
|
|
||||||
|
days.push(dayData);
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gruppiere in Wochen
|
||||||
|
const weeks = [];
|
||||||
|
for (let i = 0; i < days.length; i += 7) {
|
||||||
|
weeks.push(days.slice(i, i + 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
weeks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert ein Datum als YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
_formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob ein Datum heute ist
|
||||||
|
*/
|
||||||
|
_isToday(date) {
|
||||||
|
const today = new Date();
|
||||||
|
return date.getDate() === today.getDate() &&
|
||||||
|
date.getMonth() === today.getMonth() &&
|
||||||
|
date.getFullYear() === today.getFullYear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new CalendarService();
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@ const pageTitle = computed(() => {
|
|||||||
'vacation': 'Urlaub',
|
'vacation': 'Urlaub',
|
||||||
'sick': 'Krankheit',
|
'sick': 'Krankheit',
|
||||||
'workdays': 'Arbeitstage',
|
'workdays': 'Arbeitstage',
|
||||||
|
'calendar': 'Kalender',
|
||||||
'entries': 'Einträge',
|
'entries': 'Einträge',
|
||||||
'stats': 'Statistiken'
|
'stats': 'Statistiken'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Timefix from '../views/Timefix.vue'
|
|||||||
import Vacation from '../views/Vacation.vue'
|
import Vacation from '../views/Vacation.vue'
|
||||||
import Sick from '../views/Sick.vue'
|
import Sick from '../views/Sick.vue'
|
||||||
import Workdays from '../views/Workdays.vue'
|
import Workdays from '../views/Workdays.vue'
|
||||||
|
import Calendar from '../views/Calendar.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -84,6 +85,12 @@ const router = createRouter({
|
|||||||
component: Workdays,
|
component: Workdays,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/calendar',
|
||||||
|
name: 'calendar',
|
||||||
|
component: Calendar,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/entries',
|
path: '/entries',
|
||||||
name: 'entries',
|
name: 'entries',
|
||||||
|
|||||||
337
frontend/src/views/Calendar.vue
Normal file
337
frontend/src/views/Calendar.vue
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar-page">
|
||||||
|
<div class="card">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button @click="previousMonth" class="nav-button">«</button>
|
||||||
|
|
||||||
|
<div class="month-year-selector">
|
||||||
|
<select v-model.number="selectedMonth" @change="loadCalendar" class="month-select">
|
||||||
|
<option v-for="(name, index) in monthNames" :key="index" :value="index + 1">
|
||||||
|
{{ name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model.number="selectedYear"
|
||||||
|
@change="loadCalendar"
|
||||||
|
min="2000"
|
||||||
|
max="2100"
|
||||||
|
class="year-input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="nextMonth" class="nav-button">»</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kalender -->
|
||||||
|
<table class="calendar-table" v-if="calendar">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="day in weekDays" :key="day" :title="day">
|
||||||
|
{{ day.substring(0, 3) }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(week, weekIndex) in calendar.weeks" :key="weekIndex">
|
||||||
|
<td v-for="(day, dayIndex) in week" :key="dayIndex">
|
||||||
|
<div
|
||||||
|
class="calendar-day"
|
||||||
|
:class="{
|
||||||
|
'other-month': !day.isCurrentMonth,
|
||||||
|
'same-month': day.isCurrentMonth,
|
||||||
|
'today': day.isToday,
|
||||||
|
'has-work': day.workedHours,
|
||||||
|
'has-vacation': day.vacation,
|
||||||
|
'has-holiday': day.holiday,
|
||||||
|
'has-sick': day.sick
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="day-number">{{ day.day }}</div>
|
||||||
|
<div v-if="day.workedHours" class="day-info work-info">
|
||||||
|
Gearbeitet: {{ formatHours(day.workedHours) }} h
|
||||||
|
</div>
|
||||||
|
<div v-if="day.vacation" class="day-info vacation-info">
|
||||||
|
{{ day.vacation === 'half' ? 'Halber ' : '' }}Urlaub
|
||||||
|
</div>
|
||||||
|
<div v-if="day.sick" class="day-info sick-info">
|
||||||
|
Krank
|
||||||
|
</div>
|
||||||
|
<div v-if="day.holiday" class="day-info holiday-info">
|
||||||
|
{{ day.holiday }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading">Lade Kalender...</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 now = new Date()
|
||||||
|
const selectedMonth = ref(now.getMonth() + 1)
|
||||||
|
const selectedYear = ref(now.getFullYear())
|
||||||
|
const calendar = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const monthNames = [
|
||||||
|
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||||
|
]
|
||||||
|
|
||||||
|
const weekDays = [
|
||||||
|
'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Lade Kalenderdaten
|
||||||
|
async function loadCalendar() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:3010/api/calendar?year=${selectedYear.value}&month=${selectedMonth.value}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authStore.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Laden des Kalenders')
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.value = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden des Kalenders:', err)
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
function previousMonth() {
|
||||||
|
if (selectedMonth.value === 1) {
|
||||||
|
selectedMonth.value = 12
|
||||||
|
selectedYear.value--
|
||||||
|
} else {
|
||||||
|
selectedMonth.value--
|
||||||
|
}
|
||||||
|
loadCalendar()
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextMonth() {
|
||||||
|
if (selectedMonth.value === 12) {
|
||||||
|
selectedMonth.value = 1
|
||||||
|
selectedYear.value++
|
||||||
|
} else {
|
||||||
|
selectedMonth.value++
|
||||||
|
}
|
||||||
|
loadCalendar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatiere Stunden
|
||||||
|
function formatHours(hours) {
|
||||||
|
if (!hours || typeof hours !== 'number') {
|
||||||
|
return '0:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = Math.floor(hours)
|
||||||
|
const m = Math.round((hours % 1) * 60)
|
||||||
|
return `${h}:${String(m).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiales Laden
|
||||||
|
onMounted(() => {
|
||||||
|
loadCalendar()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background: #45a049;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-year-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-select,
|
||||||
|
.year-input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-select {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-input {
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-select:focus,
|
||||||
|
.year-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-table th {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-table td {
|
||||||
|
padding: 2px;
|
||||||
|
vertical-align: top;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 90px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.other-month {
|
||||||
|
background: #f9f9f9;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today {
|
||||||
|
background: #e8f5e9;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.other-month .day-number {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-info {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin: 2px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-info {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vacation-info {
|
||||||
|
background: #fff9c4;
|
||||||
|
color: #f57f17;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sick-info {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holiday-info {
|
||||||
|
background: #f3e5f5;
|
||||||
|
color: #7b1fa2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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