Collabsable automations

This commit is contained in:
Carsten Graf
2026-01-26 10:48:39 +01:00
parent bbb176dbe3
commit 00669be250
3 changed files with 108 additions and 26 deletions

View File

@@ -113,6 +113,29 @@ function registerVerwaltungRoutes(app) {
});
});
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
const userId = req.params.id;
const currentYear = new Date().getFullYear();
const yearStart = `${currentYear}-01-01`;
const yearEnd = `${currentYear}-12-31`;
db.all(`SELECT DISTINCT date
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ? AND sick_status = 1
ORDER BY date`,
[userId, yearStart, yearEnd],
(err, entries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Krankheitstage' });
}
const sickDays = entries ? entries.length : 0;
res.json({ sickDays: sickDays, year: currentYear });
}
);
});
// API: Überstunden- und Urlaubsstatistiken für einen User abrufen
app.get('/api/verwaltung/user/:id/stats', requireVerwaltung, (req, res) => {
const userId = req.params.id;

View File

@@ -73,37 +73,46 @@
<div class="stat-unit">von <span id="totalVacation">-</span> Tagen</div>
</div>
<!-- URL-Erfassung -->
<!-- Zeiterfassung (URL & IP) -->
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
<h3 style="font-size: 14px; margin-bottom: 15px; color: #2c3e50;">Zeiterfassung per URL</h3>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="checkinUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
<h3 style="font-size: 14px; margin-bottom: 0; color: #2c3e50; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleTimeCapture()">
<span class="toggle-icon-time-capture" style="display: inline-block; transition: transform 0.3s;">▶</span>
Automatische Zeiterfassung
</h3>
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
<!-- URL-Erfassung -->
<div style="margin-bottom: 20px;">
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555;">Zeiterfassung per URL</h4>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="checkinUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="checkoutUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
</div>
</div>
</div>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="checkoutUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
<!-- IP-Erfassung -->
<div style="padding-top: 15px; border-top: 1px solid #e0e0e0;">
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555;">IP-basierte Zeiterfassung</h4>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Ping-IP Adresse</label>
<div style="display: flex; gap: 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;">
<button onclick="window.savePingIP()" class="btn btn-sm btn-success" style="padding: 8px 12px;">Speichern</button>
</div>
<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>
<!-- IP-Erfassung -->
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
<h3 style="font-size: 14px; margin-bottom: 15px; color: #2c3e50;">IP-basierte Zeiterfassung</h3>
<div class="form-group" style="margin-bottom: 15px;">
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Ping-IP Adresse</label>
<div style="display: flex; gap: 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;">
<button onclick="window.savePingIP()" class="btn btn-sm btn-success" style="padding: 8px 12px;">Speichern</button>
</div>
<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>
</div>
@@ -126,6 +135,22 @@
}
}
// Zeiterfassung ein-/ausklappen
function toggleTimeCapture() {
const content = document.getElementById('timeCaptureContent');
const icon = document.querySelector('.toggle-icon-time-capture');
if (content && icon) {
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
}
// URL-Kopier-Funktion
function copyToClipboard(inputId) {
const input = document.getElementById(inputId);

View File

@@ -113,6 +113,10 @@
<div style="display: inline-block; margin-right: 20px;">
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></span>
</div>
<div style="display: inline-block; margin-right: 20px;">
<strong>Krankheitstage (<span class="sick-days-year"><%= new Date().getFullYear() %></span>):</strong>
<span class="sick-days-count" data-user-id="<%= employee.user.id %>" style="color: #e74c3c;">-</span>
</div>
</div>
</div>
<button class="btn btn-secondary btn-sm toggle-employee-btn" data-employee-index="<%= employeeIndex %>">
@@ -330,6 +334,36 @@
// Statistiken für alle Wochen initial laden
document.querySelectorAll('.group-stats').forEach(statsDiv => loadStatsForDiv(statsDiv));
// Krankheitstage für alle Mitarbeiter laden
async function loadSickDays() {
const sickDaysElements = document.querySelectorAll('.sick-days-count');
const userIds = Array.from(sickDaysElements).map(el => el.dataset.userId);
const uniqueUserIds = [...new Set(userIds)];
for (const userId of uniqueUserIds) {
try {
const response = await fetch(`/api/verwaltung/user/${userId}/sick-days`);
if (!response.ok) {
throw new Error('Fehler beim Laden der Krankheitstage');
}
const data = await response.json();
// Alle Elemente für diesen User aktualisieren
document.querySelectorAll(`.sick-days-count[data-user-id="${userId}"]`).forEach(el => {
el.textContent = data.sickDays || 0;
});
} catch (error) {
console.error('Fehler beim Laden der Krankheitstage für User', userId, ':', error);
document.querySelectorAll(`.sick-days-count[data-user-id="${userId}"]`).forEach(el => {
el.textContent = '-';
});
}
}
}
// Krankheitstage beim Laden der Seite abrufen
loadSickDays();
// Überstunden-Offset speichern
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
btn.addEventListener('click', async function() {