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:
@@ -143,6 +143,13 @@ public class CompanySettingsController : Controller
|
||||
dto.HasCurrentSmsAgreement = await _unitOfWork.CompanySmsAgreements
|
||||
.AnyAsync(a => a.CompanyId == companyId.Value && a.TermsVersion == AppConstants.SmsTermsVersion);
|
||||
|
||||
// Timeclock settings
|
||||
dto.TimeclockEnabled = company.TimeclockEnabled;
|
||||
dto.TimeclockAllowMultiplePunchesPerDay = company.TimeclockAllowMultiplePunchesPerDay;
|
||||
dto.TimeclockAutoClockOutHours = company.TimeclockAutoClockOutHours;
|
||||
var kioskDevices = await _unitOfWork.TimeclockKioskDevices.FindAsync(d => d.CompanyId == companyId.Value);
|
||||
ViewBag.TimeclockKioskDevices = kioskDevices.OrderBy(d => d.ActivatedAt).ToList();
|
||||
|
||||
// Flag whether Stripe Connect is configured (non-placeholder client ID)
|
||||
var connectClientId = _configuration["Stripe:Connect:ConnectClientId"];
|
||||
ViewBag.StripeConnectConfigured = !string.IsNullOrWhiteSpace(connectClientId)
|
||||
@@ -741,6 +748,40 @@ public class CompanySettingsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// POST: CompanySettings/UpdateTimeclockSettings
|
||||
/// <summary>
|
||||
/// Saves company-level timeclock settings (enabled toggle, multiple-punches-per-day, auto clock-out hours).
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> UpdateTimeclockSettings([FromBody] UpdateTimeclockSettingsDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null)
|
||||
return Json(new { success = false, message = "User does not have a company ID." });
|
||||
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value);
|
||||
if (company == null)
|
||||
return Json(new { success = false, message = "Company not found." });
|
||||
|
||||
company.TimeclockEnabled = dto.TimeclockEnabled;
|
||||
company.TimeclockAllowMultiplePunchesPerDay = dto.TimeclockAllowMultiplePunchesPerDay;
|
||||
company.TimeclockAutoClockOutHours = dto.TimeclockAutoClockOutHours;
|
||||
|
||||
await _unitOfWork.Companies.UpdateAsync(company);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogInformation("Company {CompanyId} timeclock settings updated", companyId);
|
||||
return Json(new { success = true, message = "Timeclock settings saved." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating timeclock settings");
|
||||
return Json(new { success = false, message = "An error occurred while saving timeclock settings." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a suggested AI profile draft from existing company configuration — company name/location,
|
||||
/// named ovens, sandblasting capability, shop worker roles, coating inventory categories, and
|
||||
|
||||
Reference in New Issue
Block a user