Features: - Backend: Node.js/Express mit MySQL/MariaDB - Frontend: Vue.js 3 mit Composition API - UTC-Zeithandling für korrekte Zeiterfassung - Timewish-basierte Überstundenberechnung - Wochenübersicht mit Urlaubs-/Krankheits-/Feiertagshandling - Bereinigtes Arbeitsende (Generell/Woche) - Überstunden-Offset für historische Daten - Fixed Layout mit scrollbarem Content - Kompakte UI mit grünem Theme
7.6 KiB
Datenbank-Integration
Übersicht
TimeClock v3 verwendet MySQL mit der bestehenden stechuhr2 Datenbankstruktur. Die Integration erfolgt über das Repository-Pattern für saubere Abstraktion.
Architektur
Controller
↓
Service (Business-Logik)
↓
Repository (Datenbankzugriff)
↓
MySQL Datenbank
Verwendete Tabellen
Haupttabellen
worklog - Zeiteinträge
CREATE TABLE `worklog` (
`id` bigint NOT NULL AUTO_INCREMENT,
`version` int NOT NULL,
`user_id` bigint NOT NULL,
`state` text NOT NULL, -- JSON mit {action, project, description}
`tstamp` datetime DEFAULT NULL,
`relatedTo_id` bigint DEFAULT NULL, -- Verknüpfung Clock In/Out
PRIMARY KEY (`id`)
)
Verwendung:
- Clock In: Neuer Eintrag mit
relatedTo_id = NULL - Clock Out: Neuer Eintrag mit
relatedTo_idverweist auf Clock In - State: JSON-Format
{"action": "Clock In", "project": "Projektname", "description": "Beschreibung"}
user - Benutzer
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`full_name` text NOT NULL,
`daily_hours` int NOT NULL,
`week_hours` int NOT NULL,
...
)
weekly_worktime - Wochenarbeitszeit
CREATE TABLE `weekly_worktime` (
`id` int NOT NULL AUTO_INCREMENT,
`weekly_work_time` double NOT NULL DEFAULT '40',
`starting_from` date DEFAULT NULL,
`ends_at` date DEFAULT NULL,
`user_id` int DEFAULT NULL,
...
)
Konfiguration
Umgebungsvariablen (.env)
# Datenbank-Verbindung
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=stechuhr2
Connection Pool
Die Datenbankverbindung wird über einen Connection Pool verwaltet:
// src/config/database.js
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 10
});
Repository-Pattern
WorklogRepository
Verantwortlichkeiten:
- CRUD-Operationen auf
worklogTabelle - Komplexe Queries (Paare, Statistiken)
- Datenbankzugriff abstrahieren
Hauptmethoden:
await worklogRepository.findAllByUser(userId)
await worklogRepository.findById(id)
await worklogRepository.create(worklogData)
await worklogRepository.update(id, updateData)
await worklogRepository.delete(id)
await worklogRepository.findPairsByUser(userId)
await worklogRepository.getStatistics(userId)
UserRepository
Verantwortlichkeiten:
- Benutzerverwaltung
- Wochenarbeitszeit abrufen
- Benutzereinstellungen
Hauptmethoden:
await userRepository.findById(userId)
await userRepository.findByEmail(email)
await userRepository.getWeeklyWorktime(userId, date)
await userRepository.getUserSetting(userId, settingId)
Datenfluss
Timer starten
1. Frontend → POST /api/time-entries
{ project: "Website", description: "Feature XY" }
2. Controller → Service.createEntry()
3. Service → WorklogRepository.create()
{
user_id: 1,
state: '{"action":"Clock In","project":"Website","description":"Feature XY"}',
tstamp: '2025-10-15 10:00:00',
relatedTo_id: null
}
4. MySQL → INSERT INTO worklog
→ Gibt neue ID zurück (z.B. 123)
5. Service → Konvertiert zu TimeEntry-Format
{
id: 123,
startTime: '2025-10-15T10:00:00',
endTime: null,
project: 'Website',
description: 'Feature XY',
duration: null,
isRunning: true
}
6. Controller → Frontend
Status 201, JSON Response
Timer stoppen
1. Frontend → PUT /api/time-entries/123
{ endTime: '2025-10-15T12:00:00' }
2. Service → WorklogRepository.create()
{
user_id: 1,
state: '{"action":"Clock Out","project":"Website","description":"Feature XY"}',
tstamp: '2025-10-15 12:00:00',
relatedTo_id: 123 // Verweist auf Clock In
}
3. Service → Berechnet Dauer und gibt Pair zurück
Worklog-Paare
Clock In und Clock Out werden als separate Einträge gespeichert, aber logisch als Paar behandelt:
SELECT
w1.id as start_id,
w1.tstamp as start_time,
w2.id as end_id,
w2.tstamp as end_time,
TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp) as duration
FROM worklog w1
LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id
WHERE w1.user_id = ? AND w1.relatedTo_id IS NULL
Vorteile:
- Historische Genauigkeit
- Korrekturen möglich
- Audit-Trail
State-Format
Der state Feld speichert JSON-Daten:
{
"action": "Clock In|Clock Out",
"project": "Projektname",
"description": "Beschreibung der Tätigkeit"
}
Parsing im Service:
let stateData;
try {
stateData = JSON.parse(worklog.state);
} catch {
stateData = { project: 'Allgemein', description: '' };
}
Statistiken
Basis-Statistiken
SELECT
COUNT(DISTINCT w1.id) as total_entries,
COUNT(DISTINCT w2.id) as completed_entries,
SUM(TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp)) as total_seconds
FROM worklog w1
LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id
WHERE w1.user_id = ? AND w1.relatedTo_id IS NULL
Projekt-Statistiken
Werden im Service berechnet durch Gruppierung der Paare nach Projekt.
Transaktionen
Für zukünftige Erweiterungen können Transaktionen verwendet werden:
const connection = await database.getPool().getConnection();
try {
await connection.beginTransaction();
// Multiple Operationen
await connection.execute(sql1, params1);
await connection.execute(sql2, params2);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
Migration von In-Memory
Der Code unterstützt beide Modi:
- Entwicklung: In-Memory (schnell, keine DB erforderlich)
- Produktion: MySQL (persistent, skalierbar)
Umschalten durch Änderung des Service-Imports.
Performance-Optimierungen
Indizes
Wichtige Indizes sind bereits vorhanden:
KEY `worklog_tstamp_IDX` (`tstamp`)
KEY `worklog_user_id_IDX` (`user_id`, `tstamp`)
Connection Pooling
- Wiederverwendung von Verbindungen
- Limit: 10 gleichzeitige Verbindungen
- Automatisches Reconnect
Query-Optimierung
- Prepared Statements (SQL Injection Schutz)
- WHERE-Klauseln mit Indizes
- LEFT JOIN statt mehrere Queries
Fehlerbehandlung
try {
const result = await worklogRepository.create(data);
return result;
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
throw new Error('Eintrag existiert bereits');
}
if (error.code === 'ER_NO_REFERENCED_ROW_2') {
throw new Error('Benutzer nicht gefunden');
}
throw error;
}
Backup & Restore
Backup erstellen
mysqldump -u root -p stechuhr2 > backup_$(date +%Y%m%d).sql
Restore
mysql -u root -p stechuhr2 < backup_20251015.sql
Sicherheit
✅ Implemented:
- Prepared Statements (SQL Injection Schutz)
- Connection Pooling
- Error Handling
🔄 TODO:
- Input Validation
- Rate Limiting
- User Authentication
- Row-Level Security
Monitoring
Logging von Queries (Development):
if (process.env.NODE_ENV === 'development') {
console.log('SQL:', sql);
console.log('Params:', params);
}
Zukünftige Erweiterungen
-
Authentifizierung
- Verwendung der
auth_infoTabelle - JWT-Token-basiert
- Verwendung der
-
Multi-User Support
- Benutzer-ID aus Session
- Row-Level Security
-
Erweiterte Features
- Urlaub (
vacationTabelle) - Krankmeldungen (
sickTabelle) - Feiertage (
holidayTabelle)
- Urlaub (
-
Caching
- Redis für häufige Abfragen
- Cache-Invalidierung
-
Read Replicas
- Master-Slave Setup
- Read-Write Splitting