Initial commit: TimeClock v3 - Node.js/Vue.js Zeiterfassung

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
This commit is contained in:
Torsten Schulz (local)
2025-10-17 14:11:28 +02:00
commit e95bb4cb76
86 changed files with 19530 additions and 0 deletions

565
backend/SEQUELIZE.md Normal file
View File

@@ -0,0 +1,565 @@
# Sequelize ORM Integration
## Übersicht
TimeClock v3 verwendet **Sequelize** als ORM (Object-Relational Mapping) für die MySQL-Datenbank. Dies ermöglicht eine typsichere, objektorientierte Arbeitsweise mit der Datenbank.
## Vorteile von Sequelize
**Type-Safety** - Models definieren Datentypen
**Validierung** - Automatische Datenvalidierung
**Assoziationen** - Einfache Beziehungen zwischen Models
**Migrations** - Versionskontrolle für Datenbankschema
**Abstraktion** - Datenbank-unabhängiger Code
**Query Builder** - Typsichere Queries
## Konfiguration
### .env Datei
Alle Datenbankeinstellungen werden über `.env` konfiguriert:
```env
# MySQL Datenbank-Verbindung
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=ihr_passwort
DB_NAME=stechuhr2
# Datenbank-Optionen
DB_LOGGING=false # SQL-Queries in Console loggen (true/false)
DB_TIMEZONE=+01:00 # Timezone für Timestamps
DB_POOL_MAX=10 # Max. Connections im Pool
DB_POOL_MIN=0 # Min. Connections im Pool
```
### Initialisierung
```javascript
// src/config/database.js
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mysql',
logging: process.env.DB_LOGGING === 'true',
pool: {
max: parseInt(process.env.DB_POOL_MAX),
min: parseInt(process.env.DB_POOL_MIN)
}
}
);
```
## Models
### Model-Struktur
Jedes Model repräsentiert eine Datenbanktabelle:
```javascript
// src/models/User.js
const { Model, DataTypes } = require('sequelize');
class User extends Model {
static initialize(sequelize) {
User.init(
{
id: {
type: DataTypes.BIGINT,
primaryKey: true,
autoIncrement: true
},
full_name: {
type: DataTypes.TEXT,
allowNull: false
}
// ... weitere Felder
},
{
sequelize,
tableName: 'user',
timestamps: false
}
);
return User;
}
}
```
### Verfügbare Models
| Model | Tabelle | Beschreibung |
|-------|---------|--------------|
| **User** | `user` | Benutzer |
| **Worklog** | `worklog` | Zeiteinträge (Clock In/Out) |
| **AuthInfo** | `auth_info` | Authentifizierung |
| **State** | `state` | Bundesländer/Regionen |
| **WeeklyWorktime** | `weekly_worktime` | Wochenarbeitszeit |
| **Holiday** | `holiday` | Feiertage |
| **Vacation** | `vacation` | Urlaub |
| **Sick** | `sick` | Krankmeldungen |
| **SickType** | `sick_type` | Krankmeldungstypen |
### Model-Features
#### 1. Getter/Setter
```javascript
// Worklog Model
class Worklog extends Model {
// State als JSON speichern/laden
state: {
type: DataTypes.TEXT,
get() {
const rawValue = this.getDataValue('state');
try {
return JSON.parse(rawValue);
} catch {
return { action: rawValue };
}
},
set(value) {
if (typeof value === 'object') {
this.setDataValue('state', JSON.stringify(value));
}
}
}
}
// Verwendung:
worklog.state = { action: 'Clock In', project: 'Website' };
console.log(worklog.state.project); // 'Website'
```
#### 2. Instance Methods
```javascript
class Worklog extends Model {
isClockIn() {
return this.relatedTo_id === null;
}
getProject() {
return this.state.project || 'Allgemein';
}
}
// Verwendung:
if (worklog.isClockIn()) {
console.log(`Projekt: ${worklog.getProject()}`);
}
```
#### 3. Class Methods
```javascript
class User extends Model {
getFullName() {
return this.full_name;
}
}
```
## Assoziationen
Models sind miteinander verknüpft:
```javascript
// User → Worklog (1:n)
User.hasMany(Worklog, { foreignKey: 'user_id', as: 'worklogs' });
Worklog.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
// Worklog → Worklog (Clock In/Out)
Worklog.belongsTo(Worklog, { foreignKey: 'relatedTo_id', as: 'relatedTo' });
Worklog.hasOne(Worklog, { foreignKey: 'relatedTo_id', as: 'clockOut' });
// User → AuthInfo (1:1)
User.hasOne(AuthInfo, { foreignKey: 'user_id', as: 'authInfo' });
AuthInfo.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
```
### Include in Queries
```javascript
// User mit Worklogs laden
const user = await User.findByPk(1, {
include: [{
model: Worklog,
as: 'worklogs',
limit: 10
}]
});
console.log(user.worklogs); // Array von Worklog-Instanzen
```
## Repository-Pattern
Repositories verwenden Sequelize Models:
```javascript
class WorklogRepository {
async findById(id) {
const { Worklog } = database.getModels();
return await Worklog.findByPk(id);
}
async create(data) {
const { Worklog } = database.getModels();
return await Worklog.create(data);
}
async update(id, updates) {
const { Worklog } = database.getModels();
const worklog = await Worklog.findByPk(id);
await worklog.update(updates);
return worklog;
}
}
```
## Query-Beispiele
### Einfache Queries
```javascript
// Alle Benutzer
const users = await User.findAll();
// Benutzer nach ID
const user = await User.findByPk(1);
// Erstellen
const newUser = await User.create({
full_name: 'Max Mustermann',
daily_hours: 8
});
// Aktualisieren
await user.update({ full_name: 'Max M.' });
// Löschen
await user.destroy();
```
### WHERE-Bedingungen
```javascript
const { Op } = require('sequelize');
// Einfache WHERE
const worklogs = await Worklog.findAll({
where: { user_id: 1 }
});
// Operatoren
const recent = await Worklog.findAll({
where: {
tstamp: {
[Op.gte]: new Date('2025-01-01')
}
}
});
// Mehrere Bedingungen
const active = await Worklog.findAll({
where: {
user_id: 1,
relatedTo_id: null
}
});
```
### Order & Limit
```javascript
const latest = await Worklog.findAll({
where: { user_id: 1 },
order: [['tstamp', 'DESC']],
limit: 10,
offset: 0
});
```
### Aggregationen
```javascript
// COUNT
const count = await Worklog.count({
where: { user_id: 1 }
});
// SUM (mit raw query)
const [result] = await sequelize.query(`
SELECT SUM(TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp)) as total
FROM worklog w1
JOIN worklog w2 ON w2.relatedTo_id = w1.id
WHERE w1.user_id = ?
`, {
replacements: [1],
type: QueryTypes.SELECT
});
```
## Raw Queries
Für komplexe Queries können Raw SQL verwendet werden:
```javascript
const sequelize = database.getSequelize();
const [results] = await sequelize.query(`
SELECT
w1.id as start_id,
w2.id as end_id,
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 = :userId
`, {
replacements: { userId: 1 },
type: sequelize.QueryTypes.SELECT
});
```
## Transaktionen
```javascript
const sequelize = database.getSequelize();
const t = await sequelize.transaction();
try {
// Clock In erstellen
const clockIn = await Worklog.create({
user_id: 1,
state: { action: 'Clock In' },
tstamp: new Date()
}, { transaction: t });
// Weitere Operationen...
await t.commit();
} catch (error) {
await t.rollback();
throw error;
}
```
## Validierung
```javascript
class User extends Model {
static initialize(sequelize) {
User.init({
email: {
type: DataTypes.STRING,
validate: {
isEmail: true,
notEmpty: true
}
},
full_name: {
type: DataTypes.TEXT,
validate: {
len: [2, 255]
}
},
daily_hours: {
type: DataTypes.INTEGER,
validate: {
min: 1,
max: 24
}
}
}, {
sequelize,
tableName: 'user'
});
}
}
```
## Hooks (Lifecycle Events)
```javascript
class User extends Model {
static initialize(sequelize) {
User.init({ /* ... */ }, {
sequelize,
tableName: 'user',
hooks: {
beforeCreate: (user, options) => {
user.last_change = new Date();
},
beforeUpdate: (user, options) => {
user.version += 1;
user.last_change = new Date();
}
}
});
}
}
```
## Migrations (Optional)
Für Datenbankschema-Änderungen:
```bash
# Sequelize CLI installieren
npm install --save-dev sequelize-cli
# Initialisieren
npx sequelize-cli init
# Migration erstellen
npx sequelize-cli migration:generate --name add-user-field
# Migrations ausführen
npx sequelize-cli db:migrate
# Migrations rückgängig machen
npx sequelize-cli db:migrate:undo
```
## Best Practices
### 1. Model-Zugriff über Database
```javascript
// ✅ Gut
const { Worklog } = database.getModels();
const worklogs = await Worklog.findAll();
// ❌ Nicht
const Worklog = require('../models/Worklog');
```
### 2. Eager Loading für Beziehungen
```javascript
// ✅ Gut - Eine Query
const user = await User.findByPk(1, {
include: [{ model: Worklog, as: 'worklogs' }]
});
// ❌ Nicht - N+1 Problem
const user = await User.findByPk(1);
const worklogs = await Worklog.findAll({ where: { user_id: user.id } });
```
### 3. Transactions für kritische Operationen
```javascript
// ✅ Gut
const t = await sequelize.transaction();
try {
await operation1({ transaction: t });
await operation2({ transaction: t });
await t.commit();
} catch (error) {
await t.rollback();
}
```
### 4. Raw Queries für Performance
```javascript
// Für komplexe Aggregationen sind Raw Queries oft schneller
const stats = await sequelize.query(complexSQL, {
replacements: { userId },
type: QueryTypes.SELECT
});
```
## Debugging
### SQL-Queries anzeigen
```env
# In .env
DB_LOGGING=true
```
Dann werden alle SQL-Queries in der Console geloggt:
```
Executing (default): SELECT * FROM `user` WHERE `id` = 1;
```
### Model-Daten anzeigen
```javascript
console.log(user.toJSON());
```
## Performance-Tipps
1. **Indizes nutzen** - Models definieren Indizes
2. **Eager Loading** - Include statt separate Queries
3. **Pagination** - Limit & Offset verwenden
4. **Raw Queries** - Für komplexe Aggregationen
5. **Connection Pooling** - Bereits konfiguriert
## Troubleshooting
### "Sequelize not initialized"
```javascript
// Sicherstellen, dass database.initialize() aufgerufen wurde
await database.initialize();
```
### "Model not found"
```javascript
// Models werden in database.js initialisiert
// Prüfen Sie, ob Model in initializeModels() geladen wird
```
### Timezone-Probleme
```env
# In .env die richtige Timezone setzen
DB_TIMEZONE=+01:00
```
## Migration von Raw SQL
### Vorher (Raw SQL):
```javascript
const [rows] = await db.execute(
'SELECT * FROM user WHERE id = ?',
[userId]
);
```
### Nachher (Sequelize):
```javascript
const user = await User.findByPk(userId);
```
Viel einfacher und typsicher! 🎉
## Zusammenfassung
Sequelize bietet:
- ✅ Typsichere Models
- ✅ Automatische Validierung
- ✅ Einfache Assoziationen
- ✅ Query Builder
- ✅ .env-basierte Konfiguration
- ✅ Migrations-Support
Perfekt für professionelle Node.js-Anwendungen!