feat: enhance flextime tracking with future days count and tooltip hints
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
This commit is contained in:
@@ -558,7 +558,12 @@
|
|||||||
<div id="statActualHours" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-400">0h</div>
|
<div id="statActualHours" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-400">0h</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
||||||
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Saldo (Monat)</div>
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide flex items-center gap-1">
|
||||||
|
Saldo (Monat)
|
||||||
|
<span id="balanceFlextimeHint" class="hidden text-cyan-400 cursor-help" title="">
|
||||||
|
<i data-lucide="info" class="w-3 h-3"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div id="statBalance" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400">0h</div>
|
<div id="statBalance" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400">0h</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
||||||
@@ -594,7 +599,12 @@
|
|||||||
<div class="glass-card rounded-xl p-6 border-2 border-purple-500/30 shadow-lg shadow-purple-500/20">
|
<div class="glass-card rounded-xl p-6 border-2 border-purple-500/30 shadow-lg shadow-purple-500/20">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-gray-300 mb-2 uppercase tracking-wide font-semibold">Gesamt-Saldo (inkl. Vormonat)</div>
|
<div class="text-sm text-gray-300 mb-2 uppercase tracking-wide font-semibold flex items-center gap-1">
|
||||||
|
Gesamt-Saldo (inkl. Vormonat)
|
||||||
|
<span id="totalBalanceFlextimeHint" class="hidden text-cyan-400 cursor-help" title="">
|
||||||
|
<i data-lucide="info" class="w-3 h-3"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div id="statTotalBalance" class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400">0h</div>
|
<div id="statTotalBalance" class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400">0h</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
|
|||||||
@@ -1352,6 +1352,8 @@ async function updateStatistics(entries) {
|
|||||||
.map(e => e.date)
|
.map(e => e.date)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let futureFlextimeDays = 0; // Count future flextime days in current month
|
||||||
|
|
||||||
for (let day = 1; day <= lastDay; day++) {
|
for (let day = 1; day <= lastDay; day++) {
|
||||||
const dateObj = new Date(currentYear, currentMonth, day);
|
const dateObj = new Date(currentYear, currentMonth, day);
|
||||||
const year = dateObj.getFullYear();
|
const year = dateObj.getFullYear();
|
||||||
@@ -1363,13 +1365,26 @@ async function updateStatistics(entries) {
|
|||||||
const isFlextime = flextimeDays.has(dateISO);
|
const isFlextime = flextimeDays.has(dateISO);
|
||||||
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
||||||
|
|
||||||
if (!isWeekendHoliday && !isVacation) {
|
if (!isWeekendHoliday && !isVacation && !isFlextime) {
|
||||||
// Normal workday (excluding vacation days)
|
// Normal workday (excluding vacation and flextime days)
|
||||||
totalWorkdaysInMonth++;
|
totalWorkdaysInMonth++;
|
||||||
workdaysCount++;
|
workdaysCount++;
|
||||||
if (dateObj <= today) {
|
if (dateObj <= today) {
|
||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
}
|
}
|
||||||
|
} else if (!isWeekendHoliday && !isVacation && isFlextime) {
|
||||||
|
// Flextime on a workday - still counts as workday in calendar
|
||||||
|
totalWorkdaysInMonth++;
|
||||||
|
workdaysCount++;
|
||||||
|
if (dateObj <= today) {
|
||||||
|
workdaysPassed++;
|
||||||
|
} else {
|
||||||
|
// Future flextime in current month
|
||||||
|
const isCurrentMonth = currentYear === today.getFullYear() && currentMonth === today.getMonth();
|
||||||
|
if (isCurrentMonth) {
|
||||||
|
futureFlextimeDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (isFlextime && isWeekendHoliday) {
|
} else if (isFlextime && isWeekendHoliday) {
|
||||||
// Flextime on weekend/holiday counts as additional workday
|
// Flextime on weekend/holiday counts as additional workday
|
||||||
totalWorkdaysInMonth++;
|
totalWorkdaysInMonth++;
|
||||||
@@ -1380,7 +1395,7 @@ async function updateStatistics(entries) {
|
|||||||
// Vacation days are excluded from all counts
|
// Vacation days are excluded from all counts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate target hours (8h per workday passed)
|
// Calculate target hours (8h per workday passed, no reduction)
|
||||||
const targetHours = workdaysPassed * 8;
|
const targetHours = workdaysPassed * 8;
|
||||||
|
|
||||||
// Calculate actual hours worked (only up to today)
|
// Calculate actual hours worked (only up to today)
|
||||||
@@ -1406,7 +1421,10 @@ async function updateStatistics(entries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate balance for current month
|
// Calculate balance for current month
|
||||||
const balance = actualHours - targetHours;
|
let balance = actualHours - targetHours;
|
||||||
|
|
||||||
|
// Subtract future flextime days from balance (they consume flextime)
|
||||||
|
balance -= (futureFlextimeDays * 8);
|
||||||
|
|
||||||
// Calculate previous month balance
|
// Calculate previous month balance
|
||||||
const previousBalance = await calculatePreviousBalance();
|
const previousBalance = await calculatePreviousBalance();
|
||||||
@@ -1425,6 +1443,33 @@ async function updateStatistics(entries) {
|
|||||||
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
|
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
|
||||||
document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`;
|
document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`;
|
||||||
|
|
||||||
|
// Show/hide flextime hint icons
|
||||||
|
const balanceFlextimeHint = document.getElementById('balanceFlextimeHint');
|
||||||
|
const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint');
|
||||||
|
|
||||||
|
if (futureFlextimeDays > 0) {
|
||||||
|
const tooltipText = `Inkl. ${futureFlextimeDays} geplanter Gleitzeittag${futureFlextimeDays > 1 ? 'e' : ''} (-${futureFlextimeDays * 8}h)`;
|
||||||
|
|
||||||
|
balanceFlextimeHint.classList.remove('hidden');
|
||||||
|
totalBalanceFlextimeHint.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Set title attribute before re-initializing icons
|
||||||
|
balanceFlextimeHint.setAttribute('title', tooltipText);
|
||||||
|
totalBalanceFlextimeHint.setAttribute('title', tooltipText);
|
||||||
|
|
||||||
|
// Re-initialize icons
|
||||||
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-apply title after icon initialization (in case it was cleared)
|
||||||
|
balanceFlextimeHint.setAttribute('title', tooltipText);
|
||||||
|
totalBalanceFlextimeHint.setAttribute('title', tooltipText);
|
||||||
|
} else {
|
||||||
|
balanceFlextimeHint.classList.add('hidden');
|
||||||
|
totalBalanceFlextimeHint.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// Current month balance
|
// Current month balance
|
||||||
const balanceElement = document.getElementById('statBalance');
|
const balanceElement = document.getElementById('statBalance');
|
||||||
balanceElement.textContent = (balance >= 0 ? '+' : '') + balance.toFixed(1) + 'h';
|
balanceElement.textContent = (balance >= 0 ? '+' : '') + balance.toFixed(1) + 'h';
|
||||||
@@ -2651,6 +2696,90 @@ async function handleBundeslandChange(event) {
|
|||||||
const newBundesland = event.target.value;
|
const newBundesland = event.target.value;
|
||||||
const oldBundesland = currentBundesland;
|
const oldBundesland = currentBundesland;
|
||||||
|
|
||||||
|
// Show warning with backup recommendation
|
||||||
|
const bundeslandNames = {
|
||||||
|
'BW': 'Baden-Württemberg',
|
||||||
|
'BY': 'Bayern',
|
||||||
|
'BE': 'Berlin',
|
||||||
|
'BB': 'Brandenburg',
|
||||||
|
'HB': 'Bremen',
|
||||||
|
'HH': 'Hamburg',
|
||||||
|
'HE': 'Hessen',
|
||||||
|
'MV': 'Mecklenburg-Vorpommern',
|
||||||
|
'NI': 'Niedersachsen',
|
||||||
|
'NW': 'Nordrhein-Westfalen',
|
||||||
|
'RP': 'Rheinland-Pfalz',
|
||||||
|
'SL': 'Saarland',
|
||||||
|
'SN': 'Sachsen',
|
||||||
|
'ST': 'Sachsen-Anhalt',
|
||||||
|
'SH': 'Schleswig-Holstein',
|
||||||
|
'TH': 'Thüringen'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create custom modal for bundesland change confirmation
|
||||||
|
const modalHTML = `
|
||||||
|
<div id="bundeslandWarningModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display: flex;">
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-2xl p-6 max-w-md w-full mx-4 border border-yellow-600">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<i data-lucide="alert-triangle" class="w-8 h-8 text-yellow-500"></i>
|
||||||
|
<h3 class="text-xl font-bold text-yellow-500">Achtung: Bundesland ändern</h3>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-300 mb-6 space-y-3">
|
||||||
|
<p>Sie möchten das Bundesland von <strong>${bundeslandNames[oldBundesland]}</strong> auf <strong>${bundeslandNames[newBundesland]}</strong> ändern.</p>
|
||||||
|
<p class="text-yellow-400"><strong>Warnung:</strong> Durch die Änderung der Feiertage können bestehende Einträge betroffen sein. An Tagen, die zu Feiertagen werden, bleiben Arbeitseinträge erhalten.</p>
|
||||||
|
<div class="bg-gray-900 border border-blue-600 rounded-lg p-3 mt-4">
|
||||||
|
<p class="text-blue-400 text-sm mb-2"><i data-lucide="info" class="w-4 h-4 inline mr-1"></i> Wir empfehlen ein Backup vor der Änderung:</p>
|
||||||
|
<button id="quickBackupBtn" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm">
|
||||||
|
<i data-lucide="download" class="w-4 h-4"></i>
|
||||||
|
Jetzt Backup erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button id="cancelBundeslandChange" class="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button id="confirmBundeslandChange" class="flex-1 px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg transition-colors font-semibold">
|
||||||
|
Trotzdem ändern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Insert modal into DOM
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.innerHTML = modalHTML;
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
// Initialize icons in modal
|
||||||
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle backup button
|
||||||
|
document.getElementById('quickBackupBtn').addEventListener('click', async () => {
|
||||||
|
await exportDatabase();
|
||||||
|
showNotification('✓ Backup erstellt', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
document.getElementById('cancelBundeslandChange').addEventListener('click', () => {
|
||||||
|
event.target.value = oldBundesland;
|
||||||
|
document.getElementById('bundeslandWarningModal').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle confirm
|
||||||
|
document.getElementById('confirmBundeslandChange').addEventListener('click', async () => {
|
||||||
|
document.getElementById('bundeslandWarningModal').remove();
|
||||||
|
await performBundeslandChange(newBundesland, oldBundesland, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the actual bundesland change after confirmation
|
||||||
|
*/
|
||||||
|
async function performBundeslandChange(newBundesland, oldBundesland, event) {
|
||||||
// Check for conflicts with existing entries
|
// Check for conflicts with existing entries
|
||||||
const entries = await fetchEntries();
|
const entries = await fetchEntries();
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
@@ -2689,16 +2818,12 @@ async function handleBundeslandChange(event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Warn user if conflicts exist
|
// Show conflict info if exists
|
||||||
if (conflicts.length > 0) {
|
if (conflicts.length > 0) {
|
||||||
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
|
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
|
||||||
const message = `Achtung!\n\nDie folgenden Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nMöchten Sie fortfahren? Die Einträge bleiben erhalten, aber die Tage werden als Feiertage markiert.`;
|
const message = `Folgende Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nDie Einträge bleiben erhalten.`;
|
||||||
|
showNotification(`⚠️ ${conflicts.length} Konflikt(e) gefunden`, 'warning');
|
||||||
if (!confirm(message)) {
|
console.info(message);
|
||||||
// Revert selection
|
|
||||||
event.target.value = oldBundesland;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state and save
|
// Update state and save
|
||||||
|
|||||||
Reference in New Issue
Block a user