Add timeclock break/lunch tracking, manual entries, and attendance period picker
- ClockEntryType enum (Work/Break/Lunch) on EmployeeClockEntry; default 0 = Work so all existing entries are unaffected - Migration AddClockEntryType applied - Break and Lunch buttons on clock status card (only when AllowMultiplePunchesPerDay is enabled); GoOnBreak closes current Work segment and opens Break/Lunch segment - Return to Work button when on break/lunch; closes break segment, opens new Work - Status badges on clock card and Who'\''s In grid: Working / On Break / At Lunch - Break/Lunch hours excluded from all day totals, week totals, metrics, and CSV - Manager: Manual Entry modal to create a time entry for any company employee - Attendance report defaults to current ISO week; Week/Month mode toggle with auto-submitting dropdowns (last 12 weeks or months); period label shown inline - Attendance CSV: Type column added; day/week totals blank on Break/Lunch rows; filename uses period label - Week subtotal rows suppressed in single-week view (shown in month view only) - Help article and AI knowledge base updated for all new features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,16 +9,19 @@ var Timeclock = (function () {
|
||||
scheduleWhosInRefresh();
|
||||
}
|
||||
|
||||
/* ── Clock In / Out ────────────────────────────────────────────────────── */
|
||||
/* ── Clock In / Out / Break / Lunch / Return ───────────────────────────── */
|
||||
function bindClockButtons() {
|
||||
var btnIn = document.getElementById('btn-clock-in');
|
||||
var btnOut = document.getElementById('btn-clock-out');
|
||||
var btnIn = document.getElementById('btn-clock-in');
|
||||
var btnOut = document.getElementById('btn-clock-out');
|
||||
var btnBreak = document.getElementById('btn-break');
|
||||
var btnLunch = document.getElementById('btn-lunch');
|
||||
var btnReturn = document.getElementById('btn-return-from-break');
|
||||
|
||||
if (btnIn) {
|
||||
btnIn.addEventListener('click', function () {
|
||||
btnIn.disabled = true;
|
||||
postJson('/Timeclock/ClockIn', {})
|
||||
.then(function (r) { location.reload(); })
|
||||
.then(function () { location.reload(); })
|
||||
.catch(function (err) {
|
||||
showFeedback(err, 'danger');
|
||||
btnIn.disabled = false;
|
||||
@@ -31,13 +34,40 @@ var Timeclock = (function () {
|
||||
btnOut.disabled = true;
|
||||
var entryId = parseInt(btnOut.getAttribute('data-entry-id'));
|
||||
postJson('/Timeclock/ClockOut', { entryId: entryId })
|
||||
.then(function (r) { location.reload(); })
|
||||
.then(function () { location.reload(); })
|
||||
.catch(function (err) {
|
||||
showFeedback(err, 'danger');
|
||||
btnOut.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Break button (type 1) and Lunch button (type 2) share the same handler
|
||||
[btnBreak, btnLunch].forEach(function (btn) {
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function () {
|
||||
btn.disabled = true;
|
||||
var breakType = parseInt(btn.getAttribute('data-break-type'));
|
||||
postJson('/Timeclock/GoOnBreak', { breakType: breakType })
|
||||
.then(function () { location.reload(); })
|
||||
.catch(function (err) {
|
||||
showFeedback(err, 'danger');
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (btnReturn) {
|
||||
btnReturn.addEventListener('click', function () {
|
||||
btnReturn.disabled = true;
|
||||
postJson('/Timeclock/ReturnFromBreak', {})
|
||||
.then(function () { location.reload(); })
|
||||
.catch(function (err) {
|
||||
showFeedback(err, 'danger');
|
||||
btnReturn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Who's In refresh every 60 s ──────────────────────────────────────── */
|
||||
@@ -58,7 +88,7 @@ var Timeclock = (function () {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Manager: edit / delete ────────────────────────────────────────────── */
|
||||
/* ── Manager: edit / delete / manual entry ────────────────────────────── */
|
||||
function bindManagerEvents() {
|
||||
if (!_cfg.isManager) return;
|
||||
|
||||
@@ -72,6 +102,21 @@ var Timeclock = (function () {
|
||||
|
||||
var btnLoad = document.getElementById('btn-manager-load');
|
||||
if (btnLoad) btnLoad.addEventListener('click', loadManagerHistory);
|
||||
|
||||
var btnSaveManual = document.getElementById('btn-save-manual-entry');
|
||||
if (btnSaveManual) btnSaveManual.addEventListener('click', saveManualEntry);
|
||||
|
||||
// Pre-fill clock-in to "now" each time the modal opens
|
||||
var manualModal = document.getElementById('manualEntryModal');
|
||||
if (manualModal) {
|
||||
manualModal.addEventListener('show.bs.modal', function () {
|
||||
document.getElementById('manual-clock-in').value = toDatetimeLocal(new Date().toISOString());
|
||||
document.getElementById('manual-clock-out').value = '';
|
||||
document.getElementById('manual-notes').value = '';
|
||||
document.getElementById('manual-employee').value = '';
|
||||
setManualFeedback('', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openEditModal(btn) {
|
||||
@@ -109,6 +154,44 @@ var Timeclock = (function () {
|
||||
.catch(function (err) { alert('Error: ' + err); });
|
||||
}
|
||||
|
||||
function saveManualEntry() {
|
||||
var userId = document.getElementById('manual-employee').value;
|
||||
var inVal = document.getElementById('manual-clock-in').value;
|
||||
var outVal = document.getElementById('manual-clock-out').value;
|
||||
var notes = document.getElementById('manual-notes').value;
|
||||
|
||||
if (!userId) { setManualFeedback('Please select an employee.', 'danger'); return; }
|
||||
if (!inVal) { setManualFeedback('Clock-in time is required.', 'danger'); return; }
|
||||
if (outVal && new Date(outVal) <= new Date(inVal)) {
|
||||
setManualFeedback('Clock-out must be after clock-in.', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('btn-save-manual-entry');
|
||||
btn.disabled = true;
|
||||
|
||||
postJson('/Timeclock/ManualEntry', {
|
||||
userId: userId,
|
||||
clockInTime: new Date(inVal).toISOString(),
|
||||
clockOutTime: outVal ? new Date(outVal).toISOString() : null,
|
||||
notes: notes || null
|
||||
}).then(function () {
|
||||
bootstrap.Modal.getInstance(document.getElementById('manualEntryModal')).hide();
|
||||
location.reload();
|
||||
}).catch(function (err) {
|
||||
setManualFeedback(err || 'Failed to save entry.', 'danger');
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function setManualFeedback(msg, type) {
|
||||
var el = document.getElementById('manual-entry-feedback');
|
||||
if (!el) return;
|
||||
if (!msg) { el.className = 'd-none'; el.textContent = ''; return; }
|
||||
el.className = 'alert alert-' + type + ' alert-permanent py-2';
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
function loadManagerHistory() {
|
||||
var from = document.getElementById('manager-from').value;
|
||||
var to = document.getElementById('manager-to').value;
|
||||
@@ -128,12 +211,18 @@ var Timeclock = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
var TYPE_LABELS = ['<span class="badge bg-success">Work</span>',
|
||||
'<span class="badge bg-warning text-dark">Break</span>',
|
||||
'<span class="badge bg-info text-dark">Lunch</span>'];
|
||||
|
||||
var rows = entries.map(function (e) {
|
||||
var inTime = new Date(e.clockInTime).toLocaleString();
|
||||
var outTime = e.clockOutTime ? new Date(e.clockOutTime).toLocaleString() : '—';
|
||||
var hours = e.hoursWorked != null ? e.hoursWorked.toFixed(2) : '—';
|
||||
var inTime = new Date(e.clockInTime).toLocaleString();
|
||||
var outTime = e.clockOutTime ? new Date(e.clockOutTime).toLocaleString() : '—';
|
||||
var hours = e.hoursWorked != null ? e.hoursWorked.toFixed(2) : '—';
|
||||
var typeBadge = TYPE_LABELS[e.entryType] || TYPE_LABELS[0];
|
||||
return '<tr>' +
|
||||
'<td>' + escHtml(e.userDisplayName) + '</td>' +
|
||||
'<td>' + typeBadge + '</td>' +
|
||||
'<td>' + inTime + '</td>' +
|
||||
'<td>' + outTime + '</td>' +
|
||||
'<td>' + hours + '</td>' +
|
||||
@@ -143,7 +232,7 @@ var Timeclock = (function () {
|
||||
|
||||
container.innerHTML = '<table class="table table-hover table-sm mb-0">' +
|
||||
'<thead class="table-light"><tr>' +
|
||||
'<th>Employee</th><th>Clock In</th><th>Clock Out</th><th>Hours</th><th>Notes</th>' +
|
||||
'<th>Employee</th><th>Type</th><th>Clock In</th><th>Clock Out</th><th>Hours</th><th>Notes</th>' +
|
||||
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user