+
Heutige Arbeitszeit
00:00:00
-
Ende
| Pause |
Netto |
+ Saldo |
Ort |
Actions |
diff --git a/public/js/main.js b/public/js/main.js
index bc22ebd..c896604 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -84,6 +84,29 @@ function handleNextMonth() {
// TIMER FUNCTIONS
// ============================================
+/**
+ * Update timer status text and show/hide metrics
+ */
+function setTimerStatus(text, showMetrics = false) {
+ const statusTextEl = document.getElementById('timerStatusText');
+ const metricsEl = document.getElementById('timerMetrics');
+ const manualTimeLink = document.getElementById('manualTimeLink');
+
+ if (statusTextEl) {
+ statusTextEl.textContent = text;
+ }
+
+ if (metricsEl) {
+ if (showMetrics) {
+ metricsEl.classList.remove('hidden');
+ if (manualTimeLink) manualTimeLink.classList.add('hidden');
+ } else {
+ metricsEl.classList.add('hidden');
+ if (manualTimeLink) manualTimeLink.classList.remove('hidden');
+ }
+ }
+}
+
/**
* Check if timer is running (on page load)
*/
@@ -114,9 +137,7 @@ async function checkRunningTimer() {
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + todayEntry.startTime;
- document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
- document.getElementById('timerStatus').classList.add('cursor-default');
+ setTimerStatus('Läuft seit ' + todayEntry.startTime, true);
// Calculate elapsed time and check for active pauses
const elapsed = Date.now() - timerStartTime;
@@ -135,7 +156,7 @@ async function checkRunningTimer() {
timerPausedDuration = 0;
const remainingPause = (sixHoursSeconds + thirtyMinutes) - elapsedSeconds;
pauseEndTime = Date.now() + (remainingPause * 1000);
- document.getElementById('timerStatus').textContent = `Läuft seit ${todayEntry.startTime} - Pause (${Math.ceil(remainingPause / 60)} Min)`;
+ setTimerStatus(`Läuft seit ${todayEntry.startTime} - Pause (${Math.ceil(remainingPause / 60)} Min)`, true);
// Schedule end of pause
pauseTimeout = setTimeout(() => {
@@ -143,7 +164,7 @@ async function checkRunningTimer() {
isPaused = false;
pauseStartElapsed = 0;
pauseEndTime = 0;
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + todayEntry.startTime;
+ setTimerStatus('Läuft seit ' + todayEntry.startTime, true);
}, remainingPause * 1000);
}
// Check if in 9-hour pause (9h30m to 9h45m real time = 9h to 9h work time)
@@ -153,7 +174,7 @@ async function checkRunningTimer() {
timerPausedDuration = thirtyMinutes;
const remainingPause = (nineHoursSeconds + thirtyMinutes + fifteenMinutes) - elapsedSeconds;
pauseEndTime = Date.now() + (remainingPause * 1000);
- document.getElementById('timerStatus').textContent = `Läuft seit ${todayEntry.startTime} - Pause (${Math.ceil(remainingPause / 60)} Min)`;
+ setTimerStatus(`Läuft seit ${todayEntry.startTime} - Pause (${Math.ceil(remainingPause / 60)} Min)`, true);
// Schedule end of pause
pauseTimeout = setTimeout(() => {
@@ -161,7 +182,7 @@ async function checkRunningTimer() {
isPaused = false;
pauseStartElapsed = 0;
pauseEndTime = 0;
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + todayEntry.startTime;
+ setTimerStatus('Läuft seit ' + todayEntry.startTime, true);
}, remainingPause * 1000);
}
// Not in pause, but may have completed pauses
@@ -214,9 +235,7 @@ async function startWork() {
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + startTime;
- document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
- document.getElementById('timerStatus').classList.add('cursor-default');
+ setTimerStatus('Läuft seit ' + startTime, true);
// Start timer interval
timerInterval = setInterval(updateTimer, 1000);
@@ -269,9 +288,7 @@ async function stopWork() {
stopBtn.classList.add('opacity-50', 'cursor-not-allowed');
stopBtn.classList.remove('hover:bg-red-700');
document.getElementById('timerDisplay').textContent = '00:00:00';
- document.getElementById('timerStatus').textContent = 'Nicht gestartet';
- document.getElementById('timerStatus').classList.add('cursor-pointer', 'underline', 'hover:text-blue-300');
- document.getElementById('timerStatus').classList.remove('cursor-default');
+ setTimerStatus('Nicht gestartet', false);
// Reload monthly view
loadMonthlyView();
@@ -293,7 +310,7 @@ function updateTimer() {
// Update pause countdown live
const remainingSeconds = Math.max(0, Math.ceil((pauseEndTime - now) / 1000));
const remainingMinutes = Math.ceil(remainingSeconds / 60);
- document.getElementById('timerStatus').textContent = `Läuft seit ${timerStartTimeString} - Pause (${remainingMinutes} Min)`;
+ setTimerStatus(`Läuft seit ${timerStartTimeString} - Pause (${remainingMinutes} Min)`, true);
} else {
elapsed = Math.floor((now - timerStartTime) / 1000) - timerPausedDuration;
@@ -308,6 +325,9 @@ function updateTimer() {
document.getElementById('timerDisplay').textContent = formatDuration(elapsed);
+ // Calculate and display additional timer metrics
+ updateTimerMetrics(elapsed);
+
// Update live net hours in the table for current day
const netHoursCell = document.getElementById('current-day-net-hours');
if (netHoursCell) {
@@ -315,6 +335,21 @@ function updateTimer() {
netHoursCell.textContent = netHours.toFixed(2);
}
+ // Update live balance in the table for current day
+ const balanceCell = document.getElementById('current-day-balance');
+ if (balanceCell) {
+ const netHours = elapsed / 3600; // Convert seconds to hours
+ const deviation = netHours - 8.0;
+ const baseBalance = parseFloat(balanceCell.dataset.baseBalance || 0);
+ const currentBalance = baseBalance + deviation;
+
+ const balanceColor = currentBalance >= 0 ? 'text-green-400' : 'text-red-400';
+ const balanceSign = currentBalance >= 0 ? '+' : '';
+
+ balanceCell.className = `px-4 py-3 whitespace-nowrap text-sm font-semibold ${balanceColor}`;
+ balanceCell.textContent = `${balanceSign}${currentBalance.toFixed(2)}h`;
+ }
+
// Update live pause minutes in the table for current day
const pauseCell = document.getElementById('current-day-pause');
if (pauseCell) {
@@ -342,6 +377,59 @@ function updateTimer() {
}
}
+/**
+ * Calculate and update additional timer metrics
+ */
+function updateTimerMetrics(netElapsedSeconds) {
+ const targetReachedTimeSpan = document.getElementById('targetReachedTime');
+ const timeUntilTargetSpan = document.getElementById('timeUntilTarget');
+
+ if (!timerStartTime) {
+ return;
+ }
+
+ // Target: 8 hours (28800 seconds)
+ const targetSeconds = 8 * 60 * 60;
+
+ // Calculate total pause time: 30 min after 6h + 15 min after 9h
+ const pauseDuration30Min = 30 * 60; // 30 minutes in seconds
+ const pauseDuration15Min = 15 * 60; // 15 minutes in seconds
+
+ // Time needed including pauses
+ // After 6h work -> 30 min pause
+ // After 9h work -> 15 min pause
+ // Total gross time = 8h work + 30min pause + 15min pause = 8h 45min
+ const totalGrossTimeNeeded = targetSeconds + pauseDuration30Min + pauseDuration15Min;
+
+ // Calculate when target will be reached (clock time)
+ const targetReachedTimestamp = new Date(timerStartTime + totalGrossTimeNeeded * 1000);
+ const targetHours = String(targetReachedTimestamp.getHours()).padStart(2, '0');
+ const targetMinutes = String(targetReachedTimestamp.getMinutes()).padStart(2, '0');
+ targetReachedTimeSpan.textContent = `${targetHours}:${targetMinutes}`;
+
+ // Calculate countdown to target (remaining net work time)
+ const remainingNetSeconds = Math.max(0, targetSeconds - netElapsedSeconds);
+
+ if (remainingNetSeconds === 0) {
+ timeUntilTargetSpan.textContent = '00:00:00';
+ timeUntilTargetSpan.classList.add('text-green-400');
+ } else {
+ // Format as HH:MM:SS
+ const hours = Math.floor(remainingNetSeconds / 3600);
+ const minutes = Math.floor((remainingNetSeconds % 3600) / 60);
+ const seconds = remainingNetSeconds % 60;
+
+ timeUntilTargetSpan.textContent =
+ `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
+ timeUntilTargetSpan.classList.remove('text-green-400');
+ }
+
+ // Reinitialize Lucide icons for the new metrics
+ if (typeof lucide !== 'undefined' && lucide.createIcons) {
+ lucide.createIcons();
+ }
+}
+
/**
* Schedule automatic pauses at 6h and 9h with offset for existing elapsed time
*/
@@ -384,7 +472,7 @@ function pauseTimer(durationSeconds) {
timerPausedDuration += durationSeconds;
isPaused = false;
pauseEndTime = 0;
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + timerStartTimeString;
+ setTimerStatus('Läuft seit ' + timerStartTimeString, true);
}, durationSeconds * 1000);
}
@@ -497,6 +585,9 @@ function renderEntries(entries) {
emptyState.classList.add('hidden');
+ // Running balance accumulator
+ let runningBalance = 0;
+
entries.forEach(entry => {
const row = document.createElement('tr');
const dateObj = new Date(entry.date + 'T00:00:00');
@@ -529,6 +620,22 @@ function renderEntries(entries) {
` : ' | ';
+ // Calculate balance: for work and flextime entries (excluding vacation)
+ const entryType = entry.entryType || 'work';
+ let balanceCell = '';
+
+ if (entryType !== 'vacation') {
+ // For all workdays (including flextime): Actual - Target (8h)
+ // Flextime has netHours = 0, so deviation will be -8h
+ const deviation = entry.netHours - 8.0;
+ runningBalance += deviation;
+ const balanceColor = runningBalance >= 0 ? 'text-green-400' : 'text-red-400';
+ const balanceSign = runningBalance >= 0 ? '+' : '';
+ balanceCell = `${balanceSign}${runningBalance.toFixed(2)}h | `;
+ } else {
+ balanceCell = '- | ';
+ }
+
row.innerHTML = checkboxCell + `
${dayOfWeek} |
${formatDateDisplay(entry.date)} |
@@ -545,6 +652,7 @@ function renderEntries(entries) {
${entry.pauseMinutes}
${entry.netHours.toFixed(2)} |
+ ${balanceCell}
${locationIcon} ${locationText}
|
@@ -618,6 +726,9 @@ function renderMonthlyView(entries) {
entriesMap[entry.date] = entry;
});
+ // Running balance accumulator
+ let runningBalance = 0;
+
// Render all days from 1st to lastDay
for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(displayYear, displayMonth, day);
@@ -707,11 +818,33 @@ function renderMonthlyView(entries) {
` : ' | ';
+ // Calculate balance: only for past days (excluding vacation)
+ const isPastDay = dateObj < today || (dateObj.getFullYear() === todayYear && dateObj.getMonth() === todayMonth && day <= todayDay);
+ let balanceCell = '';
+
+ if (isPastDay && entryType !== 'vacation') {
+ // For all workdays (including flextime): Actual - Target (8h)
+ // Flextime has netHours = 0, so deviation will be -8h
+ const deviation = entry.netHours - 8.0;
+ runningBalance += deviation;
+ const balanceColor = runningBalance >= 0 ? 'text-green-400' : 'text-red-400';
+ const balanceSign = runningBalance >= 0 ? '+' : '';
+
+ // Add ID for current day to enable live updates
+ const balanceId = isToday && entryType === 'work' ? 'id="current-day-balance"' : '';
+ const balanceDataAttr = isToday && entryType === 'work' ? `data-base-balance="${runningBalance - deviation}"` : '';
+
+ balanceCell = `${balanceSign}${runningBalance.toFixed(2)}h | `;
+ } else {
+ balanceCell = '- | ';
+ }
+
row.innerHTML = checkboxCell + `
${dayOfWeek} |
${formatDateDisplay(entry.date)} |
${displayTimes}
${entry.netHours.toFixed(2)} |
+ ${balanceCell}
${displayIcon} ${displayText}
|
@@ -754,7 +887,7 @@ function renderMonthlyView(entries) {
data-date="${dateISO}" ${selectedEntries.has(dateISO) ? 'checked' : ''}>
` : ' | ';
- const colspan = bulkEditMode ? '5' : '5';
+ const colspan = bulkEditMode ? '6' : '6';
row.innerHTML = checkboxCell + `
${dayOfWeek} |
@@ -3577,9 +3710,7 @@ async function handleManualStartTime(timeStr) {
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + timeStr;
- document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
- document.getElementById('timerStatus').classList.add('cursor-default');
+ setTimerStatus('Läuft seit ' + timeStr, true);
// Calculate elapsed time and check for active pauses
const elapsed = now.getTime() - startDate.getTime();
@@ -3598,7 +3729,7 @@ async function handleManualStartTime(timeStr) {
timerPausedDuration = 0;
const remainingPause = (sixHoursSeconds + thirtyMinutes) - elapsedSeconds;
pauseEndTime = Date.now() + (remainingPause * 1000);
- document.getElementById('timerStatus').textContent = `Läuft seit ${timeStr} - Pause (${Math.ceil(remainingPause / 60)} Min)`;
+ setTimerStatus(`Läuft seit ${timeStr} - Pause (${Math.ceil(remainingPause / 60)} Min)`, true);
// Schedule end of pause
pauseTimeout = setTimeout(() => {
@@ -3606,7 +3737,7 @@ async function handleManualStartTime(timeStr) {
isPaused = false;
pauseStartElapsed = 0;
pauseEndTime = 0;
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + timeStr;
+ setTimerStatus('Läuft seit ' + timeStr, true);
}, remainingPause * 1000);
}
// Check if in 9-hour pause (9h30m to 9h45m real time = 9h to 9h work time)
@@ -3616,7 +3747,7 @@ async function handleManualStartTime(timeStr) {
timerPausedDuration = thirtyMinutes;
const remainingPause = (nineHoursSeconds + thirtyMinutes + fifteenMinutes) - elapsedSeconds;
pauseEndTime = Date.now() + (remainingPause * 1000);
- document.getElementById('timerStatus').textContent = `Läuft seit ${timeStr} - Pause (${Math.ceil(remainingPause / 60)} Min)`;
+ setTimerStatus(`Läuft seit ${timeStr} - Pause (${Math.ceil(remainingPause / 60)} Min)`, true);
// Schedule end of pause
pauseTimeout = setTimeout(() => {
@@ -3624,7 +3755,7 @@ async function handleManualStartTime(timeStr) {
isPaused = false;
pauseStartElapsed = 0;
pauseEndTime = 0;
- document.getElementById('timerStatus').textContent = 'Läuft seit ' + timeStr;
+ setTimerStatus('Läuft seit ' + timeStr, true);
}, remainingPause * 1000);
}
// Not in pause, but may have completed pauses
@@ -3719,6 +3850,14 @@ function initializeEventListeners() {
}
});
+ // Manual time link - opens time picker when timer is not running
+ document.getElementById('manualTimeLink').addEventListener('click', () => {
+ // Show custom time picker modal
+ document.getElementById('manualTimePickerModal').classList.remove('hidden');
+ // Set default time to 09:00
+ manualStartTimePicker.setDate('09:00', false);
+ });
+
// Manual time picker - Confirm button
document.getElementById('btnConfirmManualTime').addEventListener('click', async () => {
const timeInput = document.getElementById('manualTimeInput');