From 06176350b8305ab00188c6ad056e561ec4701165 Mon Sep 17 00:00:00 2001 From: maggot Date: Thu, 23 Oct 2025 02:04:54 +0200 Subject: [PATCH] server.js aktualisiert --- server.js | 331 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 324 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index 9006f72..f064772 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ const express = require('express'); -const { initializeDatabase } = require('./src/config/database'); -const createEntriesRouter = require('./src/routes/entries'); -const createExportRouter = require('./src/routes/export'); +const path = require('path'); +const fs = require('fs'); +const Database = require('better-sqlite3'); const app = express(); const PORT = process.env.PORT || 3000; @@ -11,11 +11,328 @@ app.use(express.json()); app.use(express.static('public')); // Initialize Database -const db = initializeDatabase(); +const dbPath = path.join(__dirname, 'db', 'timetracker.db'); +const schemaPath = path.join(__dirname, 'db', 'schema.sql'); -// Mount Routes -app.use('/api/entries', createEntriesRouter(db)); -app.use('/api/export', createExportRouter(db)); +// 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); + +// 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); +} + +console.log('Database initialized successfully'); + +// ============================================ +// BUSINESS LOGIC: Net Hours Calculation & Caps +// ============================================ + +/** + * 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)) + }; +} + +// ============================================ +// API ENDPOINTS +// ============================================ + +/** + * GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD + * Get all entries in a date range + */ +app.get('/api/entries', (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 + */ +app.post('/api/entries', (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 + */ +app.put('/api/entries/: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 + */ +app.delete('/api/entries/: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' }); + } +}); + +/** + * GET /api/export?from=YYYY-MM-DD&to=YYYY-MM-DD + * Export entries as CSV + */ +app.get('/api/export', (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' }); + } +}); // Start server app.listen(PORT, () => {