Fix time entry workers, powder usage logging, inventory edit, and mojibake
- JobTimeEntry: migrate to UserId/UserDisplayName; make ShopWorkerId nullable (migration MigrateTimeEntriesToUserId) - Log Time modal: populate worker dropdown from Identity users instead of ShopWorkers; fix ShopMobile view same issue - Inventory Ledger: scan-based JobUsage transactions now appear in Powder Usage By Job tab (synthesized from InventoryTransaction) - Inventory Ledger: add Edit button for JobUsage transactions; new GetUsageForEdit + EditUsageTransaction endpoints; inventory-ledger.js - InventoryTransactionRepository: include Job.Customer for ledger queries - InventoryAiLookupService: handle JSON-LD @graph wrapper (Columbia Coatings / WooCommerce+Yoast); add HTML price snippet fallback - Fix mojibake in 9 views: â†' → →, âœ" → ✓, âš → ⚠ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -404,10 +404,12 @@ public class JobsController : Controller
|
||||
// Workers dropdown for inline assignment
|
||||
await PopulateWorkersDropdown();
|
||||
|
||||
// Shop workers for time entry
|
||||
var shopWorkers = (await _unitOfWork.ShopWorkers.FindAsync(w => w.IsActive, false))
|
||||
.OrderBy(w => w.Name).ToList();
|
||||
ViewBag.ShopWorkers = shopWorkers.Select(w => new { w.Id, w.Name, Role = System.Text.RegularExpressions.Regex.Replace(w.Role.ToString(), "([a-z])([A-Z])", "$1 $2") }).ToList();
|
||||
// Company users for time entry worker dropdown
|
||||
var companyUsers = await _userManager.Users
|
||||
.Where(u => u.CompanyId == job.CompanyId && u.IsActive)
|
||||
.OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
|
||||
.ToListAsync();
|
||||
ViewBag.ShopWorkers = companyUsers.Select(u => new { Id = u.Id, Name = u.FullName }).ToList();
|
||||
|
||||
// Populate Edit Items wizard data (inline modal on Details page)
|
||||
var wizardCosts = await _pricingService.GetOperatingCostsAsync(job.CompanyId);
|
||||
@@ -2127,9 +2129,12 @@ public class JobsController : Controller
|
||||
.ThenBy(j => j.JobNumber)
|
||||
.ToList();
|
||||
|
||||
// Workers for filter dropdown
|
||||
var workers = await _unitOfWork.ShopWorkers.FindAsync(w => w.IsActive && !w.IsDeleted);
|
||||
ViewBag.Workers = workers.OrderBy(w => w.Name).ToList();
|
||||
// Company users for worker filter chips
|
||||
var mobileWorkers = await _userManager.Users
|
||||
.Where(u => u.CompanyId == companyId.Value && u.IsActive)
|
||||
.OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
|
||||
.ToListAsync();
|
||||
ViewBag.Workers = mobileWorkers.Select(u => new { Id = u.Id, Name = u.FullName }).ToList();
|
||||
ViewBag.CurrentWorkerId = workerId;
|
||||
ViewBag.AllStatuses = allStatuses;
|
||||
|
||||
@@ -3352,13 +3357,13 @@ public class JobsController : Controller
|
||||
{
|
||||
var entries = await _unitOfWork.JobTimeEntries.FindAsync(
|
||||
e => e.JobId == jobId, false,
|
||||
e => e.Worker);
|
||||
e => e.Worker); // Worker nav loaded for display of legacy entries that pre-date user migration
|
||||
var dtos = _mapper.Map<List<JobTimeEntryDto>>(entries.OrderByDescending(e => e.WorkDate).ToList());
|
||||
return Json(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a time entry for a shop worker on a specific job.
|
||||
/// Adds a time entry for a company user on a specific job.
|
||||
/// Validates hours are in the reasonable range (0.1–24) before saving.
|
||||
/// Returns the new entry as JSON so the UI can append it to the list without a reload.
|
||||
/// </summary>
|
||||
@@ -3371,13 +3376,14 @@ public class JobsController : Controller
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(dto.JobId);
|
||||
if (job == null) return NotFound();
|
||||
|
||||
var worker = await _unitOfWork.ShopWorkers.GetByIdAsync(dto.ShopWorkerId);
|
||||
if (worker == null) return BadRequest(new { error = "Worker not found." });
|
||||
var user = await _userManager.FindByIdAsync(dto.UserId);
|
||||
if (user == null) return BadRequest(new { error = "Worker not found." });
|
||||
|
||||
var entry = new JobTimeEntry
|
||||
{
|
||||
JobId = dto.JobId,
|
||||
ShopWorkerId = dto.ShopWorkerId,
|
||||
UserId = dto.UserId,
|
||||
UserDisplayName = user.FullName,
|
||||
WorkDate = dto.WorkDate.Date,
|
||||
HoursWorked = Math.Round(dto.HoursWorked, 2),
|
||||
Stage = string.IsNullOrWhiteSpace(dto.Stage) ? null : dto.Stage.Trim(),
|
||||
@@ -3388,9 +3394,7 @@ public class JobsController : Controller
|
||||
await _unitOfWork.JobTimeEntries.AddAsync(entry);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Reload with worker navigation for the response
|
||||
var saved = await _unitOfWork.JobTimeEntries.GetByIdAsync(entry.Id, false, e => e.Worker);
|
||||
return Json(_mapper.Map<JobTimeEntryDto>(saved));
|
||||
return Json(_mapper.Map<JobTimeEntryDto>(entry));
|
||||
}
|
||||
|
||||
/// <summary>Updates an existing time entry's hours, stage, and notes in place.</summary>
|
||||
@@ -3400,13 +3404,14 @@ public class JobsController : Controller
|
||||
if (dto.HoursWorked <= 0 || dto.HoursWorked > 24)
|
||||
return BadRequest(new { error = "Hours must be between 0.1 and 24." });
|
||||
|
||||
var entry = await _unitOfWork.JobTimeEntries.GetByIdAsync(dto.Id, false, e => e.Worker);
|
||||
var entry = await _unitOfWork.JobTimeEntries.GetByIdAsync(dto.Id);
|
||||
if (entry == null) return NotFound();
|
||||
|
||||
var worker = await _unitOfWork.ShopWorkers.GetByIdAsync(dto.ShopWorkerId);
|
||||
if (worker == null) return BadRequest(new { error = "Worker not found." });
|
||||
var user = await _userManager.FindByIdAsync(dto.UserId);
|
||||
if (user == null) return BadRequest(new { error = "Worker not found." });
|
||||
|
||||
entry.ShopWorkerId = dto.ShopWorkerId;
|
||||
entry.UserId = dto.UserId;
|
||||
entry.UserDisplayName = user.FullName;
|
||||
entry.WorkDate = dto.WorkDate.Date;
|
||||
entry.HoursWorked = Math.Round(dto.HoursWorked, 2);
|
||||
entry.Stage = string.IsNullOrWhiteSpace(dto.Stage) ? null : dto.Stage.Trim();
|
||||
@@ -3416,9 +3421,7 @@ public class JobsController : Controller
|
||||
await _unitOfWork.JobTimeEntries.UpdateAsync(entry);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Reload for response
|
||||
var saved = await _unitOfWork.JobTimeEntries.GetByIdAsync(entry.Id, false, e => e.Worker);
|
||||
return Json(_mapper.Map<JobTimeEntryDto>(saved));
|
||||
return Json(_mapper.Map<JobTimeEntryDto>(entry));
|
||||
}
|
||||
|
||||
/// <summary>Soft-deletes a time entry. The hours are removed from the job's running total.</summary>
|
||||
|
||||
Reference in New Issue
Block a user