Require auth on all work order QR codes and add top view QR
- StatusBump (GET + POST) now requires authentication; routes by job ID instead of anonymous ShopAccessCode GUID; records actual user name in status history instead of anonymous token string - WorkOrder action generates a second "View Job" QR in the header linking to the authenticated Details page (for verifying specs and seeing catalog images on mobile); status bump QR updated to ID-based URL - WorkOrder view: top QR added to header alongside job number; status bump label updated (removed "no login required" copy) - StatusBump view: updated form routing from asp-route-token to asp-route-id - HelpKnowledgeBase and Jobs help article updated with two-tier QR docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -573,53 +573,40 @@ public class JobsController : Controller
|
||||
|
||||
/// <summary>
|
||||
/// Shows the status-bump selection page for a shop-floor QR code scan.
|
||||
/// This endpoint is AllowAnonymous — it is accessed by workers scanning a printed QR code
|
||||
/// on a work order, not by logged-in users. The <paramref name="token"/> is the job's
|
||||
/// ShopAccessCode GUID. IgnoreQueryFilters is used so the scan works even if the worker's
|
||||
/// device has no active session (common on shared shop tablets).
|
||||
/// Requires authentication — workers must be logged in before scanning. Tenant isolation
|
||||
/// is enforced by the normal global query filter on <c>GetByIdAsync</c>.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> StatusBump(Guid token)
|
||||
public async Task<IActionResult> StatusBump(int id)
|
||||
{
|
||||
// Find job by ShopAccessCode — ignore tenant/soft-delete filters so the
|
||||
// anonymous scan always finds the job regardless of active user session.
|
||||
var jobs = await _unitOfWork.Jobs.FindAsync(
|
||||
j => j.ShopAccessCode == token, true,
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(id, false,
|
||||
j => j.JobStatus,
|
||||
j => j.JobPriority,
|
||||
j => j.Customer);
|
||||
|
||||
var job = jobs.FirstOrDefault();
|
||||
if (job == null) return NotFound("Work order token not found.");
|
||||
if (job == null) return NotFound();
|
||||
|
||||
// Load all status lookups to determine next step
|
||||
var allStatuses = (await _unitOfWork.JobStatusLookups.GetAllAsync())
|
||||
.OrderBy(s => s.DisplayOrder).ToList();
|
||||
|
||||
ViewBag.AllStatuses = allStatuses;
|
||||
ViewBag.Job = job;
|
||||
ViewBag.Token = token;
|
||||
ViewBag.JobId = id;
|
||||
return View();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a QR-code status bump from the shop floor — also AllowAnonymous.
|
||||
/// Validates the token, applies the new status, records the change history, and broadcasts
|
||||
/// a SignalR update so the office dashboard refreshes in real time.
|
||||
/// The "bumped by" user is recorded as the ShopAccessCode token string (anonymous actor).
|
||||
/// Processes a QR-code status bump from the shop floor. Requires authentication.
|
||||
/// Records the authenticated user's name in status history.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StatusBump(Guid token, int newStatusId)
|
||||
public async Task<IActionResult> StatusBump(int id, int newStatusId)
|
||||
{
|
||||
var jobs = await _unitOfWork.Jobs.FindAsync(
|
||||
j => j.ShopAccessCode == token, true,
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(id, false,
|
||||
j => j.JobStatus,
|
||||
j => j.Customer);
|
||||
|
||||
var job = jobs.FirstOrDefault();
|
||||
if (job == null) return NotFound("Work order token not found.");
|
||||
if (job == null) return NotFound();
|
||||
|
||||
var allStatuses = (await _unitOfWork.JobStatusLookups.GetAllAsync()).ToList();
|
||||
var newStatus = allStatuses.FirstOrDefault(s => s.Id == newStatusId);
|
||||
@@ -630,28 +617,29 @@ public class JobsController : Controller
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
if (newStatus.StatusCode == "COMPLETED") job.CompletedDate = DateTime.UtcNow;
|
||||
|
||||
var userName = User.Identity?.Name ?? "Shop Floor";
|
||||
await _unitOfWork.JobStatusHistory.AddAsync(new JobStatusHistory
|
||||
{
|
||||
JobId = job.Id,
|
||||
FromStatusId = oldStatusId,
|
||||
ToStatusId = newStatusId,
|
||||
ChangedDate = DateTime.UtcNow,
|
||||
Notes = "Updated via shop floor QR scan",
|
||||
Notes = $"Updated via shop floor QR scan by {userName}",
|
||||
CompanyId = job.CompanyId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Reload job status for redirect display
|
||||
return RedirectToAction(nameof(StatusBump), new { token });
|
||||
return RedirectToAction(nameof(StatusBump), new { id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the printable work order view for a job.
|
||||
/// Loads all job items with their coats and prep services so the printed sheet contains
|
||||
/// the full powder specification, colors, and preparation instructions for shop workers.
|
||||
/// The work order also includes the QR code for ShopAccessCode-based status bumps.
|
||||
/// Generates two sets of QR codes: a top "view" code linking to the authenticated job
|
||||
/// details page, and bottom action codes for status bumping and powder usage logging.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> WorkOrder(int? id)
|
||||
{
|
||||
@@ -713,13 +701,19 @@ public class JobsController : Controller
|
||||
ViewBag.Company = company;
|
||||
}
|
||||
|
||||
// Generate QR code for shop floor status bumping
|
||||
var statusBumpUrl = Url.Action("StatusBump", "Jobs", new { token = job.ShopAccessCode }, Request.Scheme)!;
|
||||
using var qrGenerator = new QRCoder.QRCodeGenerator();
|
||||
|
||||
// Top QR: view/verify the job on mobile (authenticated job details page)
|
||||
var detailsUrl = Url.Action("Details", "Jobs", new { id = job.Id }, Request.Scheme)!;
|
||||
using var viewQrData = qrGenerator.CreateQrCode(detailsUrl, QRCoder.QRCodeGenerator.ECCLevel.M);
|
||||
using var viewQrCode = new QRCoder.PngByteQRCode(viewQrData);
|
||||
ViewBag.ViewQrCodeBase64 = Convert.ToBase64String(viewQrCode.GetGraphic(4));
|
||||
|
||||
// Bottom QR: status bump (authenticated, job ID routed)
|
||||
var statusBumpUrl = Url.Action("StatusBump", "Jobs", new { id = job.Id }, Request.Scheme)!;
|
||||
using var qrData = qrGenerator.CreateQrCode(statusBumpUrl, QRCoder.QRCodeGenerator.ECCLevel.M);
|
||||
using var qrCode = new QRCoder.PngByteQRCode(qrData);
|
||||
var qrBytes = qrCode.GetGraphic(4);
|
||||
ViewBag.QrCodeBase64 = Convert.ToBase64String(qrBytes);
|
||||
ViewBag.QrCodeBase64 = Convert.ToBase64String(qrCode.GetGraphic(4));
|
||||
ViewBag.StatusBumpUrl = statusBumpUrl;
|
||||
|
||||
// Generate QR codes for each unique inventory powder item so workers can
|
||||
|
||||
Reference in New Issue
Block a user