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:
@@ -0,0 +1,132 @@
|
||||
@model List<AttendanceEmployeeRow>
|
||||
@{
|
||||
ViewData["Title"] = "Attendance Report";
|
||||
ViewData["PageIcon"] = "bi-clock-history";
|
||||
var from = (DateTime)ViewBag.From;
|
||||
var to = (DateTime)ViewBag.To;
|
||||
var isManager = (bool)(ViewBag.IsManager ?? false);
|
||||
|
||||
// ISO week number helper
|
||||
int IsoWeek(DateTime d) => System.Globalization.ISOWeek.GetWeekOfYear(d);
|
||||
}
|
||||
|
||||
<div class="row mb-3 align-items-end">
|
||||
<div class="col">
|
||||
<form method="get" class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<label class="fw-semibold">From</label>
|
||||
<input type="date" name="from" class="form-control form-control-sm" style="width:150px" value="@from.ToString("yyyy-MM-dd")" />
|
||||
<label class="fw-semibold">To</label>
|
||||
<input type="date" name="to" class="form-control form-control-sm" style="width:150px" value="@to.ToString("yyyy-MM-dd")" />
|
||||
<button type="submit" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-search me-1"></i>Filter
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a asp-action="AttendanceCsv"
|
||||
asp-route-from="@from.ToString("yyyy-MM-dd")"
|
||||
asp-route-to="@to.ToString("yyyy-MM-dd")"
|
||||
class="btn btn-sm btn-outline-success">
|
||||
<i class="bi bi-file-earmark-spreadsheet me-1"></i>Export CSV (Payroll)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="alert alert-info alert-permanent">
|
||||
<i class="bi bi-info-circle me-2"></i>No clock entries found for the selected date range.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="pcl-metric-strip mb-4">
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "EMPLOYEES", Value: Model.Count.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "TOTAL HOURS", Value: Model.Sum(r => r.TotalHours).ToString("F1"), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "DAYS COVERED", Value: ((to - from).Days + 1).ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach (var employee in Model)
|
||||
{
|
||||
// Group days into ISO weeks for week subtotals
|
||||
var weekGroups = employee.Days
|
||||
.GroupBy(d => (d.Date.Year, IsoWeek(d.Date)))
|
||||
.OrderByDescending(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-person me-2"></i>@employee.DisplayName
|
||||
</span>
|
||||
<span class="badge bg-primary">@employee.TotalHours.ToString("F2") hrs total</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Clock In</th>
|
||||
<th>Clock Out</th>
|
||||
<th class="text-end">Segment Hrs</th>
|
||||
<th class="text-end">Day Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var wg in weekGroups)
|
||||
{
|
||||
var weekTotal = wg.Sum(d => d.DayTotal);
|
||||
// First day of week group (chronologically earliest) for label
|
||||
var weekStart = wg.Min(d => d.Date);
|
||||
var weekEnd = wg.Max(d => d.Date);
|
||||
|
||||
@foreach (var day in wg.OrderByDescending(d => d.Date))
|
||||
{
|
||||
var segs = day.Segments;
|
||||
@for (int i = 0; i < segs.Count; i++)
|
||||
{
|
||||
var seg = segs[i];
|
||||
<tr>
|
||||
@if (i == 0)
|
||||
{
|
||||
<td class="fw-semibold" rowspan="@segs.Count">
|
||||
@day.Date.ToString("ddd M/d/yy")
|
||||
</td>
|
||||
}
|
||||
<td>@seg.ClockInTime.ToLocalTime().ToString("h:mm tt")</td>
|
||||
<td>
|
||||
@if (seg.ClockOutTime.HasValue)
|
||||
{ @seg.ClockOutTime.Value.ToLocalTime().ToString("h:mm tt") }
|
||||
else
|
||||
{ <span class="badge bg-success">In Progress</span> }
|
||||
</td>
|
||||
<td class="text-end">@Html.Raw(seg.HoursWorked?.ToString("F2") ?? "—")</td>
|
||||
@if (i == 0)
|
||||
{
|
||||
<td class="text-end fw-semibold" rowspan="@segs.Count">
|
||||
@day.DayTotal.ToString("F2")h
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
<tr class="table-secondary">
|
||||
<td colspan="4" class="text-end text-muted small fst-italic">
|
||||
Week of @weekStart.ToString("M/d") – @weekEnd.ToString("M/d")
|
||||
</td>
|
||||
<td class="text-end fw-bold small">@weekTotal.ToString("F2")h</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,14 @@
|
||||
<p>Monthly revenue, job count, and average order value trends with breakdowns by customer type and priority.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="Attendance" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ecfdf5;color:#059669;">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</div>
|
||||
<h5>Attendance</h5>
|
||||
<p>Daily punch detail and total hours per employee. See who clocked in, when, and how long each segment lasted.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user