BROKEN
This commit is contained in:
@@ -104,26 +104,41 @@ class LDAPService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wert eines LDAP-Attributs extrahieren
|
* Wert eines LDAP-Attributs extrahieren
|
||||||
|
*
|
||||||
|
* Die ldapjs-Bibliothek behandelt UTF-8-Zeichen automatisch korrekt.
|
||||||
|
* Diese Funktion stellt sicher, dass UTF-8-Zeichen wie ß, ä, ö, ü korrekt zurückgegeben werden.
|
||||||
*/
|
*/
|
||||||
static getAttributeValue(entry, attributeName) {
|
static getAttributeValue(entry, attributeName) {
|
||||||
const attr = entry.attributes.find(a => a.type === attributeName);
|
const attr = entry.attributes.find(a => a.type === attributeName);
|
||||||
if (!attr) {
|
if (!attr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return Array.isArray(attr.values) ? attr.values[0] : attr.values;
|
const value = Array.isArray(attr.values) ? attr.values[0] : attr.values;
|
||||||
|
// Stelle sicher, dass der Wert als String zurückgegeben wird (UTF-8 wird automatisch korrekt behandelt)
|
||||||
|
return value != null ? String(value) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection)
|
* Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection)
|
||||||
|
*
|
||||||
|
* WICHTIG: UTF-8-Zeichen wie ß, ä, ö, ü müssen NICHT escaped werden.
|
||||||
|
* LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515.
|
||||||
|
* Nur die speziellen LDAP-Filter-Zeichen werden escaped.
|
||||||
*/
|
*/
|
||||||
static escapeLDAPFilter(value) {
|
static escapeLDAPFilter(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
return value
|
|
||||||
.replace(/\\/g, '\\5c')
|
// Stelle sicher, dass der Wert als String behandelt wird
|
||||||
.replace(/\*/g, '\\2a')
|
const str = String(value);
|
||||||
.replace(/\(/g, '\\28')
|
|
||||||
.replace(/\)/g, '\\29')
|
// Escape nur die speziellen LDAP-Filter-Zeichen
|
||||||
.replace(/\0/g, '\\00');
|
// UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet
|
||||||
|
return str
|
||||||
|
.replace(/\\/g, '\\5c') // Backslash
|
||||||
|
.replace(/\*/g, '\\2a') // Stern
|
||||||
|
.replace(/\(/g, '\\28') // Öffnende Klammer
|
||||||
|
.replace(/\)/g, '\\29') // Schließende Klammer
|
||||||
|
.replace(/\0/g, '\\00'); // Null-Byte
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,12 +160,14 @@ class LDAPService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ldapUser = ldapUsers[index];
|
const ldapUser = ldapUsers[index];
|
||||||
const username = ldapUser.username.trim();
|
// .trim() behält UTF-8-Zeichen wie ß, ä, ö, ü korrekt bei
|
||||||
const firstname = ldapUser.firstname.trim();
|
// Stelle sicher, dass Werte als String behandelt werden
|
||||||
const lastname = ldapUser.lastname.trim();
|
const username = String(ldapUser.username || '').trim();
|
||||||
|
const firstname = String(ldapUser.firstname || '').trim();
|
||||||
|
const lastname = String(ldapUser.lastname || '').trim();
|
||||||
|
|
||||||
// Prüfe ob Benutzer bereits existiert
|
// Prüfe ob Benutzer bereits existiert (case-insensitive)
|
||||||
db.get('SELECT id, role FROM users WHERE username = ?', [username], (err, existingUser) => {
|
db.get('SELECT id, role FROM users WHERE username = ? COLLATE NOCASE', [username], (err, existingUser) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
errors.push(`Fehler beim Prüfen von ${username}: ${err.message}`);
|
errors.push(`Fehler beim Prüfen von ${username}: ${err.message}`);
|
||||||
errorCount++;
|
errorCount++;
|
||||||
@@ -158,9 +175,9 @@ class LDAPService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
// Benutzer existiert - aktualisiere nur Name, behalte Rolle
|
// Benutzer existiert - aktualisiere nur Name, behalte Rolle (case-insensitive)
|
||||||
db.run(
|
db.run(
|
||||||
'UPDATE users SET firstname = ?, lastname = ? WHERE username = ?',
|
'UPDATE users SET firstname = ?, lastname = ? WHERE username = ? COLLATE NOCASE',
|
||||||
[firstname, lastname, username],
|
[firstname, lastname, username],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -232,8 +249,18 @@ class LDAPService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Benutzer gegen LDAP authentifizieren
|
* Benutzer gegen LDAP authentifizieren
|
||||||
|
*
|
||||||
|
* Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen.
|
||||||
|
* Die ldapjs-Bibliothek behandelt UTF-8 automatisch korrekt.
|
||||||
*/
|
*/
|
||||||
static authenticate(username, password, callback) {
|
static authenticate(username, password, callback) {
|
||||||
|
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
|
||||||
|
const usernameStr = String(username || '').trim();
|
||||||
|
|
||||||
|
if (!usernameStr) {
|
||||||
|
return callback(new Error('Benutzername darf nicht leer sein'), false);
|
||||||
|
}
|
||||||
|
|
||||||
// Konfiguration abrufen
|
// Konfiguration abrufen
|
||||||
this.getConfig((err, config) => {
|
this.getConfig((err, config) => {
|
||||||
if (err || !config || !config.enabled) {
|
if (err || !config || !config.enabled) {
|
||||||
@@ -249,7 +276,8 @@ class LDAPService {
|
|||||||
// Suche nach dem Benutzer in LDAP
|
// Suche nach dem Benutzer in LDAP
|
||||||
const baseDN = config.base_dn || '';
|
const baseDN = config.base_dn || '';
|
||||||
const usernameAttr = config.username_attribute || 'cn';
|
const usernameAttr = config.username_attribute || 'cn';
|
||||||
const escapedUsername = this.escapeLDAPFilter(username);
|
// escapeLDAPFilter behandelt UTF-8-Zeichen korrekt (escaped sie nicht)
|
||||||
|
const escapedUsername = this.escapeLDAPFilter(usernameStr);
|
||||||
const searchFilter = `(${usernameAttr}=${escapedUsername})`;
|
const searchFilter = `(${usernameAttr}=${escapedUsername})`;
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
filter: searchFilter,
|
filter: searchFilter,
|
||||||
@@ -262,7 +290,9 @@ class LDAPService {
|
|||||||
client.search(baseDN, searchOptions, (err, res) => {
|
client.search(baseDN, searchOptions, (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
client.unbind();
|
client.unbind();
|
||||||
return callback(err, false);
|
// Verbesserte Fehlermeldung für mögliche Encoding-Probleme
|
||||||
|
const errorMsg = err.message || String(err);
|
||||||
|
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
res.on('searchEntry', (entry) => {
|
||||||
@@ -271,7 +301,8 @@ class LDAPService {
|
|||||||
|
|
||||||
res.on('error', (err) => {
|
res.on('error', (err) => {
|
||||||
client.unbind();
|
client.unbind();
|
||||||
callback(err, false);
|
const errorMsg = err.message || String(err);
|
||||||
|
callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', (result) => {
|
res.on('end', (result) => {
|
||||||
@@ -279,7 +310,8 @@ class LDAPService {
|
|||||||
client.unbind();
|
client.unbind();
|
||||||
|
|
||||||
if (!userDN) {
|
if (!userDN) {
|
||||||
return callback(new Error('Benutzer nicht gefunden'), false);
|
// Verbesserte Fehlermeldung: Hinweis auf mögliche Encoding-Probleme
|
||||||
|
return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versuche, sich mit den Benutzer-Credentials zu binden
|
// Versuche, sich mit den Benutzer-Credentials zu binden
|
||||||
@@ -297,7 +329,8 @@ class LDAPService {
|
|||||||
authClient.bind(userDN, password, (err) => {
|
authClient.bind(userDN, password, (err) => {
|
||||||
authClient.unbind();
|
authClient.unbind();
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(new Error('Ungültiges Passwort'), false);
|
const errorMsg = err.message || String(err);
|
||||||
|
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
|
||||||
}
|
}
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,19 @@ body {
|
|||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn:disabled,
|
||||||
|
.btn[disabled] {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: #ecf0f1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled:hover,
|
||||||
|
.btn[disabled]:hover {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #3498db;
|
background-color: #3498db;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -100,6 +113,19 @@ body {
|
|||||||
background-color: #229954;
|
background-color: #229954;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-success:disabled,
|
||||||
|
.btn-success[disabled] {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: #ecf0f1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:disabled:hover,
|
||||||
|
.btn-success[disabled]:hover {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background-color: #e74c3c;
|
background-color: #e74c3c;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -322,6 +348,10 @@ body {
|
|||||||
border-left-color: #27ae60;
|
border-left-color: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card.stat-planned {
|
||||||
|
border-left-color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|||||||
@@ -36,6 +36,28 @@ async function loadUserStats() {
|
|||||||
if (totalVacationEl) {
|
if (totalVacationEl) {
|
||||||
totalVacationEl.textContent = (stats.urlaubstage || 0).toFixed(1);
|
totalVacationEl.textContent = (stats.urlaubstage || 0).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verplante Urlaubstage anzeigen
|
||||||
|
const plannedVacationEl = document.getElementById('plannedVacation');
|
||||||
|
if (plannedVacationEl) {
|
||||||
|
plannedVacationEl.textContent = (stats.plannedVacationDays || 0).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kalenderwochen anzeigen
|
||||||
|
const plannedWeeksEl = document.getElementById('plannedWeeks');
|
||||||
|
if (plannedWeeksEl) {
|
||||||
|
if (stats.plannedWeeks && stats.plannedWeeks.length > 0) {
|
||||||
|
const weeksHTML = stats.plannedWeeks.map(w => {
|
||||||
|
const daysText = w.days === 1 ? '1 Tag' : `${w.days.toFixed(1)} Tage`;
|
||||||
|
return `${w.year} KW${String(w.week).padStart(2, '0')} (${daysText})`;
|
||||||
|
}).join('<br>');
|
||||||
|
plannedWeeksEl.innerHTML = weeksHTML;
|
||||||
|
plannedWeeksEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
plannedWeeksEl.textContent = '';
|
||||||
|
plannedWeeksEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Statistiken:', error);
|
console.error('Fehler beim Laden der Statistiken:', error);
|
||||||
// Fehlerbehandlung: Zeige "-" oder "Fehler"
|
// Fehlerbehandlung: Zeige "-" oder "Fehler"
|
||||||
@@ -45,6 +67,13 @@ async function loadUserStats() {
|
|||||||
if (remainingVacationEl) remainingVacationEl.textContent = '-';
|
if (remainingVacationEl) remainingVacationEl.textContent = '-';
|
||||||
const totalVacationEl = document.getElementById('totalVacation');
|
const totalVacationEl = document.getElementById('totalVacation');
|
||||||
if (totalVacationEl) totalVacationEl.textContent = '-';
|
if (totalVacationEl) totalVacationEl.textContent = '-';
|
||||||
|
const plannedVacationEl = document.getElementById('plannedVacation');
|
||||||
|
if (plannedVacationEl) plannedVacationEl.textContent = '-';
|
||||||
|
const plannedWeeksEl = document.getElementById('plannedWeeks');
|
||||||
|
if (plannedWeeksEl) {
|
||||||
|
plannedWeeksEl.textContent = '';
|
||||||
|
plannedWeeksEl.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +84,28 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
const response = await fetch('/api/user/last-week');
|
const response = await fetch('/api/user/last-week');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.last_week_start) {
|
if (data.last_week_start) {
|
||||||
currentWeekStart = data.last_week_start;
|
// Prüfe ob last_week_start wirklich ein Montag ist
|
||||||
|
if (isMonday(data.last_week_start)) {
|
||||||
|
currentWeekStart = data.last_week_start;
|
||||||
|
} else {
|
||||||
|
// Korrigiere zu Montag falls nicht
|
||||||
|
console.warn('last_week_start war kein Montag, korrigiere:', data.last_week_start);
|
||||||
|
currentWeekStart = getMonday(data.last_week_start);
|
||||||
|
// Speichere die korrigierte Woche
|
||||||
|
saveLastWeek();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Konnte letzte Woche nicht vom Server laden:', error);
|
console.warn('Konnte letzte Woche nicht vom Server laden:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stelle sicher, dass currentWeekStart immer ein Montag ist
|
||||||
|
if (currentWeekStart && !isMonday(currentWeekStart)) {
|
||||||
|
console.warn('currentWeekStart war kein Montag, korrigiere:', currentWeekStart);
|
||||||
|
currentWeekStart = getMonday(currentWeekStart);
|
||||||
|
saveLastWeek();
|
||||||
|
}
|
||||||
|
|
||||||
// Ping-IP laden
|
// Ping-IP laden
|
||||||
loadPingIP();
|
loadPingIP();
|
||||||
|
|
||||||
@@ -70,17 +115,23 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
loadWeek();
|
loadWeek();
|
||||||
|
|
||||||
document.getElementById('prevWeek').addEventListener('click', function() {
|
document.getElementById('prevWeek').addEventListener('click', function() {
|
||||||
const date = new Date(currentWeekStart);
|
// Parse als lokales Datum
|
||||||
|
const parts = currentWeekStart.split('-');
|
||||||
|
const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
date.setDate(date.getDate() - 7);
|
date.setDate(date.getDate() - 7);
|
||||||
currentWeekStart = formatDate(date);
|
// Stelle sicher, dass es ein Montag ist
|
||||||
|
currentWeekStart = getMonday(date);
|
||||||
saveLastWeek();
|
saveLastWeek();
|
||||||
loadWeek();
|
loadWeek();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('nextWeek').addEventListener('click', function() {
|
document.getElementById('nextWeek').addEventListener('click', function() {
|
||||||
const date = new Date(currentWeekStart);
|
// Parse als lokales Datum
|
||||||
|
const parts = currentWeekStart.split('-');
|
||||||
|
const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
date.setDate(date.getDate() + 7);
|
date.setDate(date.getDate() + 7);
|
||||||
currentWeekStart = formatDate(date);
|
// Stelle sicher, dass es ein Montag ist
|
||||||
|
currentWeekStart = getMonday(date);
|
||||||
saveLastWeek();
|
saveLastWeek();
|
||||||
loadWeek();
|
loadWeek();
|
||||||
});
|
});
|
||||||
@@ -140,13 +191,36 @@ async function saveLastWeek() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Montag der aktuellen Woche ermitteln
|
// Prüft ob ein Datum ein Montag ist (1 = Montag)
|
||||||
|
function isMonday(dateStr) {
|
||||||
|
const d = new Date(dateStr + 'T00:00:00'); // Lokale Zeit verwenden, nicht UTC
|
||||||
|
return d.getDay() === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Montag der aktuellen Woche ermitteln (robust gegen Zeitzonenprobleme)
|
||||||
function getMonday(date) {
|
function getMonday(date) {
|
||||||
const d = new Date(date);
|
// Wenn date bereits ein String ist (YYYY-MM-DD), parsen wir es als lokales Datum
|
||||||
const day = d.getDay();
|
let d;
|
||||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
if (typeof date === 'string') {
|
||||||
d.setDate(diff);
|
// Parse als lokales Datum, nicht UTC
|
||||||
return formatDate(d);
|
const parts = date.split('-');
|
||||||
|
d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
} else {
|
||||||
|
d = new Date(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stelle sicher, dass wir mit lokaler Zeit arbeiten
|
||||||
|
const day = d.getDay(); // 0 = Sonntag, 1 = Montag, ..., 6 = Samstag
|
||||||
|
// Berechne Differenz zum Montag: Montag = 1, also diff = 1 - day
|
||||||
|
// Aber: Sonntag = 0, also für Sonntag: diff = 1 - 0 = 1, aber wir wollen -6 Tage zurück
|
||||||
|
const diff = day === 0 ? -6 : 1 - day;
|
||||||
|
d.setDate(d.getDate() + diff);
|
||||||
|
|
||||||
|
// Format als YYYY-MM-DD in lokaler Zeit
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dayOfMonth = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${dayOfMonth}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kalenderwoche berechnen (ISO 8601 - Woche beginnt Montag)
|
// Kalenderwoche berechnen (ISO 8601 - Woche beginnt Montag)
|
||||||
@@ -277,7 +351,12 @@ function renderWeek() {
|
|||||||
|
|
||||||
// Prüfen ob Werktag (Montag-Freitag, i < 5) ausgefüllt ist
|
// Prüfen ob Werktag (Montag-Freitag, i < 5) ausgefüllt ist
|
||||||
// Bei ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt
|
// Bei ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt
|
||||||
if (i < 5 && vacationType !== 'full' && !sickStatus && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) {
|
// Bei 8 Überstunden (ganzer Tag) gilt der Tag auch als ausgefüllt
|
||||||
|
const overtimeValue = overtimeTaken ? parseFloat(overtimeTaken) : 0;
|
||||||
|
const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8;
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
||||||
|
|
||||||
|
if (i < 5 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) {
|
||||||
allWeekdaysFilled = false;
|
allWeekdaysFilled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +603,8 @@ function updateOvertimeDisplay() {
|
|||||||
// Gesamtstunden berechnen - direkt aus DOM-Elementen lesen für Echtzeit-Aktualisierung
|
// Gesamtstunden berechnen - direkt aus DOM-Elementen lesen für Echtzeit-Aktualisierung
|
||||||
let totalHours = 0;
|
let totalHours = 0;
|
||||||
let vacationHours = 0;
|
let vacationHours = 0;
|
||||||
|
const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8;
|
||||||
|
let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden (wie im Backend)
|
||||||
const startDateObj = new Date(startDate);
|
const startDateObj = new Date(startDate);
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const date = new Date(startDateObj);
|
const date = new Date(startDateObj);
|
||||||
@@ -536,6 +617,16 @@ function updateOvertimeDisplay() {
|
|||||||
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
||||||
const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false);
|
const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false);
|
||||||
|
|
||||||
|
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
|
||||||
|
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
|
||||||
|
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (currentEntries[dateStr]?.overtime_taken_hours ? parseFloat(currentEntries[dateStr].overtime_taken_hours) : 0);
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
||||||
|
|
||||||
|
// Zähle volle Überstundentage (wie im Backend)
|
||||||
|
if (isFullDayOvertime) {
|
||||||
|
fullDayOvertimeDays++;
|
||||||
|
}
|
||||||
|
|
||||||
// Wenn Urlaub oder Krank, zähle nur diese Stunden (nicht zusätzlich Arbeitsstunden)
|
// Wenn Urlaub oder Krank, zähle nur diese Stunden (nicht zusätzlich Arbeitsstunden)
|
||||||
if (vacationType === 'full') {
|
if (vacationType === 'full') {
|
||||||
vacationHours += 8; // Ganzer Tag Urlaub = 8 Stunden
|
vacationHours += 8; // Ganzer Tag Urlaub = 8 Stunden
|
||||||
@@ -550,37 +641,43 @@ function updateOvertimeDisplay() {
|
|||||||
const endTime = endInput ? endInput.value : '';
|
const endTime = endInput ? endInput.value : '';
|
||||||
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
|
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
|
||||||
|
|
||||||
if (startTime && endTime) {
|
if (startTime && endTime && !isFullDayOvertime) {
|
||||||
const start = new Date(`2000-01-01T${startTime}`);
|
const start = new Date(`2000-01-01T${startTime}`);
|
||||||
const end = new Date(`2000-01-01T${endTime}`);
|
const end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
||||||
totalHours += hours;
|
totalHours += hours;
|
||||||
} else if (currentEntries[dateStr]?.total_hours) {
|
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
|
||||||
// Fallback auf gespeicherte Werte
|
// Fallback auf gespeicherte Werte
|
||||||
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
|
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
|
||||||
}
|
}
|
||||||
} else if (sickStatus) {
|
} else if (sickStatus) {
|
||||||
totalHours += 8; // Krank = 8 Stunden
|
totalHours += 8; // Krank = 8 Stunden
|
||||||
} else {
|
} else {
|
||||||
// Berechne Stunden direkt aus Start-/Endzeit und Pause
|
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden
|
||||||
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
if (isFullDayOvertime) {
|
||||||
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
|
// Tag zählt als 0 Stunden (Überstunden werden separat abgezogen)
|
||||||
const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`);
|
// totalHours bleibt unverändert (0 Stunden für diesen Tag)
|
||||||
|
} else {
|
||||||
|
// Berechne Stunden direkt aus Start-/Endzeit und Pause
|
||||||
|
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
||||||
|
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
|
||||||
|
const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`);
|
||||||
|
|
||||||
const startTime = startInput ? startInput.value : '';
|
const startTime = startInput ? startInput.value : '';
|
||||||
const endTime = endInput ? endInput.value : '';
|
const endTime = endInput ? endInput.value : '';
|
||||||
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
|
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
|
||||||
|
|
||||||
if (startTime && endTime) {
|
if (startTime && endTime) {
|
||||||
const start = new Date(`2000-01-01T${startTime}`);
|
const start = new Date(`2000-01-01T${startTime}`);
|
||||||
const end = new Date(`2000-01-01T${endTime}`);
|
const end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
||||||
totalHours += hours;
|
totalHours += hours;
|
||||||
} else if (currentEntries[dateStr]?.total_hours) {
|
} else if (currentEntries[dateStr]?.total_hours) {
|
||||||
// Fallback auf gespeicherte Werte
|
// Fallback auf gespeicherte Werte
|
||||||
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
|
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,9 +698,15 @@ function updateOvertimeDisplay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Sollstunden
|
// Überstunden berechnen (wie im Backend: mit adjustedSollStunden)
|
||||||
|
// Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden
|
||||||
|
// Die negativen Stunden (wegen 0 statt Sollstunden) werden durch die verbrauchten Überstunden ausgeglichen
|
||||||
|
// Daher: adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours)
|
||||||
|
// So werden die Tage mit 8 Überstunden nicht zu negativen Überstunden führen
|
||||||
const totalHoursWithVacation = totalHours + vacationHours;
|
const totalHoursWithVacation = totalHours + vacationHours;
|
||||||
const overtimeHours = totalHoursWithVacation - sollStunden;
|
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
||||||
|
// overtimeHours = Überstunden diese Woche (wie im Backend berechnet)
|
||||||
|
const overtimeHours = totalHoursWithVacation - adjustedSollStunden;
|
||||||
|
|
||||||
// Überstunden-Anzeige aktualisieren
|
// Überstunden-Anzeige aktualisieren
|
||||||
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
|
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
|
||||||
@@ -828,6 +931,11 @@ async function saveEntry(input) {
|
|||||||
// Überstunden-Anzeige aktualisieren (bei jeder Änderung)
|
// Überstunden-Anzeige aktualisieren (bei jeder Änderung)
|
||||||
updateOvertimeDisplay();
|
updateOvertimeDisplay();
|
||||||
|
|
||||||
|
// Wenn vacation_type geändert wurde, Statistiken aktualisieren (für verplante Tage)
|
||||||
|
if (input.dataset.field === 'vacation_type') {
|
||||||
|
loadUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
// Submit-Button Status prüfen (nach jedem Speichern)
|
// Submit-Button Status prüfen (nach jedem Speichern)
|
||||||
checkWeekComplete();
|
checkWeekComplete();
|
||||||
|
|
||||||
@@ -873,6 +981,18 @@ function checkWeekComplete() {
|
|||||||
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt)
|
||||||
|
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
|
||||||
|
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0);
|
||||||
|
|
||||||
|
// Berechne fullDayHours (normalerweise 8 Stunden)
|
||||||
|
const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8;
|
||||||
|
|
||||||
|
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt
|
||||||
|
if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
|
||||||
|
continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag)
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle)
|
// Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle)
|
||||||
// Auch bei manueller Eingabe werden die Werte hier erkannt
|
// Auch bei manueller Eingabe werden die Werte hier erkannt
|
||||||
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
||||||
@@ -883,15 +1003,18 @@ function checkWeekComplete() {
|
|||||||
const endTime = endInput ? (endInput.value || '').trim() : '';
|
const endTime = endInput ? (endInput.value || '').trim() : '';
|
||||||
|
|
||||||
// Debug-Ausgabe - zeigt auch den tatsächlichen Wert im Input-Feld
|
// Debug-Ausgabe - zeigt auch den tatsächlichen Wert im Input-Feld
|
||||||
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}"`, {
|
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, {
|
||||||
startInputExists: !!startInput,
|
startInputExists: !!startInput,
|
||||||
endInputExists: !!endInput,
|
endInputExists: !!endInput,
|
||||||
startInputValue: startInput ? startInput.value : 'N/A',
|
startInputValue: startInput ? startInput.value : 'N/A',
|
||||||
endInputValue: endInput ? endInput.value : 'N/A',
|
endInputValue: endInput ? endInput.value : 'N/A',
|
||||||
vacationValue: vacationValue
|
vacationValue: vacationValue,
|
||||||
|
overtimeValue: overtimeValue,
|
||||||
|
fullDayHours: fullDayHours
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
|
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
|
||||||
|
// (außer wenn 8 Überstunden eingetragen sind, dann sind Start/Ende nicht nötig)
|
||||||
if (!startTime || !endTime || startTime === '' || endTime === '') {
|
if (!startTime || !endTime || startTime === '' || endTime === '') {
|
||||||
allWeekdaysFilled = false;
|
allWeekdaysFilled = false;
|
||||||
missingFields.push(formatDateDE(dateStr));
|
missingFields.push(formatDateDE(dateStr));
|
||||||
@@ -979,6 +1102,16 @@ async function submitWeek() {
|
|||||||
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig)
|
||||||
|
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
|
||||||
|
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0);
|
||||||
|
const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8;
|
||||||
|
|
||||||
|
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt
|
||||||
|
if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
|
||||||
|
continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag)
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe IMMER direkt die Input-Felder im DOM - auch bei manueller Eingabe
|
// Prüfe IMMER direkt die Input-Felder im DOM - auch bei manueller Eingabe
|
||||||
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
|
||||||
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
|
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
|
||||||
@@ -988,13 +1121,15 @@ async function submitWeek() {
|
|||||||
const endTime = endInput ? (endInput.value || '').trim() : '';
|
const endTime = endInput ? (endInput.value || '').trim() : '';
|
||||||
|
|
||||||
// Debug-Ausgabe mit detaillierten Informationen
|
// Debug-Ausgabe mit detaillierten Informationen
|
||||||
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}"`, {
|
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, {
|
||||||
startInputExists: !!startInput,
|
startInputExists: !!startInput,
|
||||||
endInputExists: !!endInput,
|
endInputExists: !!endInput,
|
||||||
startInputValue: startInput ? `"${startInput.value}"` : 'N/A',
|
startInputValue: startInput ? `"${startInput.value}"` : 'N/A',
|
||||||
endInputValue: endInput ? `"${endInput.value}"` : 'N/A',
|
endInputValue: endInput ? `"${endInput.value}"` : 'N/A',
|
||||||
startInputType: startInput ? typeof startInput.value : 'N/A',
|
startInputType: startInput ? typeof startInput.value : 'N/A',
|
||||||
vacationValue: vacationValue
|
vacationValue: vacationValue,
|
||||||
|
overtimeValue: overtimeValue,
|
||||||
|
fullDayHours: fullDayHours
|
||||||
});
|
});
|
||||||
|
|
||||||
const missing = [];
|
const missing = [];
|
||||||
@@ -1294,6 +1429,57 @@ async function loadPingIP() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client-IP ermitteln und eintragen (global für onclick)
|
||||||
|
window.detectClientIP = async function() {
|
||||||
|
const pingIpInput = document.getElementById('pingIpInput');
|
||||||
|
const detectButton = document.querySelector('button[onclick*="detectClientIP"]');
|
||||||
|
|
||||||
|
if (!pingIpInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button-Status während des Ladens
|
||||||
|
if (detectButton) {
|
||||||
|
const originalText = detectButton.textContent;
|
||||||
|
detectButton.textContent = 'Ermittle...';
|
||||||
|
detectButton.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/client-ip');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Abrufen der IP-Adresse');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.client_ip && data.client_ip !== 'unknown') {
|
||||||
|
// IP in das Eingabefeld eintragen
|
||||||
|
pingIpInput.value = data.client_ip;
|
||||||
|
|
||||||
|
// Erfolgs-Feedback
|
||||||
|
detectButton.textContent = 'IP ermittelt!';
|
||||||
|
detectButton.style.backgroundColor = '#27ae60';
|
||||||
|
setTimeout(() => {
|
||||||
|
detectButton.textContent = originalText;
|
||||||
|
detectButton.style.backgroundColor = '#3498db';
|
||||||
|
detectButton.disabled = false;
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
alert('IP-Adresse konnte nicht ermittelt werden.');
|
||||||
|
detectButton.textContent = originalText;
|
||||||
|
detectButton.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Ermitteln der Client-IP:', error);
|
||||||
|
alert('Fehler beim Ermitteln der IP-Adresse');
|
||||||
|
if (detectButton) {
|
||||||
|
detectButton.textContent = originalText;
|
||||||
|
detectButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Ping-IP speichern (global für onclick)
|
// Ping-IP speichern (global für onclick)
|
||||||
window.savePingIP = async function() {
|
window.savePingIP = async function() {
|
||||||
const pingIpInput = document.getElementById('pingIpInput');
|
const pingIpInput = document.getElementById('pingIpInput');
|
||||||
|
|||||||
256
reset-db.js
256
reset-db.js
@@ -2,28 +2,218 @@ const sqlite3 = require('sqlite3').verbose();
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
const dbPath = path.join(__dirname, 'stundenerfassung.db');
|
const dbPath = path.join(__dirname, 'stundenerfassung.db');
|
||||||
|
|
||||||
console.log('🔄 Setze Datenbank zurück...\n');
|
console.log('🔄 Setze Datenbank zurück...\n');
|
||||||
|
|
||||||
// Datenbank schließen falls offen
|
// Datenbank schließen falls offen
|
||||||
let db = null;
|
let db = null;
|
||||||
|
let savedLdapConfig = [];
|
||||||
|
|
||||||
try {
|
// Hilfsfunktion zum Warten
|
||||||
// Prüfe ob Datenbank existiert
|
function sleep(ms) {
|
||||||
if (fs.existsSync(dbPath)) {
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
console.log('📁 Datenbankdatei gefunden, lösche sie...');
|
}
|
||||||
fs.unlinkSync(dbPath);
|
|
||||||
console.log('✅ Datenbankdatei gelöscht\n');
|
// Funktion zum Prüfen und Beenden von Prozessen auf bestimmten Ports
|
||||||
} else {
|
async function checkAndKillPorts(ports) {
|
||||||
console.log('ℹ️ Datenbankdatei existiert nicht, erstelle neue...\n');
|
const killedProcesses = [];
|
||||||
|
|
||||||
|
for (const port of ports) {
|
||||||
|
try {
|
||||||
|
// Prüfe, ob der Port belegt ist (Windows)
|
||||||
|
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
// Extrahiere PID aus der Ausgabe
|
||||||
|
const lines = stdout.trim().split('\n');
|
||||||
|
const pids = new Set();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
const pid = parts[parts.length - 1];
|
||||||
|
if (pid && !isNaN(pid)) {
|
||||||
|
pids.add(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beende alle Prozesse, die den Port verwenden
|
||||||
|
for (const pid of pids) {
|
||||||
|
try {
|
||||||
|
console.log(`🛑 Beende Prozess ${pid} auf Port ${port}...`);
|
||||||
|
await execAsync(`taskkill /F /PID ${pid}`);
|
||||||
|
killedProcesses.push({ port, pid });
|
||||||
|
console.log(`✅ Prozess ${pid} beendet`);
|
||||||
|
} catch (err) {
|
||||||
|
// Prozess könnte bereits beendet sein oder keine Berechtigung
|
||||||
|
if (!err.message.includes('not found') && !err.message.includes('not running')) {
|
||||||
|
console.log(`⚠️ Konnte Prozess ${pid} nicht beenden: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Port ist nicht belegt oder netstat hat nichts gefunden
|
||||||
|
if (!err.message.includes('findstr') && !err.message.includes('not found')) {
|
||||||
|
// Ignoriere Fehler, wenn der Port nicht belegt ist
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Datenbank erstellen
|
if (killedProcesses.length > 0) {
|
||||||
db = new sqlite3.Database(dbPath);
|
console.log(`\n✅ ${killedProcesses.length} Prozess(e) beendet\n`);
|
||||||
|
// Warte kurz, damit die Ports freigegeben werden
|
||||||
|
await sleep(1000);
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Keine Prozesse auf Ports 3333 oder 3334 gefunden\n');
|
||||||
|
}
|
||||||
|
|
||||||
db.serialize(() => {
|
return killedProcesses.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Löschen der Datenbankdatei mit Retry-Logik (async)
|
||||||
|
async function deleteDatabaseFile(retries = 10, initialDelay = 500) {
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(dbPath);
|
||||||
|
console.log('✅ Datenbankdatei gelöscht\n');
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EBUSY' && i < retries - 1) {
|
||||||
|
// Exponentielle Backoff-Strategie
|
||||||
|
const waitTime = initialDelay * Math.pow(2, i);
|
||||||
|
console.log(`⏳ Datenbankdatei noch gesperrt (Versuch ${i + 1}/${retries}), warte ${waitTime}ms...`);
|
||||||
|
await sleep(waitTime);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i === retries - 1) {
|
||||||
|
console.error(`❌ Konnte Datenbankdatei nach ${retries} Versuchen nicht löschen.`);
|
||||||
|
console.error(' Bitte stellen Sie sicher, dass alle Prozesse geschlossen sind, die die Datenbank verwenden.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promise-Wrapper für sqlite3 Database.all
|
||||||
|
function dbAll(db, query, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(query, params, (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promise-Wrapper für sqlite3 Database.close
|
||||||
|
function dbClose(db) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.close((err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promise-Wrapper für sqlite3 Database-Konstruktor
|
||||||
|
function openDatabase(path, mode = sqlite3.OPEN_READONLY) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const db = new sqlite3.Database(path, mode, (err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(db);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hauptfunktion als async
|
||||||
|
async function resetDatabase() {
|
||||||
|
try {
|
||||||
|
// Prüfe und beende Prozesse auf Ports 3333 und 3334
|
||||||
|
console.log('🔍 Prüfe auf laufende Server auf Ports 3333 und 3334...\n');
|
||||||
|
await checkAndKillPorts([3333, 3334]);
|
||||||
|
|
||||||
|
// Prüfe ob Datenbank existiert und sichere ldap_config Daten
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
console.log('📁 Datenbankdatei gefunden...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Temporäre Datenbankverbindung zum Lesen der ldap_config
|
||||||
|
const tempDb = await openDatabase(dbPath, sqlite3.OPEN_READONLY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Lese ldap_config Daten
|
||||||
|
const rows = await dbAll(tempDb, 'SELECT * FROM ldap_config');
|
||||||
|
if (rows && rows.length > 0) {
|
||||||
|
savedLdapConfig = rows;
|
||||||
|
console.log(`💾 ${rows.length} LDAP-Konfiguration(en) gesichert\n`);
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Keine LDAP-Konfiguration vorhanden\n');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('ℹ️ ldap_config Tabelle existiert nicht oder konnte nicht gelesen werden\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließe die temporäre Datenbank
|
||||||
|
await dbClose(tempDb);
|
||||||
|
|
||||||
|
// Warte etwas länger, damit die Datenbank wirklich geschlossen ist
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Datenbank löschen
|
||||||
|
const success = await deleteDatabaseFile();
|
||||||
|
if (success) {
|
||||||
|
createNewDatabase();
|
||||||
|
} else {
|
||||||
|
console.error('❌ Konnte Datenbankdatei nicht löschen');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('⚠️ Konnte Datenbank nicht öffnen zum Lesen, fahre fort...\n');
|
||||||
|
// Datenbank löschen
|
||||||
|
const success = await deleteDatabaseFile();
|
||||||
|
if (success) {
|
||||||
|
createNewDatabase();
|
||||||
|
} else {
|
||||||
|
console.error('❌ Konnte Datenbankdatei nicht löschen');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Datenbankdatei existiert nicht, erstelle neue...\n');
|
||||||
|
createNewDatabase();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Zurücksetzen der Datenbank:', error);
|
||||||
|
if (db) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starte das Reset
|
||||||
|
resetDatabase().catch((error) => {
|
||||||
|
console.error('❌ Unerwarteter Fehler:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function createNewDatabase() {
|
||||||
|
// Neue Datenbank erstellen
|
||||||
|
db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
console.log('📊 Erstelle Tabellen...\n');
|
console.log('📊 Erstelle Tabellen...\n');
|
||||||
|
|
||||||
// Benutzer-Tabelle
|
// Benutzer-Tabelle
|
||||||
@@ -126,7 +316,42 @@ try {
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`, (err) => {
|
)`, (err) => {
|
||||||
if (err) console.error('Fehler bei ldap_config:', err);
|
if (err) console.error('Fehler bei ldap_config:', err);
|
||||||
else console.log('✅ Tabelle ldap_config erstellt');
|
else {
|
||||||
|
console.log('✅ Tabelle ldap_config erstellt');
|
||||||
|
|
||||||
|
// Stelle gesicherte LDAP-Konfiguration wieder her
|
||||||
|
if (savedLdapConfig.length > 0) {
|
||||||
|
console.log('🔄 Stelle LDAP-Konfiguration wieder her...');
|
||||||
|
savedLdapConfig.forEach((config) => {
|
||||||
|
db.run(`INSERT INTO ldap_config (
|
||||||
|
id, enabled, url, bind_dn, bind_password, base_dn,
|
||||||
|
user_search_filter, username_attribute, firstname_attribute,
|
||||||
|
lastname_attribute, sync_interval, last_sync, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
||||||
|
config.id,
|
||||||
|
config.enabled,
|
||||||
|
config.url,
|
||||||
|
config.bind_dn,
|
||||||
|
config.bind_password,
|
||||||
|
config.base_dn,
|
||||||
|
config.user_search_filter,
|
||||||
|
config.username_attribute,
|
||||||
|
config.firstname_attribute,
|
||||||
|
config.lastname_attribute,
|
||||||
|
config.sync_interval,
|
||||||
|
config.last_sync,
|
||||||
|
config.created_at,
|
||||||
|
config.updated_at
|
||||||
|
], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Wiederherstellen der LDAP-Konfiguration:', err);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ LDAP-Konfiguration (ID: ${config.id}) wiederhergestellt`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// LDAP-Sync-Log-Tabelle
|
// LDAP-Sync-Log-Tabelle
|
||||||
@@ -217,11 +442,4 @@ try {
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler beim Zurücksetzen der Datenbank:', error);
|
|
||||||
if (db) {
|
|
||||||
db.close();
|
|
||||||
}
|
}
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ function registerAuthRoutes(app) {
|
|||||||
LDAPService.authenticate(username, password, (authErr, authSuccess) => {
|
LDAPService.authenticate(username, password, (authErr, authSuccess) => {
|
||||||
if (authErr || !authSuccess) {
|
if (authErr || !authSuccess) {
|
||||||
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
|
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
|
||||||
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
||||||
|
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,8 @@ function registerAuthRoutes(app) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank
|
// LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank
|
||||||
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
||||||
|
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
|
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,8 @@ function registerAuthRoutes(app) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// LDAP nicht aktiviert - verwende lokale Authentifizierung
|
// LDAP nicht aktiviert - verwende lokale Authentifizierung
|
||||||
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
||||||
|
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,12 +190,12 @@ function registerTimesheetRoutes(app) {
|
|||||||
const { week_start, week_end, version_reason } = req.body;
|
const { week_start, week_end, version_reason } = req.body;
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
|
||||||
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
|
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
|
||||||
db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, updated_at FROM timesheet_entries
|
db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, overtime_taken_hours, updated_at FROM timesheet_entries
|
||||||
WHERE user_id = ? AND date >= ? AND date <= ?
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
||||||
ORDER BY date, updated_at DESC, id DESC`,
|
ORDER BY date, updated_at DESC, id DESC`,
|
||||||
[userId, week_start, week_end],
|
[userId, week_start, week_end],
|
||||||
(err, entries) => {
|
(err, entries) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' });
|
return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' });
|
||||||
}
|
}
|
||||||
@@ -221,81 +221,100 @@ function registerTimesheetRoutes(app) {
|
|||||||
// Prüfe nur Werktage (Montag-Freitag, erste 5 Tage)
|
// Prüfe nur Werktage (Montag-Freitag, erste 5 Tage)
|
||||||
// Samstag und Sonntag sind optional
|
// Samstag und Sonntag sind optional
|
||||||
// Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten
|
// Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten
|
||||||
|
// Bei 8 Überstunden (ganzer Tag) ist der Tag auch als ausgefüllt zu betrachten
|
||||||
// week_start ist bereits im Format YYYY-MM-DD
|
// week_start ist bereits im Format YYYY-MM-DD
|
||||||
const startDateParts = week_start.split('-');
|
const startDateParts = week_start.split('-');
|
||||||
const startYear = parseInt(startDateParts[0]);
|
const startYear = parseInt(startDateParts[0]);
|
||||||
const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert
|
const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert
|
||||||
const startDay = parseInt(startDateParts[2]);
|
const startDay = parseInt(startDateParts[2]);
|
||||||
|
|
||||||
let missingDays = [];
|
// User-Daten laden für Überstunden-Berechnung
|
||||||
|
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
for (let i = 0; i < 5; i++) {
|
if (err) {
|
||||||
// Datum direkt berechnen ohne Zeitzonenprobleme
|
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
|
||||||
const date = new Date(startYear, startMonth, startDay + i);
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
|
||||||
const entry = entriesByDate[dateStr];
|
|
||||||
|
|
||||||
// Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
|
||||||
const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true);
|
|
||||||
if (entry && (entry.vacation_type === 'full' || isSick)) {
|
|
||||||
continue; // Tag ist ausgefüllt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
|
const wochenstunden = user?.wochenstunden || 0;
|
||||||
// start_time und end_time könnten null, undefined oder leer strings sein
|
const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8;
|
||||||
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
|
|
||||||
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
|
|
||||||
|
|
||||||
if (!entry || !hasStartTime || !hasEndTime) {
|
let missingDays = [];
|
||||||
missingDays.push(dateStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingDays.length > 0) {
|
for (let i = 0; i < 5; i++) {
|
||||||
return res.status(400).json({
|
// Datum direkt berechnen ohne Zeitzonenprobleme
|
||||||
error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.`
|
const date = new Date(startYear, startMonth, startDay + i);
|
||||||
});
|
const year = date.getFullYear();
|
||||||
}
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const dateStr = `${year}-${month}-${day}`;
|
||||||
|
const entry = entriesByDate[dateStr];
|
||||||
|
|
||||||
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
|
// Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
||||||
// Prüfe welche Version die letzte ist
|
const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true);
|
||||||
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
|
if (entry && (entry.vacation_type === 'full' || isSick)) {
|
||||||
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
|
continue; // Tag ist ausgefüllt
|
||||||
[userId, week_start, week_end],
|
|
||||||
(err, result) => {
|
|
||||||
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
|
|
||||||
|
|
||||||
const maxVersion = result && result.max_version ? result.max_version : 0;
|
|
||||||
const newVersion = maxVersion + 1;
|
|
||||||
|
|
||||||
// Wenn bereits eine Version existiert, ist version_reason erforderlich
|
|
||||||
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Bitte geben Sie einen Grund für die neue Version an.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Version erstellen (nicht überschreiben)
|
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
|
||||||
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
|
const overtimeValue = entry && entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
|
||||||
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
||||||
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
|
|
||||||
(err) => {
|
|
||||||
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
|
|
||||||
|
|
||||||
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
|
if (isFullDayOvertime) {
|
||||||
db.run(`UPDATE timesheet_entries
|
continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag)
|
||||||
SET status = 'eingereicht'
|
}
|
||||||
WHERE user_id = ? AND date >= ? AND date <= ?`,
|
|
||||||
[userId, week_start, week_end],
|
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
|
||||||
(err) => {
|
// start_time und end_time könnten null, undefined oder leer strings sein
|
||||||
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
|
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
|
||||||
res.json({ success: true, version: newVersion });
|
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
|
||||||
});
|
|
||||||
});
|
if (!entry || !hasStartTime || !hasEndTime) {
|
||||||
});
|
missingDays.push(dateStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingDays.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
|
||||||
|
// Prüfe welche Version die letzte ist
|
||||||
|
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
|
||||||
|
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
|
||||||
|
[userId, week_start, week_end],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
|
||||||
|
|
||||||
|
const maxVersion = result && result.max_version ? result.max_version : 0;
|
||||||
|
const newVersion = maxVersion + 1;
|
||||||
|
|
||||||
|
// Wenn bereits eine Version existiert, ist version_reason erforderlich
|
||||||
|
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Bitte geben Sie einen Grund für die neue Version an.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Version erstellen (nicht überschreiben)
|
||||||
|
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
|
||||||
|
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
|
||||||
|
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
|
||||||
|
(err) => {
|
||||||
|
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
|
||||||
|
|
||||||
|
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
|
||||||
|
db.run(`UPDATE timesheet_entries
|
||||||
|
SET status = 'eingereicht'
|
||||||
|
WHERE user_id = ? AND date >= ? AND date <= ?`,
|
||||||
|
[userId, week_start, week_end],
|
||||||
|
(err) => {
|
||||||
|
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
|
||||||
|
res.json({ success: true, version: newVersion });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
179
routes/user.js
179
routes/user.js
@@ -51,6 +51,22 @@ function registerUserRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Client-IP abrufen
|
||||||
|
app.get('/api/user/client-ip', requireAuth, (req, res) => {
|
||||||
|
// Versuche verschiedene Methoden, um die Client-IP zu erhalten
|
||||||
|
const clientIp = req.ip ||
|
||||||
|
req.connection.remoteAddress ||
|
||||||
|
req.socket.remoteAddress ||
|
||||||
|
(req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
'unknown';
|
||||||
|
|
||||||
|
// Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1)
|
||||||
|
const cleanIp = clientIp.replace(/^::ffff:/, '');
|
||||||
|
|
||||||
|
res.json({ client_ip: cleanIp });
|
||||||
|
});
|
||||||
|
|
||||||
// API: Ping-IP abrufen
|
// API: Ping-IP abrufen
|
||||||
app.get('/api/user/ping-ip', requireAuth, (req, res) => {
|
app.get('/api/user/ping-ip', requireAuth, (req, res) => {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
@@ -120,6 +136,52 @@ function registerUserRoutes(app) {
|
|||||||
res.json({ success: true, currentRole: role });
|
res.json({ success: true, currentRole: role });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Verplante Urlaubstage (alle Wochen, auch nicht-eingereichte)
|
||||||
|
app.get('/api/user/planned-vacation', requireAuth, (req, res) => {
|
||||||
|
const userId = req.session.userId;
|
||||||
|
const { getCalendarWeek } = require('../helpers/utils');
|
||||||
|
|
||||||
|
db.all(`SELECT date, vacation_type FROM timesheet_entries
|
||||||
|
WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`,
|
||||||
|
[userId],
|
||||||
|
(err, entries) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let plannedDays = 0;
|
||||||
|
const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } }
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const dayValue = entry.vacation_type === 'full' ? 1 : 0.5;
|
||||||
|
plannedDays += dayValue;
|
||||||
|
|
||||||
|
// Berechne Kalenderwoche
|
||||||
|
const date = new Date(entry.date);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const week = getCalendarWeek(entry.date);
|
||||||
|
const weekKey = `${year}-KW${week}`;
|
||||||
|
|
||||||
|
if (!weeksMap[weekKey]) {
|
||||||
|
weeksMap[weekKey] = { year, week, days: 0 };
|
||||||
|
}
|
||||||
|
weeksMap[weekKey].days += dayValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Konvertiere zu sortiertem Array
|
||||||
|
const weeks = Object.values(weeksMap).sort((a, b) => {
|
||||||
|
if (a.year !== b.year) return a.year - b.year;
|
||||||
|
return a.week - b.week;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
plannedVacationDays: plannedDays,
|
||||||
|
weeks: weeks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage)
|
// API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage)
|
||||||
app.get('/api/user/stats', requireAuth, (req, res) => {
|
app.get('/api/user/stats', requireAuth, (req, res) => {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
@@ -134,29 +196,66 @@ function registerUserRoutes(app) {
|
|||||||
const urlaubstage = user.urlaubstage || 0;
|
const urlaubstage = user.urlaubstage || 0;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
|
||||||
// Alle eingereichten Wochen abrufen
|
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
|
||||||
db.all(`SELECT DISTINCT week_start, week_end
|
const { getCalendarWeek } = require('../helpers/utils');
|
||||||
FROM weekly_timesheets
|
db.all(`SELECT date, vacation_type FROM timesheet_entries
|
||||||
WHERE user_id = ? AND status = 'eingereicht'
|
WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`,
|
||||||
ORDER BY week_start`,
|
|
||||||
[userId],
|
[userId],
|
||||||
(err, weeks) => {
|
(err, allVacationEntries) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
|
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn keine Wochen vorhanden
|
let plannedVacationDays = 0;
|
||||||
if (!weeks || weeks.length === 0) {
|
const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } }
|
||||||
return res.json({
|
|
||||||
currentOvertime: overtimeOffsetHours,
|
(allVacationEntries || []).forEach(entry => {
|
||||||
remainingVacation: urlaubstage,
|
const dayValue = entry.vacation_type === 'full' ? 1 : 0.5;
|
||||||
totalOvertimeHours: 0,
|
plannedVacationDays += dayValue;
|
||||||
totalOvertimeTaken: 0,
|
|
||||||
totalVacationDays: 0,
|
// Berechne Kalenderwoche
|
||||||
urlaubstage: urlaubstage,
|
const date = new Date(entry.date);
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
const year = date.getFullYear();
|
||||||
});
|
const week = getCalendarWeek(entry.date);
|
||||||
}
|
const weekKey = `${year}-KW${week}`;
|
||||||
|
|
||||||
|
if (!weeksMap[weekKey]) {
|
||||||
|
weeksMap[weekKey] = { year, week, days: 0 };
|
||||||
|
}
|
||||||
|
weeksMap[weekKey].days += dayValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Konvertiere zu sortiertem Array
|
||||||
|
const plannedWeeks = Object.values(weeksMap).sort((a, b) => {
|
||||||
|
if (a.year !== b.year) return a.year - b.year;
|
||||||
|
return a.week - b.week;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alle eingereichten Wochen abrufen
|
||||||
|
db.all(`SELECT DISTINCT week_start, week_end
|
||||||
|
FROM weekly_timesheets
|
||||||
|
WHERE user_id = ? AND status = 'eingereicht'
|
||||||
|
ORDER BY week_start`,
|
||||||
|
[userId],
|
||||||
|
(err, weeks) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn keine Wochen vorhanden
|
||||||
|
if (!weeks || weeks.length === 0) {
|
||||||
|
return res.json({
|
||||||
|
currentOvertime: overtimeOffsetHours,
|
||||||
|
remainingVacation: urlaubstage,
|
||||||
|
totalOvertimeHours: 0,
|
||||||
|
totalOvertimeTaken: 0,
|
||||||
|
totalVacationDays: 0,
|
||||||
|
plannedVacationDays: plannedVacationDays,
|
||||||
|
plannedWeeks: plannedWeeks,
|
||||||
|
urlaubstage: urlaubstage,
|
||||||
|
overtimeOffsetHours: overtimeOffsetHours
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let totalOvertimeHours = 0;
|
let totalOvertimeHours = 0;
|
||||||
let totalOvertimeTaken = 0;
|
let totalOvertimeTaken = 0;
|
||||||
@@ -246,6 +345,8 @@ function registerUserRoutes(app) {
|
|||||||
totalOvertimeHours: totalOvertimeHours,
|
totalOvertimeHours: totalOvertimeHours,
|
||||||
totalOvertimeTaken: totalOvertimeTaken,
|
totalOvertimeTaken: totalOvertimeTaken,
|
||||||
totalVacationDays: totalVacationDays,
|
totalVacationDays: totalVacationDays,
|
||||||
|
plannedVacationDays: plannedVacationDays,
|
||||||
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
overtimeOffsetHours: overtimeOffsetHours
|
||||||
});
|
});
|
||||||
@@ -259,11 +360,24 @@ function registerUserRoutes(app) {
|
|||||||
let weekVacationDays = 0;
|
let weekVacationDays = 0;
|
||||||
let weekVacationHours = 0;
|
let weekVacationHours = 0;
|
||||||
|
|
||||||
|
const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8;
|
||||||
|
let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden
|
||||||
|
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
|
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
|
||||||
|
const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
||||||
|
|
||||||
if (entry.overtime_taken_hours) {
|
if (entry.overtime_taken_hours) {
|
||||||
weekOvertimeTaken += entry.overtime_taken_hours;
|
weekOvertimeTaken += entry.overtime_taken_hours;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden
|
||||||
|
// Diese Tage werden separat gezählt, um die Sollstunden anzupassen
|
||||||
|
if (isFullDayOvertime) {
|
||||||
|
fullDayOvertimeDays++;
|
||||||
|
}
|
||||||
|
|
||||||
// Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden
|
// Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden
|
||||||
if (entry.vacation_type === 'full') {
|
if (entry.vacation_type === 'full') {
|
||||||
weekVacationDays += 1;
|
weekVacationDays += 1;
|
||||||
@@ -273,12 +387,12 @@ function registerUserRoutes(app) {
|
|||||||
weekVacationDays += 0.5;
|
weekVacationDays += 0.5;
|
||||||
weekVacationHours += 4; // Halber Tag = 4 Stunden
|
weekVacationHours += 4; // Halber Tag = 4 Stunden
|
||||||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
weekTotalHours += entry.total_hours;
|
weekTotalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Kein Urlaub - zähle nur Arbeitsstunden
|
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
weekTotalHours += entry.total_hours;
|
weekTotalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,11 +401,22 @@ function registerUserRoutes(app) {
|
|||||||
// Sollstunden berechnen
|
// Sollstunden berechnen
|
||||||
const sollStunden = (wochenstunden / 5) * workdays;
|
const sollStunden = (wochenstunden / 5) * workdays;
|
||||||
|
|
||||||
// Überstunden für diese Woche: Urlaub zählt als normale Arbeitszeit
|
// Überstunden für diese Woche berechnen (wie im Frontend: totalHoursWithVacation - sollStunden)
|
||||||
|
// Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden
|
||||||
|
// Die Berechnung: (totalHours + vacationHours) - sollStunden
|
||||||
|
// Bei 8 Überstunden: totalHours = 0, daher: 0 - sollStunden = -sollStunden für diese Woche
|
||||||
|
// Die negativen Stunden (wegen 0 statt Sollstunden) werden durch die verbrauchten Überstunden ausgeglichen
|
||||||
|
// Daher: adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours)
|
||||||
|
// So werden die Tage mit 8 Überstunden nicht zu negativen Überstunden führen
|
||||||
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours;
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours;
|
||||||
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
|
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
||||||
|
// weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet)
|
||||||
|
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
||||||
|
|
||||||
// Kumulativ addieren
|
// Kumulativ addieren
|
||||||
|
// WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden)
|
||||||
|
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
||||||
|
// Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden
|
||||||
totalOvertimeHours += weekOvertimeHours;
|
totalOvertimeHours += weekOvertimeHours;
|
||||||
totalOvertimeTaken += weekOvertimeTaken;
|
totalOvertimeTaken += weekOvertimeTaken;
|
||||||
totalVacationDays += weekVacationDays;
|
totalVacationDays += weekVacationDays;
|
||||||
@@ -300,6 +425,9 @@ function registerUserRoutes(app) {
|
|||||||
|
|
||||||
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
|
// Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset
|
||||||
|
// weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend)
|
||||||
|
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
||||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||||
const remainingVacation = urlaubstage - totalVacationDays;
|
const remainingVacation = urlaubstage - totalVacationDays;
|
||||||
|
|
||||||
@@ -309,6 +437,8 @@ function registerUserRoutes(app) {
|
|||||||
totalOvertimeHours: totalOvertimeHours,
|
totalOvertimeHours: totalOvertimeHours,
|
||||||
totalOvertimeTaken: totalOvertimeTaken,
|
totalOvertimeTaken: totalOvertimeTaken,
|
||||||
totalVacationDays: totalVacationDays,
|
totalVacationDays: totalVacationDays,
|
||||||
|
plannedVacationDays: plannedVacationDays,
|
||||||
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
overtimeOffsetHours: overtimeOffsetHours
|
||||||
});
|
});
|
||||||
@@ -316,6 +446,7 @@ function registerUserRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const PORT = 3333;
|
|||||||
// Middleware
|
// Middleware
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
// Trust proxy für korrekte Client-IP-Erkennung (wichtig bei Proxies/Reverse Proxies)
|
||||||
|
app.set('trust proxy', true);
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)">Woche abschicken</button>
|
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)" disabled>Woche abschicken</button>
|
||||||
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
|
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,15 +63,30 @@
|
|||||||
<div class="user-stats-panel">
|
<div class="user-stats-panel">
|
||||||
<!-- Statistik-Karten -->
|
<!-- Statistik-Karten -->
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Aktuelle Überstunden</div>
|
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
Aktuelle Überstunden
|
||||||
|
<span class="help-icon" onclick="showHelpModal('overtime-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||||
|
</div>
|
||||||
<div class="stat-value" id="currentOvertime">-</div>
|
<div class="stat-value" id="currentOvertime">-</div>
|
||||||
<div class="stat-unit">Stunden</div>
|
<div class="stat-unit">Stunden</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-vacation">
|
<div class="stat-card stat-vacation">
|
||||||
<div class="stat-label">Verbleibende Urlaubstage</div>
|
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
Verbleibende Urlaubstage
|
||||||
|
<span class="help-icon" onclick="showHelpModal('remaining-vacation-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||||
|
</div>
|
||||||
<div class="stat-value" id="remainingVacation">-</div>
|
<div class="stat-value" id="remainingVacation">-</div>
|
||||||
<div class="stat-unit">von <span id="totalVacation">-</span> Tagen</div>
|
<div class="stat-unit">von <span id="totalVacation">-</span> Tagen</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card stat-planned">
|
||||||
|
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
Verplante Urlaubstage
|
||||||
|
<span class="help-icon" onclick="showHelpModal('planned-vacation-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value" id="plannedVacation">-</div>
|
||||||
|
<div class="stat-unit">Tage</div>
|
||||||
|
<div id="plannedWeeks" style="font-size: 11px; color: #666; margin-top: 8px; line-height: 1.4;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Zeiterfassung (URL & IP) -->
|
<!-- Zeiterfassung (URL & IP) -->
|
||||||
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
|
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
|
||||||
@@ -82,7 +97,10 @@
|
|||||||
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
||||||
<!-- URL-Erfassung -->
|
<!-- URL-Erfassung -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555;">Zeiterfassung per URL</h4>
|
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
||||||
|
Zeiterfassung per URL
|
||||||
|
<span class="help-icon" onclick="showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||||
|
</h4>
|
||||||
<div class="form-group" style="margin-bottom: 15px;">
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||||
<div style="display: flex; gap: 5px;">
|
<div style="display: flex; gap: 5px;">
|
||||||
@@ -101,13 +119,17 @@
|
|||||||
|
|
||||||
<!-- IP-Erfassung -->
|
<!-- IP-Erfassung -->
|
||||||
<div style="padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
<div style="padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555;">IP-basierte Zeiterfassung</h4>
|
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
||||||
|
IP-basierte Zeiterfassung
|
||||||
|
<span class="help-icon" onclick="showHelpModal('ip-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||||
|
</h4>
|
||||||
<div class="form-group" style="margin-bottom: 15px;">
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Ping-IP Adresse</label>
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Ping-IP Adresse</label>
|
||||||
<div style="display: flex; gap: 5px;">
|
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
|
||||||
<input type="text" id="pingIpInput" placeholder="z.B. 192.168.1.100" style="flex: 1; padding: 8px; font-size: 12px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="text" id="pingIpInput" placeholder="z.B. 192.168.1.100" style="flex: 1; padding: 8px; font-size: 12px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
<button onclick="window.savePingIP()" class="btn btn-sm btn-success" style="padding: 8px 12px;">Speichern</button>
|
<button onclick="window.savePingIP()" class="btn btn-sm btn-success" style="padding: 8px 12px;">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button onclick="window.detectClientIP()" class="btn btn-sm" style="padding: 6px 12px; background-color: #3498db; color: white; border: none; border-radius: 4px; font-size: 11px; cursor: pointer; margin-bottom: 5px;">Aktuelle IP ermitteln</button>
|
||||||
<p style="font-size: 11px; color: #666; margin-top: 5px; font-style: italic;">Ihre IP-Adresse für automatische Zeiterfassung</p>
|
<p style="font-size: 11px; color: #666; margin-top: 5px; font-style: italic;">Ihre IP-Adresse für automatische Zeiterfassung</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,5 +252,154 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Help Modal -->
|
||||||
|
<div id="helpModal" style="display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5);">
|
||||||
|
<div style="position: relative; background-color: #fff; margin: 10% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||||
|
<span onclick="closeHelpModal()" style="position: absolute; right: 15px; top: 15px; color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; line-height: 1;">×</span>
|
||||||
|
<div id="helpModalContent" style="padding-right: 30px;">
|
||||||
|
<!-- Content wird dynamisch eingefügt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Help Modal Funktionen
|
||||||
|
function showHelpModal(type) {
|
||||||
|
const modal = document.getElementById('helpModal');
|
||||||
|
const content = document.getElementById('helpModalContent');
|
||||||
|
|
||||||
|
let title = '';
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
if (type === 'url-help') {
|
||||||
|
title = 'Zeiterfassung per URL';
|
||||||
|
text = `
|
||||||
|
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Mit den Check-in und Check-out URLs können Sie Ihre Arbeitszeit automatisch erfassen,
|
||||||
|
indem Sie die URLs in Ihrem Browser aufrufen oder als Lesezeichen speichern.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Check-in URL:</strong> Öffnen Sie diese URL, um Ihre Start-Zeit zu erfassen.<br>
|
||||||
|
<strong>Check-out URL:</strong> Öffnen Sie diese URL, um Ihre End-Zeit zu erfassen.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Die URLs sind personalisiert und funktionieren nur für Ihren Account. Sie können sie
|
||||||
|
kopieren und als Lesezeichen in Ihrem Browser speichern.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else if (type === 'ip-help') {
|
||||||
|
title = 'IP-basierte Zeiterfassung';
|
||||||
|
text = `
|
||||||
|
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Die IP-basierte Zeiterfassung erkennt automatisch, wenn Sie sich im Firmennetzwerk befinden,
|
||||||
|
indem Ihre IP-Adresse regelmäßig geprüft wird.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>So funktioniert es:</strong>
|
||||||
|
</p>
|
||||||
|
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||||
|
<li>Tragen Sie Ihre IP-Adresse ein (z.B. 192.168.1.100)</li>
|
||||||
|
<li>Das System prüft regelmäßig, ob diese IP-Adresse erreichbar ist</li>
|
||||||
|
<li>Wenn die IP erreichbar ist, wird automatisch eine Start-Zeit erfasst</li>
|
||||||
|
<li>Wenn die IP nicht mehr erreichbar ist, wird automatisch eine End-Zeit erfasst</li>
|
||||||
|
</ul>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Tipp:</strong> Verwenden Sie den Button "Aktuelle IP ermitteln", um Ihre aktuelle
|
||||||
|
IP-Adresse automatisch zu erkennen.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else if (type === 'remaining-vacation-help') {
|
||||||
|
title = 'Verbleibende Urlaubstage';
|
||||||
|
text = `
|
||||||
|
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Die <strong>verbleibenden Urlaubstage</strong> zeigen an, wie viele Urlaubstage Sie noch
|
||||||
|
zur Verfügung haben.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Wichtig:</strong> Diese Zahl berücksichtigt nur Urlaubstage aus Wochen, die bereits
|
||||||
|
<strong>eingereicht</strong> wurden. Urlaubstage, die Sie nur geplant, aber noch nicht
|
||||||
|
abgeschickt haben, werden hier nicht abgezogen.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Beispiel:</strong> Wenn Sie 25 Urlaubstage haben und bereits 5 Tage in eingereichten
|
||||||
|
Wochen genommen haben, zeigt diese Anzeige 20 verbleibende Tage.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else if (type === 'planned-vacation-help') {
|
||||||
|
title = 'Verplante Urlaubstage';
|
||||||
|
text = `
|
||||||
|
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Die <strong>verplanten Urlaubstage</strong> zeigen alle Urlaubstage an, die Sie in irgendeiner
|
||||||
|
Woche eingetragen haben - unabhängig davon, ob die Woche bereits eingereicht wurde oder nicht.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Unterschied zu "Verbleibende Urlaubstage":</strong>
|
||||||
|
</p>
|
||||||
|
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||||
|
<li><strong>Verbleibende Urlaubstage:</strong> Nur von eingereichten Wochen</li>
|
||||||
|
<li><strong>Verplante Urlaubstage:</strong> Alle geplanten Tage (auch nicht-eingereichte Wochen)</li>
|
||||||
|
</ul>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Beispiel:</strong> Wenn Sie in einer noch nicht eingereichten Woche 3 Tage Urlaub
|
||||||
|
eintragen, erscheinen diese sofort in "Verplante Urlaubstage", aber noch nicht in
|
||||||
|
"Verbleibende Urlaubstage". Erst nach dem Abschicken der Woche werden sie auch von den
|
||||||
|
verbleibenden Tagen abgezogen.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||||
|
<strong>Hinweis:</strong> Unter dieser Anzeige sehen Sie, in welchen Kalenderwochen
|
||||||
|
(KW) Sie Urlaub geplant haben.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else if (type === 'overtime-help') {
|
||||||
|
title = 'Aktuelle Überstunden';
|
||||||
|
text = `
|
||||||
|
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
Die <strong>aktuellen Überstunden</strong> zeigen Ihre gesamten Überstunden an, die sich aus
|
||||||
|
allen bereits eingereichten Wochen ergeben.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>Wichtig:</strong> Überstunden werden erst berechnet und angezeigt, wenn die entsprechende
|
||||||
|
Woche <strong>abgeschickt</strong> wurde. Überstunden aus Wochen, die Sie nur geplant, aber noch
|
||||||
|
nicht abgeschickt haben, werden hier nicht berücksichtigt.
|
||||||
|
</p>
|
||||||
|
<p style="color: #555; line-height: 1.6;">
|
||||||
|
<strong>So funktioniert die Berechnung:</strong>
|
||||||
|
</p>
|
||||||
|
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||||
|
<li>Für jede eingereichte Woche werden Ihre tatsächlichen Arbeitsstunden mit den Sollstunden verglichen</li>
|
||||||
|
<li>Die Differenz ergibt die Überstunden (positiv) oder Minusstunden (negativ) für diese Woche</li>
|
||||||
|
<li>Alle Überstunden aus eingereichten Wochen werden summiert</li>
|
||||||
|
<li>Zusätzlich können manuelle Korrekturen (Offset) durch die Verwaltung hinzugefügt werden</li>
|
||||||
|
</ul>
|
||||||
|
<p style="color: #555; line-height: 1.6; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||||
|
<strong>Beispiel:</strong> Wenn Sie diese Woche 42 Stunden arbeiten, aber nur 40 Stunden Soll haben,
|
||||||
|
entstehen 2 Überstunden. Diese werden jedoch erst nach dem Abschicken der Woche zu Ihren
|
||||||
|
aktuellen Überstunden hinzugefügt.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content.innerHTML = text;
|
||||||
|
modal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHelpModal() {
|
||||||
|
document.getElementById('helpModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal schließen wenn außerhalb geklickt wird
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('helpModal');
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeHelpModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user