Add Timeclock settings tab in Company Settings with multi-kiosk support
Settings tab (Company Settings > Timeclock): - Enable/disable timeclock toggle (hides nav link and attendance report when off) - Allow multiple clock-ins per day toggle - Auto clock-out after X hours (auto-closes forgotten open entries on next punch) - Kiosk devices table: lists activated tablets with name, activated date, last seen; Deactivate button removes that device's access immediately Multi-kiosk support (replaces single TimeclockKioskToken on Company): - New TimeclockKioskDevice entity (one row per tablet, unique token, DeviceName, LastSeenAt) - KioskActivate GET shows a form for optional device name before activating - KioskDeactivate POST accepts device ID, deletes specific row (not all devices) - Kiosk validation (Kiosk, KioskEmployees, KioskPunch) queries device table with ignoreQueryFilters since no user is logged in on kiosk requests - LastSeenAt updated on each Kiosk page load Enforcement: - ClockIn and KioskPunch both auto-close stale entries if AutoClockOutHours is set - ClockIn and KioskPunch both block second same-day punch if AllowMultiplePunches=false - TimeclockEnabled=false hides nav link (SubscriptionMiddleware sets Items key) and returns Forbid on kiosk punch - Migration: AddTimeclockSettings (adds 3 columns to Companies, new TimeclockKioskDevices table) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,11 @@
|
||||
<i class="bi bi-tablet"></i> Kiosk
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="timeclock-tab" data-bs-toggle="tab" data-bs-target="#timeclock" type="button" role="tab">
|
||||
<i class="bi bi-clock-history"></i> Timeclock
|
||||
</button>
|
||||
</li>
|
||||
@if (ViewBag.AllowCustomFormulas == true)
|
||||
{
|
||||
<li class="nav-item" role="presentation">
|
||||
@@ -2062,6 +2067,108 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Timeclock ─────────────────────────────────────────────────── -->
|
||||
<div class="tab-pane fade" id="timeclock" role="tabpanel">
|
||||
<div class="card shadow-sm mt-3">
|
||||
<div class="card-header fw-semibold"><i class="bi bi-clock-history me-2"></i>Timeclock Settings</div>
|
||||
<div class="card-body">
|
||||
|
||||
@{
|
||||
var kioskDevices = ViewBag.TimeclockKioskDevices as List<PowderCoating.Core.Entities.TimeclockKioskDevice> ?? new();
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="timeclockEnabled" @(Model.TimeclockEnabled ? "checked" : "") />
|
||||
<label class="form-check-label fw-semibold" for="timeclockEnabled">Enable Timeclock</label>
|
||||
</div>
|
||||
<div class="form-text">When disabled, the Timeclock link is hidden from the navigation and the Attendance report is not accessible.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="timeclockMultiplePunches" @(Model.TimeclockAllowMultiplePunchesPerDay ? "checked" : "") />
|
||||
<label class="form-check-label fw-semibold" for="timeclockMultiplePunches">Allow multiple clock-ins per day</label>
|
||||
</div>
|
||||
<div class="form-text">When enabled, employees can clock in and out multiple times per day (e.g. for lunch breaks). When disabled, each employee is limited to one in/out pair per day.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold" for="timeclockAutoClockOut">Auto clock-out after</label>
|
||||
<div class="input-group" style="max-width:220px;">
|
||||
<input type="number" id="timeclockAutoClockOut" class="form-control" min="1" max="24"
|
||||
value="@(Model.TimeclockAutoClockOutHours?.ToString() ?? "")"
|
||||
placeholder="Disabled" />
|
||||
<span class="input-group-text">hours</span>
|
||||
</div>
|
||||
<div class="form-text">If an employee forgets to clock out, the system will automatically clock them out after this many hours. Leave blank to disable. Maximum 24 hours.</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="btn-save-timeclock">
|
||||
<i class="bi bi-save me-1"></i>Save Timeclock Settings
|
||||
</button>
|
||||
<span id="timeclock-status" class="ms-2 small"></span>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h6 class="fw-semibold mb-1">Kiosk Tablets</h6>
|
||||
<p class="text-muted small mb-3">
|
||||
Activate the timeclock kiosk on any shop-floor tablet. Employees tap their name and enter their PIN to clock in or out — no login required.
|
||||
Multiple tablets are supported; each device is listed below.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-info alert-permanent small mb-3">
|
||||
<i class="bi bi-tablet me-2"></i>
|
||||
<strong>To activate a new kiosk:</strong> on the tablet's browser, navigate to
|
||||
<code>/Timeclock/Kiosk/Activate</code>, optionally enter a device name, and click
|
||||
“Activate This Device.” The tablet will then show the employee clock-in grid at
|
||||
<code>/Timeclock/Kiosk</code>.
|
||||
</div>
|
||||
|
||||
@if (!kioskDevices.Any())
|
||||
{
|
||||
<p class="text-muted small fst-italic">No kiosk devices activated yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Device Name</th>
|
||||
<th>Activated</th>
|
||||
<th>Last Seen</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var dev in kioskDevices)
|
||||
{
|
||||
<tr data-device-id="@dev.Id">
|
||||
<td class="fw-semibold">@(dev.DeviceName ?? "Unnamed Device")</td>
|
||||
<td class="text-muted small">@dev.ActivatedAt.ToLocalTime().ToString("M/d/yy h:mm tt")</td>
|
||||
<td class="text-muted small">
|
||||
@if (dev.LastSeenAt.HasValue)
|
||||
{ @dev.LastSeenAt.Value.ToLocalTime().ToString("M/d/yy h:mm tt") }
|
||||
else
|
||||
{ <span class="fst-italic">Never used</span> }
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-xs btn-outline-danger btn-deactivate-kiosk"
|
||||
data-id="@dev.Id"
|
||||
data-name="@(dev.DeviceName ?? "Unnamed Device")">
|
||||
<i class="bi bi-x-circle me-1"></i>Deactivate
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Custom Formula Item Templates ──────────────────────────────── -->
|
||||
@if (ViewBag.AllowCustomFormulas == true)
|
||||
{
|
||||
@@ -3478,6 +3585,60 @@
|
||||
if (btn) new bootstrap.Tab(btn).show();
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
// Timeclock settings
|
||||
(function () {
|
||||
var antiForgery = document.querySelector('input[name="__RequestVerificationToken"]');
|
||||
var token = antiForgery ? antiForgery.value : '';
|
||||
|
||||
function setStatus(msg, ok) {
|
||||
var el = document.getElementById('timeclock-status');
|
||||
el.textContent = msg;
|
||||
el.className = 'ms-2 small text-' + (ok ? 'success' : 'danger');
|
||||
setTimeout(function () { el.textContent = ''; }, 4000);
|
||||
}
|
||||
|
||||
document.getElementById('btn-save-timeclock').addEventListener('click', function () {
|
||||
var hours = document.getElementById('timeclockAutoClockOut').value.trim();
|
||||
fetch('/CompanySettings/UpdateTimeclockSettings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': token },
|
||||
body: JSON.stringify({
|
||||
TimeclockEnabled: document.getElementById('timeclockEnabled').checked,
|
||||
TimeclockAllowMultiplePunchesPerDay: document.getElementById('timeclockMultiplePunches').checked,
|
||||
TimeclockAutoClockOutHours: hours !== '' ? parseInt(hours, 10) : null
|
||||
})
|
||||
}).then(function (r) { return r.json(); }).then(function (res) {
|
||||
setStatus(res.message, res.success);
|
||||
}).catch(function () { setStatus('Error saving settings.', false); });
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-deactivate-kiosk').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = btn.dataset.id;
|
||||
var name = btn.dataset.name;
|
||||
if (!confirm('Deactivate "' + name + '"? The tablet will lose kiosk access immediately.')) return;
|
||||
fetch('/Timeclock/KioskDeactivate/' + id, {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': token }
|
||||
}).then(function (r) { return r.json(); }).then(function (res) {
|
||||
if (res.success) {
|
||||
var row = document.querySelector('tr[data-device-id="' + id + '"]');
|
||||
if (row) row.remove();
|
||||
} else {
|
||||
alert(res.message || 'Error deactivating device.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-show tab if URL fragment matches
|
||||
if (window.location.hash === '#timeclock' || new URLSearchParams(window.location.search).get('tab') === 'timeclock') {
|
||||
var btn = document.querySelector('[data-bs-target="#timeclock"]');
|
||||
if (btn) new bootstrap.Tab(btn).show();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/company-settings-custom-formulas.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
// Load formula templates when the tab is first shown; auto-show walkthrough if no templates yet
|
||||
|
||||
@@ -1270,10 +1270,13 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
|
||||
<span>Maintenance</span>
|
||||
</a>
|
||||
}
|
||||
@if (Context.Items["TimeclockEnabled"] as bool? == true)
|
||||
{
|
||||
<a asp-controller="Timeclock" asp-action="Index" class="nav-link" data-nav="ops">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
<span>Timeclock</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@* ── Reports & Templates ──────────────────────────────────── *@
|
||||
@if (hasReports || hasJobs)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@{
|
||||
ViewData["Title"] = "Activate Kiosk Device";
|
||||
Layout = "_KioskLayout";
|
||||
}
|
||||
|
||||
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height:80vh;">
|
||||
<div class="card shadow-lg" style="max-width:420px;width:100%;">
|
||||
<div class="card-header text-center py-3">
|
||||
<h4 class="mb-0"><i class="bi bi-tablet me-2"></i>Activate This Device</h4>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<p class="text-muted text-center mb-4">
|
||||
This will register the current device as a Timeclock kiosk tablet.
|
||||
Employees will be able to clock in and out without logging in.
|
||||
</p>
|
||||
<form method="post" asp-action="KioskActivatePost" asp-route-area="">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Device Name <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<input type="text" name="deviceName" class="form-control form-control-lg"
|
||||
placeholder="e.g. Front Entrance Tablet" maxlength="100" />
|
||||
<div class="form-text">Helps you identify this device on the Timeclock Settings page.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-lg w-100">
|
||||
<i class="bi bi-check-circle me-2"></i>Activate This Device
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center text-muted small py-3">
|
||||
Only Managers and Company Admins can activate kiosk devices.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user