diff --git a/public/js/api/apiClient.js b/public/js/api/apiClient.js deleted file mode 100644 index 9cd1db6..0000000 --- a/public/js/api/apiClient.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * API Client for backend communication - */ - -import { formatDateISO } from '../utils/dateUtils.js'; -import { showNotification } from '../ui/notifications.js'; - -/** - * Fetch entries from the backend - * @param {string|null} fromDate - Start date (YYYY-MM-DD) - * @param {string|null} toDate - End date (YYYY-MM-DD) - * @returns {Promise} - Array of entries - */ -export async function fetchEntries(fromDate = null, toDate = null) { - try { - let url = '/api/entries'; - const params = new URLSearchParams(); - - if (fromDate) params.append('from', fromDate); - if (toDate) params.append('to', toDate); - - if (params.toString()) { - url += '?' + params.toString(); - } - - const response = await fetch(url); - - if (!response.ok) { - throw new Error('Failed to fetch entries'); - } - - const entries = await response.json(); - return entries; - } catch (error) { - console.error('Error fetching entries:', error); - showNotification('Fehler beim Laden der Einträge', 'error'); - return []; - } -} - -/** - * Create a new entry - * @param {string} date - Date in DD.MM.YYYY format - * @param {string} startTime - Start time HH:MM - * @param {string} endTime - End time HH:MM - * @param {number|null} pauseMinutes - Pause in minutes - * @param {string} location - Location (office/home) - * @returns {Promise} - Created entry or null - */ -export async function createEntry(date, startTime, endTime, pauseMinutes, location) { - try { - const body = { - date: formatDateISO(date), - startTime, - endTime, - location: location || 'office' - }; - - // Only include pauseMinutes if explicitly provided (not empty) - if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') { - body.pauseMinutes = parseInt(pauseMinutes); - } - - const response = await fetch('/api/entries', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to create entry'); - } - - const entry = await response.json(); - return entry; - } catch (error) { - console.error('Error creating entry:', error); - showNotification(error.message || 'Fehler beim Erstellen des Eintrags', 'error'); - return null; - } -} - -/** - * Update an existing entry - * @param {number} id - Entry ID - * @param {string} date - Date in DD.MM.YYYY format - * @param {string} startTime - Start time HH:MM - * @param {string} endTime - End time HH:MM - * @param {number|null} pauseMinutes - Pause in minutes - * @param {string} location - Location (office/home) - * @returns {Promise} - Updated entry or null - */ -export async function updateEntry(id, date, startTime, endTime, pauseMinutes, location) { - try { - const body = { - date: formatDateISO(date), - startTime, - endTime, - location: location || 'office' - }; - - // Only include pauseMinutes if explicitly provided (not empty) - if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') { - body.pauseMinutes = parseInt(pauseMinutes); - } - - const response = await fetch(`/api/entries/${id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to update entry'); - } - - const entry = await response.json(); - return entry; - } catch (error) { - console.error('Error updating entry:', error); - showNotification(error.message || 'Fehler beim Aktualisieren des Eintrags', 'error'); - return null; - } -} - -/** - * Delete an entry - * @param {number} id - Entry ID - * @returns {Promise} - True if successful - */ -export async function deleteEntry(id) { - try { - const response = await fetch(`/api/entries/${id}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error('Failed to delete entry'); - } - - return true; - } catch (error) { - console.error('Error deleting entry:', error); - showNotification('Fehler beim Löschen des Eintrags', 'error'); - return false; - } -} - -/** - * Export entries as CSV - * @param {string|null} fromDate - Start date (YYYY-MM-DD) - * @param {string|null} toDate - End date (YYYY-MM-DD) - */ -export async function exportEntries(fromDate = null, toDate = null) { - try { - let url = '/api/export'; - const params = new URLSearchParams(); - - if (fromDate) params.append('from', fromDate); - if (toDate) params.append('to', toDate); - - if (params.toString()) { - url += '?' + params.toString(); - } - - const response = await fetch(url); - - if (!response.ok) { - throw new Error('Failed to export entries'); - } - - const blob = await response.blob(); - const downloadUrl = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = downloadUrl; - a.download = 'zeiterfassung.csv'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(downloadUrl); - - showNotification('Export erfolgreich', 'success'); - } catch (error) { - console.error('Error exporting entries:', error); - showNotification('Fehler beim Exportieren', 'error'); - } -} diff --git a/public/js/ui/notifications.js b/public/js/ui/notifications.js deleted file mode 100644 index 5a2f0a9..0000000 --- a/public/js/ui/notifications.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Toast notification system - */ - -/** - * Show toast notification - * @param {string} message - Message to display - * @param {string} type - Type of notification (success, error, info) - */ -export function showNotification(message, type = 'info') { - const container = document.getElementById('toastContainer'); - - // Create toast element - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - - // Icon based on type - const icons = { - success: '✓', - error: '✕', - info: 'ℹ' - }; - - toast.innerHTML = ` - ${icons[type] || 'ℹ'} - ${message} - `; - - container.appendChild(toast); - - // Auto-remove after 3 seconds - setTimeout(() => { - toast.classList.add('hiding'); - setTimeout(() => { - container.removeChild(toast); - }, 300); - }, 3000); -} diff --git a/public/js/utils/dateUtils.js b/public/js/utils/dateUtils.js deleted file mode 100644 index f81ef60..0000000 --- a/public/js/utils/dateUtils.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Date utility functions - */ - -/** - * Format date from YYYY-MM-DD to DD.MM.YYYY - * @param {string} dateStr - Date in YYYY-MM-DD format - * @returns {string} - Date in DD.MM.YYYY format - */ -export function formatDateDisplay(dateStr) { - const [year, month, day] = dateStr.split('-'); - return `${day}.${month}.${year}`; -} - -/** - * Format date from DD.MM.YYYY to YYYY-MM-DD - * @param {string} dateStr - Date in DD.MM.YYYY format - * @returns {string} - Date in YYYY-MM-DD format - */ -export function formatDateISO(dateStr) { - const [day, month, year] = dateStr.split('.'); - return `${year}-${month}-${day}`; -} - -/** - * Get today's date in YYYY-MM-DD format - * @returns {string} - Today's date - */ -export function getTodayISO() { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -/** - * Get day of week name in German - * @param {Date} date - Date object - * @returns {string} - German day name (Mo, Di, Mi, etc.) - */ -export function getDayOfWeek(date) { - const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; - return days[date.getDay()]; -} - -/** - * Get month name in German - * @param {number} monthIndex - Month index (0-11) - * @returns {string} - German month name - */ -export function getMonthName(monthIndex) { - const months = [ - 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', - 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' - ]; - return months[monthIndex]; -} - -/** - * Check if date is weekend or holiday - * @param {Date} date - Date object - * @returns {boolean} - True if weekend or holiday - */ -export function isWeekendOrHoliday(date) { - const dayOfWeek = date.getDay(); - return dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday -} diff --git a/public/js/utils/timeUtils.js b/public/js/utils/timeUtils.js deleted file mode 100644 index 307642f..0000000 --- a/public/js/utils/timeUtils.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Time utility functions - */ - -/** - * Round time down to nearest 15 minutes - * @param {Date} date - Date object - * @returns {Date} - Rounded date - */ -export function roundDownTo15Min(date) { - const minutes = date.getMinutes(); - const roundedMinutes = Math.floor(minutes / 15) * 15; - date.setMinutes(roundedMinutes); - date.setSeconds(0); - date.setMilliseconds(0); - return date; -} - -/** - * Round time up to nearest 15 minutes - * @param {Date} date - Date object - * @returns {Date} - Rounded date - */ -export function roundUpTo15Min(date) { - const minutes = date.getMinutes(); - const roundedMinutes = Math.ceil(minutes / 15) * 15; - date.setMinutes(roundedMinutes); - date.setSeconds(0); - date.setMilliseconds(0); - return date; -} - -/** - * Format time as HH:MM - * @param {Date} date - Date object - * @returns {string} - Time in HH:MM format - */ -export function formatTime(date) { - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${hours}:${minutes}`; -} - -/** - * Format seconds to HH:MM:SS - * @param {number} seconds - Duration in seconds - * @returns {string} - Formatted duration - */ -export function formatDuration(seconds) { - const hrs = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; -} diff --git a/src/config/database.js b/src/config/database.js deleted file mode 100644 index a8538be..0000000 --- a/src/config/database.js +++ /dev/null @@ -1,52 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const Database = require('better-sqlite3'); - -/** - * Initialize and configure the SQLite database - * @returns {Database} - Configured database instance - */ -function initializeDatabase() { - const dbPath = path.join(__dirname, '..', '..', 'db', 'timetracker.db'); - const schemaPath = path.join(__dirname, '..', '..', 'db', 'schema.sql'); - - // Ensure db directory exists - const dbDir = path.dirname(dbPath); - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - } - - const db = new Database(dbPath); - - // Create table if it doesn't exist - const schema = fs.readFileSync(schemaPath, 'utf8'); - db.exec(schema); - - // Run migrations - runMigrations(db); - - console.log('Database initialized successfully'); - return db; -} - -/** - * Run database migrations - * @param {Database} db - Database instance - */ -function runMigrations(db) { - // Migration: Add location column if it doesn't exist - try { - const tableInfo = db.pragma('table_info(entries)'); - const hasLocationColumn = tableInfo.some(col => col.name === 'location'); - - if (!hasLocationColumn) { - console.log('Adding location column to entries table...'); - db.exec(`ALTER TABLE entries ADD COLUMN location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home'))`); - console.log('Location column added successfully'); - } - } catch (error) { - console.error('Error during migration:', error); - } -} - -module.exports = { initializeDatabase }; diff --git a/src/routes/entries.js b/src/routes/entries.js deleted file mode 100644 index 5bd8f1a..0000000 --- a/src/routes/entries.js +++ /dev/null @@ -1,181 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { calculateNetHours } = require('../utils/timeCalculator'); - -/** - * Initialize routes with database instance - * @param {Database} db - SQLite database instance - * @returns {Router} - Configured Express router - */ -function createEntriesRouter(db) { - /** - * GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD - * Get all entries in a date range - */ - router.get('/', (req, res) => { - try { - const { from, to } = req.query; - - let query = 'SELECT * FROM entries'; - const params = []; - - if (from && to) { - query += ' WHERE date >= ? AND date <= ?'; - params.push(from, to); - } else if (from) { - query += ' WHERE date >= ?'; - params.push(from); - } else if (to) { - query += ' WHERE date <= ?'; - params.push(to); - } - - query += ' ORDER BY date DESC, start_time DESC'; - - const stmt = db.prepare(query); - const entries = stmt.all(...params); - - // Add calculated net hours to each entry - const enrichedEntries = entries.map(entry => { - const calculated = calculateNetHours(entry.start_time, entry.end_time, entry.pause_minutes); - return { - id: entry.id, - date: entry.date, - startTime: entry.start_time, - endTime: entry.end_time, - pauseMinutes: entry.pause_minutes, - netHours: calculated.netHours, - location: entry.location || 'office' - }; - }); - - res.json(enrichedEntries); - } catch (error) { - console.error('Error fetching entries:', error); - res.status(500).json({ error: 'Failed to fetch entries' }); - } - }); - - /** - * POST /api/entries - * Create a new entry - */ - router.post('/', (req, res) => { - try { - const { date, startTime, endTime, pauseMinutes, location } = req.body; - - if (!date || !startTime || !endTime) { - return res.status(400).json({ error: 'Missing required fields: date, startTime, endTime' }); - } - - // Calculate with auto-pause or use provided pause - const calculated = calculateNetHours(startTime, endTime, pauseMinutes); - const pause = calculated.pauseMinutes; - const loc = location || 'office'; - - try { - const stmt = db.prepare('INSERT INTO entries (date, start_time, end_time, pause_minutes, location) VALUES (?, ?, ?, ?, ?)'); - const result = stmt.run(date, startTime, endTime, pause, loc); - - // Return the created entry with calculated fields - const newEntry = { - id: result.lastInsertRowid, - date, - startTime, - endTime, - pauseMinutes: pause, - netHours: calculated.netHours, - location: loc - }; - - res.status(201).json(newEntry); - } catch (dbError) { - // Check for UNIQUE constraint violation - if (dbError.message.includes('UNIQUE constraint failed')) { - return res.status(409).json({ error: 'Ein Eintrag für dieses Datum existiert bereits' }); - } - throw dbError; - } - } catch (error) { - console.error('Error creating entry:', error); - res.status(500).json({ error: 'Failed to create entry' }); - } - }); - - /** - * PUT /api/entries/:id - * Update an existing entry - */ - router.put('/:id', (req, res) => { - try { - const { id } = req.params; - const { date, startTime, endTime, pauseMinutes, location } = req.body; - - if (!date || !startTime || !endTime) { - return res.status(400).json({ error: 'Missing required fields: date, startTime, endTime' }); - } - - // Calculate with auto-pause or use provided pause - const calculated = calculateNetHours(startTime, endTime, pauseMinutes); - const pause = calculated.pauseMinutes; - const loc = location || 'office'; - - try { - const stmt = db.prepare('UPDATE entries SET date = ?, start_time = ?, end_time = ?, pause_minutes = ?, location = ? WHERE id = ?'); - const result = stmt.run(date, startTime, endTime, pause, loc, id); - - if (result.changes === 0) { - return res.status(404).json({ error: 'Entry not found' }); - } - - // Return the updated entry with calculated fields - const updatedEntry = { - id: parseInt(id), - date, - startTime, - endTime, - pauseMinutes: pause, - netHours: calculated.netHours, - location: loc - }; - - res.json(updatedEntry); - } catch (dbError) { - // Check for UNIQUE constraint violation - if (dbError.message.includes('UNIQUE constraint failed')) { - return res.status(409).json({ error: 'Ein Eintrag für dieses Datum existiert bereits' }); - } - throw dbError; - } - } catch (error) { - console.error('Error updating entry:', error); - res.status(500).json({ error: 'Failed to update entry' }); - } - }); - - /** - * DELETE /api/entries/:id - * Delete an entry - */ - router.delete('/:id', (req, res) => { - try { - const { id } = req.params; - - const stmt = db.prepare('DELETE FROM entries WHERE id = ?'); - const result = stmt.run(id); - - if (result.changes === 0) { - return res.status(404).json({ error: 'Entry not found' }); - } - - res.json({ message: 'Entry deleted successfully' }); - } catch (error) { - console.error('Error deleting entry:', error); - res.status(500).json({ error: 'Failed to delete entry' }); - } - }); - - return router; -} - -module.exports = createEntriesRouter; diff --git a/src/routes/export.js b/src/routes/export.js deleted file mode 100644 index 1eb2a0b..0000000 --- a/src/routes/export.js +++ /dev/null @@ -1,66 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { calculateNetHours } = require('../utils/timeCalculator'); - -/** - * Initialize export routes with database instance - * @param {Database} db - SQLite database instance - * @returns {Router} - Configured Express router - */ -function createExportRouter(db) { - /** - * GET /api/export?from=YYYY-MM-DD&to=YYYY-MM-DD - * Export entries as CSV - */ - router.get('/', (req, res) => { - try { - const { from, to } = req.query; - - let query = 'SELECT * FROM entries'; - const params = []; - - if (from && to) { - query += ' WHERE date >= ? AND date <= ?'; - params.push(from, to); - } else if (from) { - query += ' WHERE date >= ?'; - params.push(from); - } else if (to) { - query += ' WHERE date <= ?'; - params.push(to); - } - - query += ' ORDER BY date ASC, start_time ASC'; - - const stmt = db.prepare(query); - const entries = stmt.all(...params); - - // Generate CSV with German formatting - let csv = 'Datum,Startzeit,Endzeit,Pause in Minuten,Gesamtstunden\n'; - - entries.forEach(entry => { - const calculated = calculateNetHours(entry.start_time, entry.end_time, entry.pause_minutes); - - // Format date as DD.MM.YYYY - const [year, month, day] = entry.date.split('-'); - const formattedDate = `${day}.${month}.${year}`; - - // Use comma as decimal separator for hours - const netHoursFormatted = calculated.netHours.toFixed(2).replace('.', ','); - - csv += `${formattedDate},${entry.start_time},${entry.end_time},${entry.pause_minutes},${netHoursFormatted}\n`; - }); - - res.setHeader('Content-Type', 'text/csv; charset=utf-8'); - res.setHeader('Content-Disposition', 'attachment; filename="zeiterfassung.csv"'); - res.send(csv); - } catch (error) { - console.error('Error exporting entries:', error); - res.status(500).json({ error: 'Failed to export entries' }); - } - }); - - return router; -} - -module.exports = createExportRouter; diff --git a/src/utils/timeCalculator.js b/src/utils/timeCalculator.js deleted file mode 100644 index 80661a2..0000000 --- a/src/utils/timeCalculator.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Auto-calculate pause based on German break rules - * @param {number} grossHours - Total work hours - * @returns {number} - Pause in minutes - */ -function calculateAutoPause(grossHours) { - if (grossHours > 9) { - return 45; - } else if (grossHours > 6) { - return 30; - } - return 0; -} - -/** - * Calculate net hours with pause and 10-hour cap - * @param {string} startTime - Format: "HH:MM" - * @param {string} endTime - Format: "HH:MM" - * @param {number|null} pauseMinutes - Manual pause in minutes (null for auto-calculation) - * @returns {object} - { grossHours, pauseMinutes, netHours } - */ -function calculateNetHours(startTime, endTime, pauseMinutes = null) { - const [startHour, startMin] = startTime.split(':').map(Number); - const [endHour, endMin] = endTime.split(':').map(Number); - - const startTotalMin = startHour * 60 + startMin; - const endTotalMin = endHour * 60 + endMin; - - // Handle overnight shifts - let diffMin = endTotalMin - startTotalMin; - if (diffMin < 0) { - diffMin += 24 * 60; // Add 24 hours - } - - const grossHours = diffMin / 60; - - // Calculate required minimum pause based on gross hours - const requiredMinPause = calculateAutoPause(grossHours); - - // Determine actual pause to use - let actualPause; - if (pauseMinutes !== null && pauseMinutes !== undefined) { - // Manual pause provided - enforce minimum - actualPause = Math.max(pauseMinutes, requiredMinPause); - } else { - // No pause provided - use required minimum - actualPause = requiredMinPause; - } - - // Calculate net hours - const netMinutes = diffMin - actualPause; - let netHours = netMinutes / 60; - - // Cap at 10 hours - if (netHours > 10) { - netHours = 10.0; - } - - return { - grossHours: parseFloat(grossHours.toFixed(2)), - pauseMinutes: actualPause, - netHours: parseFloat(netHours.toFixed(2)) - }; -} - -module.exports = { - calculateAutoPause, - calculateNetHours -};