Revert "Update README.md with installation instructions and Docker usage; add docker-compose.yml for service orchestration"

This reverts commit c8c2a800bb.
This commit is contained in:
Felix Schlusche
2025-10-23 01:54:25 +02:00
parent 31c156d157
commit a09a9b5820
8 changed files with 0 additions and 721 deletions

View File

@@ -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>} - 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<Object|null>} - 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<Object|null>} - 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<boolean>} - 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');
}
}

View File

@@ -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 = `
<span class="toast-icon">${icons[type] || ''}</span>
<span>${message}</span>
`;
container.appendChild(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, 3000);
}

View File

@@ -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
}

View File

@@ -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')}`;
}

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
};