From 97745f9a655129e402984a4d61b5ad900180c320 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 27 May 2026 00:12:46 -0400 Subject: [PATCH] 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 --- .../DTOs/Company/CompanySettingsDtos.cs | 14 ++ src/PowderCoating.Core/Entities/Company.cs | 13 +- .../Entities/TimeclockKioskDevice.cs | 22 ++ .../Interfaces/IUnitOfWork.cs | 1 + .../Data/ApplicationDbContext.cs | 11 + .../Repositories/UnitOfWork.cs | 5 + .../Controllers/CompanySettingsController.cs | 41 ++++ .../Controllers/TimeclockController.cs | 211 +++++++++++++++--- .../Middleware/SubscriptionMiddleware.cs | 2 + .../Views/CompanySettings/Index.cshtml | 161 +++++++++++++ .../Views/Shared/_Layout.cshtml | 3 + .../Views/Timeclock/KioskActivate.cshtml | 33 +++ 12 files changed, 479 insertions(+), 38 deletions(-) create mode 100644 src/PowderCoating.Core/Entities/TimeclockKioskDevice.cs create mode 100644 src/PowderCoating.Web/Views/Timeclock/KioskActivate.cshtml diff --git a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs index f8df2c1..f172fd3 100644 --- a/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs +++ b/src/PowderCoating.Application/DTOs/Company/CompanySettingsDtos.cs @@ -44,6 +44,20 @@ namespace PowderCoating.Application.DTOs.Company /// True when the company has an accepted agreement for the current SmsTermsVersion. public bool HasCurrentSmsAgreement { get; set; } public string SmsTermsVersion { get; set; } = string.Empty; + + // Timeclock settings + public bool TimeclockEnabled { get; set; } + public bool TimeclockAllowMultiplePunchesPerDay { get; set; } + public int? TimeclockAutoClockOutHours { get; set; } + } + + /// DTO for updating company-level timeclock settings from the Settings tab. + public class UpdateTimeclockSettingsDto + { + public bool TimeclockEnabled { get; set; } + public bool TimeclockAllowMultiplePunchesPerDay { get; set; } + [Range(1, 24, ErrorMessage = "Auto clock-out must be between 1 and 24 hours.")] + public int? TimeclockAutoClockOutHours { get; set; } } /// diff --git a/src/PowderCoating.Core/Entities/Company.cs b/src/PowderCoating.Core/Entities/Company.cs index 4ee522b..9a533cb 100644 --- a/src/PowderCoating.Core/Entities/Company.cs +++ b/src/PowderCoating.Core/Entities/Company.cs @@ -133,11 +133,14 @@ public class Company : BaseEntity /// public string? KioskActivationToken { get; set; } - /// - /// Device activation token for the Timeclock kiosk tablet. - /// Null = timeclock kiosk not activated on any device. - /// - public string? TimeclockKioskToken { get; set; } + /// Timeclock feature enabled for this company. When false, the nav link, dashboard, and reports are hidden. + public bool TimeclockEnabled { get; set; } = true; + + /// When true, employees can clock in/out multiple times per day (lunch breaks, etc.). When false, only one in/out pair is allowed per day. + public bool TimeclockAllowMultiplePunchesPerDay { get; set; } = true; + + /// If set, any open clock entry older than this many hours is automatically closed on the next clock-in. Null = no auto clock-out. + public int? TimeclockAutoClockOutHours { get; set; } // Navigation Properties public virtual ICollection Users { get; set; } = new List(); diff --git a/src/PowderCoating.Core/Entities/TimeclockKioskDevice.cs b/src/PowderCoating.Core/Entities/TimeclockKioskDevice.cs new file mode 100644 index 0000000..0d3c630 --- /dev/null +++ b/src/PowderCoating.Core/Entities/TimeclockKioskDevice.cs @@ -0,0 +1,22 @@ +namespace PowderCoating.Core.Entities; + +/// +/// Represents an activated shop-floor kiosk tablet for the timeclock. +/// One row per device; multiple rows per company are supported so shops can have +/// tablets at multiple entry points. The is stored in a +/// device-specific cookie and validated on every kiosk request. +/// +public class TimeclockKioskDevice : BaseEntity +{ + /// Human-readable label for this device (e.g. "Front Entrance Tablet"). + public string? DeviceName { get; set; } + + /// Cryptographically random token written to the device cookie on activation. Revoke by deleting this row. + public string Token { get; set; } = string.Empty; + + /// UTC timestamp when a manager activated this device. + public DateTime ActivatedAt { get; set; } + + /// UTC timestamp of the most recent kiosk request from this device; null if never used after activation. + public DateTime? LastSeenAt { get; set; } +} diff --git a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs index 86e92b7..37687db 100644 --- a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs +++ b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs @@ -160,6 +160,7 @@ IRepository ReworkRecords { get; } // Employee Timeclock IRepository EmployeeClockEntries { get; } + IRepository TimeclockKioskDevices { get; } Task SaveChangesAsync(); Task CompleteAsync(); // Alias for SaveChangesAsync diff --git a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs index a6294f0..d30ce26 100644 --- a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs +++ b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs @@ -382,6 +382,9 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro /// Facility-level clock-in/clock-out entries per employee; tenant-filtered with soft delete. Multiple entries per day are supported (lunch breaks, etc.). public DbSet EmployeeClockEntries { get; set; } + /// One row per activated kiosk tablet per company. Token stored in device cookie; delete row to revoke a device. + public DbSet TimeclockKioskDevices { get; set; } + /// /// Platform-wide audit log capturing who changed what and when, across all tenants. /// No global query filter — SuperAdmin controllers query this directly. @@ -793,6 +796,14 @@ modelBuilder.Entity().HasQueryFilter(e => modelBuilder.Entity() .HasIndex(c => new { c.CompanyId, c.ClockInTime }); + // Timeclock kiosk devices — one row per activated tablet per company + modelBuilder.Entity().HasQueryFilter(d => + !d.IsDeleted && (IsPlatformAdmin || d.CompanyId == CurrentCompanyId)); + modelBuilder.Entity() + .HasIndex(d => d.Token).IsUnique(); + modelBuilder.Entity() + .HasIndex(d => d.CompanyId); + // Account self-referencing hierarchy modelBuilder.Entity() .HasOne(a => a.ParentAccount) diff --git a/src/PowderCoating.Infrastructure/Repositories/UnitOfWork.cs b/src/PowderCoating.Infrastructure/Repositories/UnitOfWork.cs index 6e98c0f..345b534 100644 --- a/src/PowderCoating.Infrastructure/Repositories/UnitOfWork.cs +++ b/src/PowderCoating.Infrastructure/Repositories/UnitOfWork.cs @@ -125,6 +125,7 @@ public class UnitOfWork : IUnitOfWork // Employee Timeclock private IRepository? _employeeClockEntries; + private IRepository? _timeclockKioskDevices; // Custom Formula Templates private IRepository? _customItemTemplates; @@ -467,6 +468,10 @@ public class UnitOfWork : IUnitOfWork public IRepository EmployeeClockEntries => _employeeClockEntries ??= new Repository(_context); + /// Repository for activated tablet records; one row per device. Delete a row to revoke that device's access. + public IRepository TimeclockKioskDevices => + _timeclockKioskDevices ??= new Repository(_context); + /// Repository for per-company reusable NCalc pricing formula templates; tenant-filtered with soft delete. public IRepository CustomItemTemplates => _customItemTemplates ??= new Repository(_context); diff --git a/src/PowderCoating.Web/Controllers/CompanySettingsController.cs b/src/PowderCoating.Web/Controllers/CompanySettingsController.cs index 15c3253..6104f35 100644 --- a/src/PowderCoating.Web/Controllers/CompanySettingsController.cs +++ b/src/PowderCoating.Web/Controllers/CompanySettingsController.cs @@ -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 + /// + /// Saves company-level timeclock settings (enabled toggle, multiple-punches-per-day, auto clock-out hours). + /// + [HttpPost] + public async Task 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." }); + } + } + /// /// Builds a suggested AI profile draft from existing company configuration — company name/location, /// named ovens, sandblasting capability, shop worker roles, coating inventory categories, and diff --git a/src/PowderCoating.Web/Controllers/TimeclockController.cs b/src/PowderCoating.Web/Controllers/TimeclockController.cs index ee34c6b..a0aee11 100644 --- a/src/PowderCoating.Web/Controllers/TimeclockController.cs +++ b/src/PowderCoating.Web/Controllers/TimeclockController.cs @@ -131,24 +131,59 @@ public class TimeclockController : Controller return Json(result); } - /// Clocks the current user in. Returns an error if already clocked in. + /// + /// Clocks the current user in. Enforces three company-level timeclock settings: + /// (1) auto-closes stale open entries if AutoClockOutHours is set, + /// (2) blocks a second punch today if AllowMultiplePunchesPerDay is false, + /// (3) blocks immediately if already clocked in. + /// [HttpPost] public async Task ClockIn([FromBody] ClockInRequest request) { var userId = _userManager.GetUserId(User)!; var companyId = GetCurrentCompanyId(); + var company = await _unitOfWork.Companies.GetByIdAsync(companyId); + if (company == null) return Forbid(); + var now = DateTime.UtcNow; + + // Auto clock-out stale open entries if configured + if (company.TimeclockAutoClockOutHours is > 0) + { + var cutoff = now.AddHours(-company.TimeclockAutoClockOutHours.Value); + var stale = await _unitOfWork.EmployeeClockEntries.FindAsync( + e => e.UserId == userId && e.ClockOutTime == null + && e.CompanyId == companyId && e.ClockInTime <= cutoff); + foreach (var staleEntry in stale) + { + staleEntry.ClockOutTime = staleEntry.ClockInTime.AddHours(company.TimeclockAutoClockOutHours.Value); + staleEntry.HoursWorked = company.TimeclockAutoClockOutHours.Value; + staleEntry.Notes = (staleEntry.Notes + " [Auto clocked out]").Trim(); + } + if (stale.Any()) await _unitOfWork.CompleteAsync(); + } + + // Still clocked in? var alreadyOpen = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync( e => e.UserId == userId && e.ClockOutTime == null && e.CompanyId == companyId); - if (alreadyOpen != null) return BadRequest(new { message = "You are already clocked in. Please clock out first." }); + // Block second punch today if setting disallows it + if (!company.TimeclockAllowMultiplePunchesPerDay) + { + var todayStart = now.Date; + var punchedToday = await _unitOfWork.EmployeeClockEntries.AnyAsync( + e => e.UserId == userId && e.CompanyId == companyId && e.ClockInTime >= todayStart); + if (punchedToday) + return BadRequest(new { message = "Multiple clock-ins per day are not allowed. You have already clocked in today." }); + } + var entry = new EmployeeClockEntry { UserId = userId, CompanyId = companyId, - ClockInTime = DateTime.UtcNow, + ClockInTime = now, Notes = request.Notes?.Trim() }; @@ -264,64 +299,109 @@ public class TimeclockController : Controller // ========================================================================= /// - /// Manager activates a tablet device by navigating to this URL. - /// Generates (or regenerates) the company's TimeclockKioskToken and writes the device cookie. + /// Shows the kiosk activation form where a manager can optionally name the device + /// before activating it. Navigate to this URL on each tablet to register it. /// [HttpGet("Timeclock/Kiosk/Activate")] public async Task KioskActivate() { if (!await IsManagerAsync()) return Forbid(); - - var companyId = GetCurrentCompanyId(); - var company = await _unitOfWork.Companies.GetByIdAsync(companyId); - if (company == null) return NotFound(); - - company.TimeclockKioskToken = Guid.NewGuid().ToString("N"); - await _unitOfWork.CompleteAsync(); - - WriteTimeclockKioskCookie(companyId, company.TimeclockKioskToken); - - TempData["Success"] = "Timeclock kiosk activated on this device."; - return RedirectToAction(nameof(Kiosk)); + return View(); } - /// Deactivates the timeclock kiosk — clears the token and removes the cookie. - [HttpPost("Timeclock/Kiosk/Deactivate")] - public async Task KioskDeactivate() + /// + /// Creates a new row and writes the device cookie for + /// this tablet. Multiple devices per company are supported — each tablet gets its own token. + /// + [HttpPost("Timeclock/Kiosk/Activate")] + [ValidateAntiForgeryToken] + public async Task KioskActivatePost(string? deviceName) { if (!await IsManagerAsync()) return Forbid(); var companyId = GetCurrentCompanyId(); - var company = await _unitOfWork.Companies.GetByIdAsync(companyId); - if (company != null) - { - company.TimeclockKioskToken = null; - await _unitOfWork.CompleteAsync(); - } + var token = Guid.NewGuid().ToString("N"); + + var device = new TimeclockKioskDevice + { + CompanyId = companyId, + Token = token, + DeviceName = string.IsNullOrWhiteSpace(deviceName) ? null : deviceName.Trim(), + ActivatedAt = DateTime.UtcNow + }; + + await _unitOfWork.TimeclockKioskDevices.AddAsync(device); + await _unitOfWork.CompleteAsync(); + + WriteTimeclockKioskCookie(companyId, token); + + TempData["Success"] = "Kiosk activated on this device."; + return RedirectToAction(nameof(Kiosk)); + } + + /// Deactivates a specific kiosk device by ID. Deletes its row so the cookie is invalidated on next use. + [HttpPost] + public async Task KioskDeactivate(int id) + { + if (!await IsManagerAsync()) return Json(new { success = false, message = "Forbidden." }); + + var companyId = GetCurrentCompanyId(); + var device = await _unitOfWork.TimeclockKioskDevices.GetByIdAsync(id); + if (device == null || device.CompanyId != companyId) + return Json(new { success = false, message = "Device not found." }); + + await _unitOfWork.TimeclockKioskDevices.DeleteAsync(device); + await _unitOfWork.CompleteAsync(); + + // If the current browser holds this device's cookie, clear it too + var cookie = ReadTimeclockKioskCookie(); + if (cookie?.token == device.Token) + DeleteTimeclockKioskCookie(); - DeleteTimeclockKioskCookie(); return Json(new { success = true }); } + /// Returns the list of active kiosk devices for this company. Used by the Settings tab. + [HttpGet] + public async Task KioskDevices() + { + if (!await IsManagerAsync()) return Forbid(); + + var companyId = GetCurrentCompanyId(); + var devices = await _unitOfWork.TimeclockKioskDevices.FindAsync( + d => d.CompanyId == companyId); + + var result = devices.OrderBy(d => d.ActivatedAt).Select(d => new + { + id = d.Id, + deviceName = d.DeviceName ?? "Unnamed Device", + activatedAt = d.ActivatedAt, + lastSeenAt = d.LastSeenAt + }); + + return Json(result); + } + // ========================================================================= // KIOSK — tablet UI // ========================================================================= /// /// Tablet timeclock kiosk page. Requires device-cookie auth; no user login needed. - /// Shows employee tiles for all active employees who have a KioskPin set. + /// Validates the cookie token against rows (supports multiple tablets per company). /// [AllowAnonymous] [HttpGet("Timeclock/Kiosk")] public async Task Kiosk() { - var cookie = ReadTimeclockKioskCookie(); - if (cookie == null) + var (device, company) = await ValidateKioskCookieAsync(); + if (device == null || company == null) return View("KioskError", "This device is not activated as a timeclock kiosk. Ask a manager to activate it at Timeclock › Activate Kiosk."); - var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true); - if (company == null || company.TimeclockKioskToken != cookie.Value.token) - return View("KioskError", "Kiosk activation is invalid or has been revoked. Ask a manager to re-activate this device."); + if (!company.TimeclockEnabled) + return View("KioskError", "Timeclock is currently disabled for this company."); + + await TouchDeviceAsync(device); ViewBag.CompanyName = company.CompanyName; ViewBag.CompanyId = company.Id; @@ -338,6 +418,12 @@ public class TimeclockController : Controller var cookie = ReadTimeclockKioskCookie(); if (cookie == null) return Forbid(); + // Validate device (no need to touch LastSeenAt on every poll) + var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync( + d => d.CompanyId == cookie.Value.companyId && d.Token == cookie.Value.token, + true); + if (device == null) return Forbid(); + var companyId = cookie.Value.companyId; var users = await _userManager.Users @@ -373,6 +459,13 @@ public class TimeclockController : Controller var companyId = cookie.Value.companyId; + var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync( + d => d.CompanyId == companyId && d.Token == cookie.Value.token, true); + if (device == null) return Forbid(); + + var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true); + if (company == null || !company.TimeclockEnabled) return Forbid(); + var user = await _userManager.FindByIdAsync(request.UserId); if (user == null || user.CompanyId != companyId || !user.IsActive || user.KioskPin == null) return BadRequest(new { message = "Employee not found or kiosk disabled." }); @@ -384,6 +477,22 @@ public class TimeclockController : Controller var now = DateTime.UtcNow; + // Auto clock-out stale open entries (shared logic with main-app ClockIn) + if (company.TimeclockAutoClockOutHours is > 0) + { + var cutoff = now.AddHours(-company.TimeclockAutoClockOutHours.Value); + var stale = await _unitOfWork.EmployeeClockEntries.FindAsync( + e => e.UserId == user.Id && e.ClockOutTime == null + && e.CompanyId == companyId && e.ClockInTime <= cutoff); + foreach (var s in stale) + { + s.ClockOutTime = s.ClockInTime.AddHours(company.TimeclockAutoClockOutHours.Value); + s.HoursWorked = company.TimeclockAutoClockOutHours.Value; + s.Notes = (s.Notes + " [Auto clocked out]").Trim(); + } + if (stale.Any()) await _unitOfWork.CompleteAsync(); + } + // Check for open entry var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync( e => e.UserId == user.Id && e.ClockOutTime == null && e.CompanyId == companyId); @@ -402,6 +511,15 @@ public class TimeclockController : Controller } else { + // Block second punch today if not allowed + if (!company.TimeclockAllowMultiplePunchesPerDay) + { + var todayPunched = await _unitOfWork.EmployeeClockEntries.AnyAsync( + e => e.UserId == user.Id && e.CompanyId == companyId && e.ClockInTime >= now.Date); + if (todayPunched) + return BadRequest(new { message = "Multiple clock-ins per day are not allowed." }); + } + // Clock in var entry = new EmployeeClockEntry { @@ -468,6 +586,33 @@ public class TimeclockController : Controller // Helpers // ========================================================================= + /// + /// Validates the kiosk device cookie against the table. + /// Returns (null, null) if the cookie is absent or the token no longer exists. + /// Queries with ignoreQueryFilters because the kiosk runs without a logged-in user context. + /// + private async Task<(TimeclockKioskDevice? device, Company? company)> ValidateKioskCookieAsync() + { + var cookie = ReadTimeclockKioskCookie(); + if (cookie == null) return (null, null); + + var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync( + d => d.CompanyId == cookie.Value.companyId && d.Token == cookie.Value.token, + true); + if (device == null) return (null, null); + + var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true); + return (device, company); + } + + /// Updates without blocking the response. + private async Task TouchDeviceAsync(TimeclockKioskDevice device) + { + device.LastSeenAt = DateTime.UtcNow; + await _unitOfWork.TimeclockKioskDevices.UpdateAsync(device); + await _unitOfWork.CompleteAsync(); + } + private int GetCurrentCompanyId() { var claim = User.FindFirst("CompanyId")?.Value; diff --git a/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs b/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs index 00fbe1c..e06ce21 100644 --- a/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs +++ b/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs @@ -97,6 +97,7 @@ public class SubscriptionMiddleware context.Items["AllowAccounting"] = true; context.Items["AllowSms"] = true; context.Items["AllowCustomFormulas"] = true; + context.Items["TimeclockEnabled"] = true; await _next(context); return; } @@ -148,6 +149,7 @@ public class SubscriptionMiddleware || (!company.SmsDisabledByAdmin && (planConfig?.AllowSms ?? false) && company.SmsEnabled); context.Items["AllowCustomFormulas"] = company.IsComped || (planConfig?.AllowCustomFormulas ?? false); + context.Items["TimeclockEnabled"] = company.TimeclockEnabled; if (company.IsComped) { diff --git a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml index 5cf746d..03fd5ab 100644 --- a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml +++ b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml @@ -106,6 +106,11 @@ Kiosk + @if (ViewBag.AllowCustomFormulas == true) {