From b65a13d815d97ffb6d6a416b675c77c02186e563 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 17 Oct 2025 15:54:30 +0200 Subject: [PATCH] Add timefix and vacation routes to backend; update frontend for new routes and page titles --- backend/add-vacation-index.sql | 9 + backend/src/controllers/TimefixController.js | 94 ++++ backend/src/controllers/VacationController.js | 73 +++ backend/src/index.js | 8 + backend/src/middleware/unhashRequest.js | 2 + backend/src/routes/timefix.js | 21 + backend/src/routes/vacation.js | 18 + backend/src/services/TimefixService.js | 330 +++++++++++ backend/src/services/VacationService.js | 120 ++++ frontend/src/App.vue | 55 +- frontend/src/components/Modal.vue | 216 ++++++++ frontend/src/composables/useModal.js | 67 +++ frontend/src/router/index.js | 14 + frontend/src/views/Timefix.vue | 513 ++++++++++++++++++ frontend/src/views/Vacation.vue | 384 +++++++++++++ frontend/src/views/WeekOverview.vue | 2 - 16 files changed, 1913 insertions(+), 13 deletions(-) create mode 100644 backend/add-vacation-index.sql create mode 100644 backend/src/controllers/TimefixController.js create mode 100644 backend/src/controllers/VacationController.js create mode 100644 backend/src/routes/timefix.js create mode 100644 backend/src/routes/vacation.js create mode 100644 backend/src/services/TimefixService.js create mode 100644 backend/src/services/VacationService.js create mode 100644 frontend/src/components/Modal.vue create mode 100644 frontend/src/composables/useModal.js create mode 100644 frontend/src/views/Timefix.vue create mode 100644 frontend/src/views/Vacation.vue diff --git a/backend/add-vacation-index.sql b/backend/add-vacation-index.sql new file mode 100644 index 0000000..7f28f09 --- /dev/null +++ b/backend/add-vacation-index.sql @@ -0,0 +1,9 @@ +-- Füge Index für vacation Tabelle hinzu +-- Optimiert die Abfrage: WHERE user_id = ? AND first_day >= ? ORDER BY first_day DESC + +CREATE INDEX idx_vacation_user_first_day +ON vacation (user_id, first_day DESC); + +-- Prüfe ob der Index erstellt wurde +SHOW INDEX FROM vacation WHERE Key_name = 'idx_vacation_user_first_day'; + diff --git a/backend/src/controllers/TimefixController.js b/backend/src/controllers/TimefixController.js new file mode 100644 index 0000000..bff31cd --- /dev/null +++ b/backend/src/controllers/TimefixController.js @@ -0,0 +1,94 @@ +const timefixService = require('../services/TimefixService'); + +class TimefixController { + /** + * Holt alle Worklog-Einträge für ein bestimmtes Datum + */ + async getWorklogEntriesForDate(req, res) { + try { + const userId = req.user.userId; + const { date } = req.query; + + if (!date) { + return res.status(400).json({ message: 'Datum ist erforderlich' }); + } + + const entries = await timefixService.getWorklogEntriesForDate(userId, date); + res.json(entries); + } catch (error) { + console.error('Fehler beim Abrufen der Worklog-Einträge:', error); + res.status(500).json({ + message: 'Fehler beim Abrufen der Worklog-Einträge', + error: error.message + }); + } + } + + /** + * Holt alle Zeitkorrekturen für den heutigen Tag + */ + async getTodayTimefixes(req, res) { + try { + const userId = req.user.userId; + const timefixes = await timefixService.getTodayTimefixes(userId); + res.json(timefixes); + } catch (error) { + console.error('Fehler beim Abrufen der Timefixes:', error); + res.status(500).json({ + message: 'Fehler beim Abrufen der Zeitkorrekturen', + error: error.message + }); + } + } + + /** + * Erstellt eine neue Zeitkorrektur + */ + async createTimefix(req, res) { + try { + const userId = req.user.userId; + const { worklogId, newDate, newTime, newAction } = req.body; + + if (!worklogId || !newDate || !newTime || !newAction) { + return res.status(400).json({ + message: 'Fehlende Pflichtfelder: worklogId, newDate, newTime, newAction' + }); + } + + const timefix = await timefixService.createTimefix(userId, worklogId, newDate, newTime, newAction); + + res.status(201).json({ + message: 'Zeitkorrektur erfolgreich erstellt', + timefix + }); + } catch (error) { + console.error('Fehler beim Erstellen der Timefix:', error); + res.status(500).json({ + message: 'Fehler beim Erstellen der Zeitkorrektur', + error: error.message + }); + } + } + + /** + * Löscht eine Zeitkorrektur + */ + async deleteTimefix(req, res) { + try { + const userId = req.user.userId; + const timefixId = req.params.id; + + await timefixService.deleteTimefix(userId, timefixId); + + res.json({ message: 'Zeitkorrektur erfolgreich gelöscht' }); + } catch (error) { + console.error('Fehler beim Löschen der Timefix:', error); + res.status(500).json({ + message: 'Fehler beim Löschen der Zeitkorrektur', + error: error.message + }); + } + } +} + +module.exports = new TimefixController(); diff --git a/backend/src/controllers/VacationController.js b/backend/src/controllers/VacationController.js new file mode 100644 index 0000000..8581488 --- /dev/null +++ b/backend/src/controllers/VacationController.js @@ -0,0 +1,73 @@ +const vacationService = require('../services/VacationService'); + +class VacationController { + /** + * Holt alle Urlaubseinträge für einen User + */ + async getAllVacations(req, res) { + try { + const userId = req.user.userId; + const vacations = await vacationService.getAllVacations(userId); + res.json(vacations); + } catch (error) { + console.error('Fehler beim Abrufen der Urlaubseinträge:', error); + res.status(500).json({ + message: 'Fehler beim Abrufen der Urlaubseinträge', + error: error.message + }); + } + } + + /** + * Erstellt einen neuen Urlaubseintrag + */ + async createVacation(req, res) { + try { + const userId = req.user.userId; + const { vacationType, startDate, endDate } = req.body; + + if (vacationType === undefined || !startDate) { + return res.status(400).json({ + message: 'Fehlende Pflichtfelder: vacationType, startDate' + }); + } + + const vacation = await vacationService.createVacation(userId, vacationType, startDate, endDate); + + res.status(201).json({ + message: 'Urlaubseintrag erfolgreich erstellt', + vacation + }); + } catch (error) { + console.error('Fehler beim Erstellen des Urlaubseintrags:', error); + res.status(500).json({ + message: 'Fehler beim Erstellen des Urlaubseintrags', + error: error.message + }); + } + } + + /** + * Löscht einen Urlaubseintrag + */ + async deleteVacation(req, res) { + try { + const userId = req.user.userId; + const vacationId = req.params.id; + + console.log('DEBUG deleteVacation: vacationId =', vacationId, 'type =', typeof vacationId); + + await vacationService.deleteVacation(userId, vacationId); + + res.json({ message: 'Urlaubseintrag erfolgreich gelöscht' }); + } catch (error) { + console.error('Fehler beim Löschen des Urlaubseintrags:', error); + res.status(500).json({ + message: 'Fehler beim Löschen des Urlaubseintrags', + error: error.message + }); + } + } +} + +module.exports = new VacationController(); diff --git a/backend/src/index.js b/backend/src/index.js index 20c51de..21ce5fe 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -66,6 +66,14 @@ app.use('/api/time-entries', authenticateToken, timeEntriesRouter); const weekOverviewRouter = require('./routes/weekOverview'); app.use('/api/week-overview', authenticateToken, weekOverviewRouter); +// Timefix routes (geschützt) - MIT ID-Hashing +const timefixRouter = require('./routes/timefix'); +app.use('/api/timefix', authenticateToken, timefixRouter); + +// Vacation routes (geschützt) - MIT ID-Hashing +const vacationRouter = require('./routes/vacation'); +app.use('/api/vacation', authenticateToken, vacationRouter); + // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); diff --git a/backend/src/middleware/unhashRequest.js b/backend/src/middleware/unhashRequest.js index fc3ab8f..8a03764 100644 --- a/backend/src/middleware/unhashRequest.js +++ b/backend/src/middleware/unhashRequest.js @@ -16,7 +16,9 @@ const unhashRequestIds = (req, res, next) => { // Route-Parameter verarbeiten if (req.params && typeof req.params === 'object') { + console.log('DEBUG unhashRequest: req.params before =', req.params); req.params = unhashRequestData(req.params); + console.log('DEBUG unhashRequest: req.params after =', req.params); } next(); diff --git a/backend/src/routes/timefix.js b/backend/src/routes/timefix.js new file mode 100644 index 0000000..648c449 --- /dev/null +++ b/backend/src/routes/timefix.js @@ -0,0 +1,21 @@ +const express = require('express'); +const router = express.Router(); +const TimefixController = require('../controllers/TimefixController'); +const unhashRequestIds = require('../middleware/unhashRequest'); + +// Hinweis: Authentifizierung wird bereits in index.js durch authenticateToken Middleware gehandhabt + +// GET /api/timefix/worklog-entries - Worklog-Einträge für ein Datum +router.get('/worklog-entries', TimefixController.getWorklogEntriesForDate); + +// GET /api/timefix - Liste aller Timefixes für heute +router.get('/', TimefixController.getTodayTimefixes); + +// POST /api/timefix - Neue Zeitkorrektur erstellen +router.post('/', TimefixController.createTimefix); + +// DELETE /api/timefix/:id - Zeitkorrektur löschen +router.delete('/:id', unhashRequestIds, TimefixController.deleteTimefix); + +module.exports = router; + diff --git a/backend/src/routes/vacation.js b/backend/src/routes/vacation.js new file mode 100644 index 0000000..867aa40 --- /dev/null +++ b/backend/src/routes/vacation.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); +const VacationController = require('../controllers/VacationController'); +const unhashRequestIds = require('../middleware/unhashRequest'); + +// Hinweis: Authentifizierung wird bereits in index.js durch authenticateToken Middleware gehandhabt + +// GET /api/vacation - Liste aller Urlaubseinträge +router.get('/', VacationController.getAllVacations); + +// POST /api/vacation - Neuen Urlaubseintrag erstellen +router.post('/', VacationController.createVacation); + +// DELETE /api/vacation/:id - Urlaubseintrag löschen +router.delete('/:id', unhashRequestIds, VacationController.deleteVacation); + +module.exports = router; + diff --git a/backend/src/services/TimefixService.js b/backend/src/services/TimefixService.js new file mode 100644 index 0000000..894d67a --- /dev/null +++ b/backend/src/services/TimefixService.js @@ -0,0 +1,330 @@ +const database = require('../config/database'); +const { Op } = require('sequelize'); + +/** + * Service-Klasse für Zeitkorrekturen + * Enthält die gesamte Business-Logik für Timefixes + */ +class TimefixService { + constructor() { + this.defaultUserId = 1; + } + + /** + * Holt alle Worklog-Einträge für ein bestimmtes Datum + * @param {number} userId - Benutzer-ID + * @param {string} date - Datum im Format YYYY-MM-DD + * @returns {Promise} Array von Worklog-Einträgen mit korrigierten Zeiten + */ + async getWorklogEntriesForDate(userId, date) { + const { Worklog } = database.getModels(); + const sequelize = database.sequelize; + + // Hole alle Worklog-Einträge für das Datum + const entries = await Worklog.findAll({ + where: { + user_id: userId, + tstamp: { + [Op.like]: `${date}%` + } + }, + order: [['tstamp', 'ASC']], + raw: true + }); + + // Hole auch die Timefixes für diese Einträge + const entryIds = entries.map(e => e.id); + const timefixesForEntries = await sequelize.query( + `SELECT worklog_id, fix_type, fix_date_time FROM timefix WHERE worklog_id IN (?)`, + { + replacements: [entryIds], + type: sequelize.QueryTypes.SELECT + } + ); + + // Erstelle eine Map: worklog_id -> Timefix + const timefixMap = new Map(); + timefixesForEntries.forEach(tf => { + if (!timefixMap.has(tf.worklog_id)) { + timefixMap.set(tf.worklog_id, []); + } + timefixMap.get(tf.worklog_id).push(tf); + }); + + // Formatiere die Einträge für das Frontend + const formattedEntries = entries.map(entry => { + // Prüfe ob es eine Korrektur gibt + const timefixForEntry = timefixMap.get(entry.id); + + let hours, minutes, action; + + if (timefixForEntry && timefixForEntry.length > 0) { + // Verwende korrigierte Werte aus Timefix + const tf = timefixForEntry[0]; + + if (typeof tf.fix_date_time === 'string') { + const [datePart, timePart] = tf.fix_date_time.split(' '); + [hours, minutes] = timePart.split(':').map(Number); + } else if (tf.fix_date_time instanceof Date) { + hours = tf.fix_date_time.getHours(); + minutes = tf.fix_date_time.getMinutes(); + } + + action = tf.fix_type; + } else { + // Keine Korrektur - verwende Original-Werte aus Worklog + if (typeof entry.tstamp === 'string') { + const [datePart, timePart] = entry.tstamp.split(' '); + [hours, minutes] = timePart.split(':').map(Number); + } else if (entry.tstamp instanceof Date) { + hours = entry.tstamp.getUTCHours(); + minutes = entry.tstamp.getUTCMinutes(); + } + + // Parse state + action = entry.state; + if (typeof action === 'string') { + try { + const parsed = JSON.parse(action); + action = parsed.action || action; + } catch (e) { + // action bleibt als String + } + } + } + + return { + id: entry.id, + time: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`, + action: action, + tstamp: entry.tstamp + }; + }).filter(e => e !== null); + + return formattedEntries; + } + + /** + * Holt alle Zeitkorrekturen für den heutigen Tag + * @param {number} userId - Benutzer-ID + * @returns {Promise} Array von Zeitkorrekturen + */ + async getTodayTimefixes(userId) { + const sequelize = database.sequelize; + + // Berechne Start und Ende des heutigen Tages (lokale Zeit) + const now = new Date(); + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); + const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); + + // Hole alle Timefixes für heute mit Raw SQL + const todayStartStr = `${todayStart.getFullYear()}-${String(todayStart.getMonth() + 1).padStart(2, '0')}-${String(todayStart.getDate()).padStart(2, '0')} 00:00:00`; + const todayEndStr = `${todayEnd.getFullYear()}-${String(todayEnd.getMonth() + 1).padStart(2, '0')}-${String(todayEnd.getDate()).padStart(2, '0')} 23:59:59`; + + const timefixes = await sequelize.query( + `SELECT id, user_id, worklog_id, fix_type, fix_date_time + FROM timefix + WHERE user_id = ? AND fix_date_time BETWEEN ? AND ? + ORDER BY fix_date_time DESC`, + { + replacements: [userId, todayStartStr, todayEndStr], + type: sequelize.QueryTypes.SELECT + } + ); + + // Hole die zugehörigen Worklog-Einträge separat mit Raw SQL + const worklogIds = timefixes.map(tf => tf.worklog_id); + + if (worklogIds.length === 0) { + return []; + } + + const worklogs = await sequelize.query( + `SELECT id, tstamp, state, relatedTo_id FROM worklog WHERE id IN (?)`, + { + replacements: [worklogIds], + type: sequelize.QueryTypes.SELECT + } + ); + + // Erstelle eine Map für schnellen Zugriff + const worklogMap = new Map(); + worklogs.forEach(wl => { + worklogMap.set(wl.id, wl); + }); + + // Formatiere die Daten für das Frontend + const formattedTimefixes = timefixes.map(tf => { + const worklog = worklogMap.get(tf.worklog_id); + + if (!worklog || !worklog.tstamp) { + console.error('Worklog oder tstamp fehlt für Timefix:', tf.id); + return null; + } + + // KORRIGIERTE Werte aus timefix.fix_date_time + let newDate, newHours, newMinutes; + + if (typeof tf.fix_date_time === 'string') { + const [datePart, timePart] = tf.fix_date_time.split(' '); + newDate = datePart; + [newHours, newMinutes] = timePart.split(':').map(Number); + } else if (tf.fix_date_time instanceof Date) { + const d = tf.fix_date_time; + newDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + newHours = d.getHours(); + newMinutes = d.getMinutes(); + } else { + console.error('Unbekannter fix_date_time-Typ:', typeof tf.fix_date_time); + return null; + } + + // ORIGINAL Werte aus worklog + let originalDate, originalHours, originalMinutes; + + if (typeof worklog.tstamp === 'string') { + const [datePart, timePart] = worklog.tstamp.split(' '); + originalDate = datePart; + [originalHours, originalMinutes] = timePart.split(':').map(Number); + } else if (worklog.tstamp instanceof Date) { + const d = worklog.tstamp; + originalDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + originalHours = d.getHours(); + originalMinutes = d.getMinutes(); + } else { + console.error('Unbekannter tstamp-Typ:', typeof worklog.tstamp); + return null; + } + + // Parse original action aus worklog.state + let originalAction = worklog.state; + if (typeof originalAction === 'string') { + try { + const parsed = JSON.parse(originalAction); + originalAction = parsed.action || originalAction; + } catch (e) { + // action bleibt als String + } + } + + return { + id: tf.id, + worklogId: worklog.id, + fixType: tf.fix_type, + fixDateTime: tf.fix_date_time, + // Neue Werte (KORRIGIERT - aus timefix.fix_date_time) + newDate: newDate, + newTime: `${newHours.toString().padStart(2, '0')}:${newMinutes.toString().padStart(2, '0')}`, + newAction: tf.fix_type, + // Original-Werte (aus worklog) + originalDate: originalDate, + originalTime: `${originalHours.toString().padStart(2, '0')}:${originalMinutes.toString().padStart(2, '0')}`, + originalAction: originalAction + }; + }).filter(tf => tf !== null); + + return formattedTimefixes; + } + + /** + * Erstellt eine neue Zeitkorrektur + * @param {number} userId - Benutzer-ID + * @param {number} worklogId - Worklog-Eintrag-ID + * @param {string} newDate - Neues Datum (YYYY-MM-DD) + * @param {string} newTime - Neue Uhrzeit (HH:MM) + * @param {string} newAction - Neue Aktion + * @returns {Promise} Erstellter Timefix + */ + async createTimefix(userId, worklogId, newDate, newTime, newAction) { + const { Timefix } = database.getModels(); + const sequelize = database.sequelize; + + // Hole den Original-Worklog-Eintrag mit Raw SQL + const worklogResult = await sequelize.query( + `SELECT id, user_id, tstamp, state FROM worklog WHERE id = ?`, + { + replacements: [worklogId], + type: sequelize.QueryTypes.SELECT + } + ); + + if (!worklogResult || worklogResult.length === 0) { + throw new Error('Worklog-Eintrag nicht gefunden'); + } + + const worklog = worklogResult[0]; + + // Prüfe Berechtigung + if (worklog.user_id !== userId) { + throw new Error('Keine Berechtigung für diesen Eintrag'); + } + + // Erstelle die korrigierte Zeit als Date-Objekt + const [newHours, newMinutes] = newTime.split(':').map(Number); + const [newYear, newMonth, newDay] = newDate.split('-').map(Number); + const correctedDateTime = new Date(newYear, newMonth - 1, newDay, newHours, newMinutes, 0); + + // Prüfe ob bereits ein Timefix existiert + const existingTimefix = await Timefix.findOne({ + where: { + worklog_id: worklogId, + fix_type: newAction + } + }); + + if (existingTimefix) { + // Update existierender Timefix + await existingTimefix.update({ + fix_date_time: correctedDateTime + }); + + return { + id: existingTimefix.id, + worklogId: existingTimefix.worklog_id, + fixType: existingTimefix.fix_type, + fixDateTime: existingTimefix.fix_date_time + }; + } + + // Erstelle neuen Timefix + const timefix = await Timefix.create({ + user_id: userId, + worklog_id: worklogId, + fix_type: newAction, + fix_date_time: correctedDateTime + }); + + return { + id: timefix.id, + worklogId: timefix.worklog_id, + fixType: timefix.fix_type, + fixDateTime: timefix.fix_date_time + }; + } + + /** + * Löscht eine Zeitkorrektur + * @param {number} userId - Benutzer-ID + * @param {number} timefixId - Timefix-ID + * @returns {Promise} + */ + async deleteTimefix(userId, timefixId) { + const { Timefix } = database.getModels(); + + const timefix = await Timefix.findByPk(timefixId); + + if (!timefix) { + throw new Error('Zeitkorrektur nicht gefunden'); + } + + // Prüfe Berechtigung + if (timefix.user_id !== userId) { + throw new Error('Keine Berechtigung für diese Zeitkorrektur'); + } + + await timefix.destroy(); + } +} + +module.exports = new TimefixService(); + diff --git a/backend/src/services/VacationService.js b/backend/src/services/VacationService.js new file mode 100644 index 0000000..1cf1498 --- /dev/null +++ b/backend/src/services/VacationService.js @@ -0,0 +1,120 @@ +const database = require('../config/database'); + +/** + * Service-Klasse für Urlaubseinträge + * Enthält die gesamte Business-Logik für Vacation + */ +class VacationService { + constructor() { + this.defaultUserId = 1; + } + + /** + * Holt alle Urlaubseinträge für einen User + * Zeigt nur aktuelles Jahr oder letzte 3 Monate (falls wir in den ersten 3 Monaten sind) + * @param {number} userId - Benutzer-ID + * @returns {Promise} Array von Urlaubseinträgen + */ + async getAllVacations(userId) { + const { Vacation } = database.getModels(); + const { Op } = require('sequelize'); + + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth(); // 0-11 + + let startDate; + + if (currentMonth < 3) { + // Erste 3 Monate des Jahres: Zeige letzte 3 Monate + const threeMonthsAgo = new Date(now); + threeMonthsAgo.setMonth(now.getMonth() - 3); + startDate = `${threeMonthsAgo.getFullYear()}-${String(threeMonthsAgo.getMonth() + 1).padStart(2, '0')}-01`; + } else { + // Rest des Jahres: Zeige nur aktuelles Jahr + startDate = `${currentYear}-01-01`; + } + + const vacations = await Vacation.findAll({ + where: { + user_id: userId, + first_day: { + [Op.gte]: startDate + } + }, + order: [['first_day', 'DESC']], + raw: true + }); + + // Formatiere die Daten + return vacations.map(vac => ({ + id: vac.id, + type: vac.vacation_type === 1 ? 'Halber Tag' : 'Zeitraum', + typeValue: vac.vacation_type, + startDate: vac.first_day, + endDate: vac.last_day + })); + } + + /** + * Erstellt einen neuen Urlaubseintrag + * @param {number} userId - Benutzer-ID + * @param {number} vacationType - Typ (0 = Zeitraum, 1 = Halber Tag) + * @param {string} startDate - Startdatum (YYYY-MM-DD) + * @param {string} endDate - Enddatum (YYYY-MM-DD) + * @returns {Promise} Erstellter Urlaubseintrag + */ + async createVacation(userId, vacationType, startDate, endDate) { + const { Vacation } = database.getModels(); + + // Bei "Halber Tag" muss startDate = endDate sein + let finalEndDate = endDate; + if (vacationType === 1) { + finalEndDate = startDate; + } else if (!endDate) { + throw new Error('Urlaubsende ist erforderlich für Zeitraum'); + } + + const vacation = await Vacation.create({ + user_id: userId, + first_day: startDate, + last_day: finalEndDate, + vacation_type: vacationType, + version: 0 + }); + + return { + id: vacation.id, + type: vacationType === 1 ? 'Halber Tag' : 'Zeitraum', + typeValue: vacation.vacation_type, + startDate: vacation.first_day, + endDate: vacation.last_day + }; + } + + /** + * Löscht einen Urlaubseintrag + * @param {number} userId - Benutzer-ID + * @param {number} vacationId - Vacation-ID + * @returns {Promise} + */ + async deleteVacation(userId, vacationId) { + const { Vacation } = database.getModels(); + + const vacation = await Vacation.findByPk(vacationId); + + if (!vacation) { + throw new Error('Urlaubseintrag nicht gefunden'); + } + + // Prüfe Berechtigung + if (vacation.user_id !== userId) { + throw new Error('Keine Berechtigung für diesen Eintrag'); + } + + await vacation.destroy(); + } +} + +module.exports = new VacationService(); + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b7b7138..22a0a8d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,17 +6,20 @@

Stechuhr

- @@ -44,19 +47,33 @@ + diff --git a/frontend/src/composables/useModal.js b/frontend/src/composables/useModal.js new file mode 100644 index 0000000..381fccf --- /dev/null +++ b/frontend/src/composables/useModal.js @@ -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 + } +} + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f2b6147..4f96b0d 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -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', diff --git a/frontend/src/views/Timefix.vue b/frontend/src/views/Timefix.vue new file mode 100644 index 0000000..b4b7a3b --- /dev/null +++ b/frontend/src/views/Timefix.vue @@ -0,0 +1,513 @@ + + + + + + diff --git a/frontend/src/views/Vacation.vue b/frontend/src/views/Vacation.vue new file mode 100644 index 0000000..6fc1e7c --- /dev/null +++ b/frontend/src/views/Vacation.vue @@ -0,0 +1,384 @@ + + + + + + diff --git a/frontend/src/views/WeekOverview.vue b/frontend/src/views/WeekOverview.vue index 921801b..938f0be 100644 --- a/frontend/src/views/WeekOverview.vue +++ b/frontend/src/views/WeekOverview.vue @@ -1,8 +1,6 @@