Add Employee Timeclock feature with kiosk, attendance report, and payroll CSV export
- New EmployeeClockEntry entity (facility-level attendance, separate from job time entries) - KioskPin added to ApplicationUser; TimeclockKioskToken added to Company - TimeclockController: clock in/out, who's in, 14-day history, manager edit/delete, tablet kiosk with device-cookie auth, PIN management via Users edit page - Kiosk UI: employee tile grid + 4-digit PIN pad + auto-detect clock-in vs clock-out - Attendance report at /Reports/Attendance with weekly subtotal rows - Payroll CSV export at /Reports/AttendanceCsv (flat, one row per segment) - AllowCustomFormulas wired through PlatformSubscriptionController + subscription views - Fix soft-delete bug on CustomItemTemplate (missing HasQueryFilter in OnModelCreating) - Help article (Help/Timeclock.cshtml) and AI knowledge base updated - Migrations: AddEmployeeTimeclock, AddTimeclockKioskToken Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,6 +256,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Kiosk PIN ─────────────────────────────────────────────── *@
|
||||
<div class="d-flex align-items-center gap-2 mb-3 pb-2 border-bottom mt-4">
|
||||
<h5 class="card-title mb-0">Timeclock Kiosk PIN</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Kiosk PIN"
|
||||
data-bs-content="Set a 4-digit PIN so this employee can clock in and out on the shared shop tablet. Leave blank to disable kiosk access for this employee.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">New PIN <span class="text-muted small">(4 digits — blank to disable)</span></label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="kiosk-pin-input" class="form-control" maxlength="4"
|
||||
placeholder="••••" inputmode="numeric" pattern="\d{4}"
|
||||
autocomplete="new-password" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="btn-set-pin">
|
||||
<i class="bi bi-key me-1"></i>Set PIN
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" id="btn-clear-pin">
|
||||
<i class="bi bi-x-circle me-1"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
<div id="pin-feedback" class="form-text mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
@if (!string.IsNullOrEmpty(ViewBag.ReturnUrl))
|
||||
{
|
||||
@@ -277,6 +305,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(function () {
|
||||
var userId = '@Model.Id';
|
||||
var antiForgery = document.querySelector('input[name="__RequestVerificationToken"]');
|
||||
var token = antiForgery ? antiForgery.value : '';
|
||||
|
||||
function pinPost(pin) {
|
||||
var body = new URLSearchParams();
|
||||
body.append('userId', userId);
|
||||
if (pin !== null) body.append('pin', pin);
|
||||
return fetch('/Timeclock/SetKioskPin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': token },
|
||||
body: body.toString()
|
||||
}).then(function (r) {
|
||||
if (!r.ok) return r.json().then(function (e) { throw e.message || 'Error'; });
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-set-pin').addEventListener('click', function () {
|
||||
var val = document.getElementById('kiosk-pin-input').value.trim();
|
||||
if (!/^\d{4}$/.test(val)) {
|
||||
setFeedback('PIN must be exactly 4 digits.', 'danger');
|
||||
return;
|
||||
}
|
||||
pinPost(val).then(function () {
|
||||
setFeedback('PIN set successfully.', 'success');
|
||||
document.getElementById('kiosk-pin-input').value = '';
|
||||
}).catch(function (e) { setFeedback(e, 'danger'); });
|
||||
});
|
||||
|
||||
document.getElementById('btn-clear-pin').addEventListener('click', function () {
|
||||
if (!confirm('Clear this employee's kiosk PIN? They will no longer be able to use the tablet timeclock.')) return;
|
||||
pinPost(null).then(function () {
|
||||
setFeedback('PIN cleared. Employee is now disabled on the kiosk.', 'success');
|
||||
}).catch(function (e) { setFeedback(e, 'danger'); });
|
||||
});
|
||||
|
||||
function setFeedback(msg, type) {
|
||||
var el = document.getElementById('pin-feedback');
|
||||
el.textContent = msg;
|
||||
el.className = 'form-text mt-1 text-' + type;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
|
||||
Reference in New Issue
Block a user