mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
WorkOrderDataService.AddAsync overwrote the UTC CreatedDate set by WorkOrderService with local server time, offsetting the SLA response clock on any host not running in UTC. Data services now stamp CreatedDate, LastModificationTime and DeletionTime with DateTime.UtcNow, and a work order keeps the creation time its caller set.
542 lines
22 KiB
C#
542 lines
22 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Helpers;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.DataServices.Models;
|
|
|
|
namespace SeaHaven.DataServices.Implementation
|
|
{
|
|
public class WorkOrderDataService : IWorkOrderDataService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public WorkOrderDataService(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<WorkOrder?> GetByIdAsync(int id)
|
|
{
|
|
return await _context.workOrders.FindAsync(id);
|
|
}
|
|
|
|
public async Task<WorkOrder?> GetByIdWithDetailsAsync(int id)
|
|
{
|
|
return await _context.workOrders
|
|
.Include(w => w.Locations)
|
|
.Include(w => w.AssignToUser)
|
|
.FirstOrDefaultAsync(w => w.Id == id);
|
|
}
|
|
|
|
public async Task<WorkOrder?> GetByInternalNumberAsync(string internalNumber)
|
|
{
|
|
return await _context.workOrders
|
|
.FirstOrDefaultAsync(w => w.InternalWONumber == internalNumber);
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetAllAsync()
|
|
{
|
|
return await _context.workOrders.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetByLocationIdAsync(int locationId)
|
|
{
|
|
return await _context.workOrders
|
|
.Where(w => w.LocationId == locationId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetByAssigneeAsync(string assignee)
|
|
{
|
|
return await _context.workOrders
|
|
.Where(w => w.AssignTo == assignee)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetByStatusAsync(string status)
|
|
{
|
|
return await _context.workOrders
|
|
.Where(w => w.Status == status)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetByPriorityAsync(string priority)
|
|
{
|
|
return await _context.workOrders
|
|
.Where(w => w.Priority == priority)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<(IEnumerable<WorkOrder> Items, int TotalCount)> GetPagedAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search = null,
|
|
string? status = null,
|
|
string? priority = null)
|
|
{
|
|
var query = _context.workOrders.AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
query = query.Where(w =>
|
|
w.WorkerOrderNumber!.Contains(search) ||
|
|
w.WorkerOrderTitle!.Contains(search));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(status))
|
|
{
|
|
query = query.Where(w => w.Status == status);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(priority))
|
|
{
|
|
query = query.Where(w => w.Priority == priority);
|
|
}
|
|
|
|
var totalCount = await query.CountAsync();
|
|
var items = await query
|
|
.OrderByDescending(w => w.CreatedDate)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
return (items, totalCount);
|
|
}
|
|
|
|
public async Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search = null,
|
|
string? status = null,
|
|
string? assignee = null,
|
|
int? locationId = null,
|
|
string? sortBy = "lastUpdated",
|
|
string? sortDir = "desc",
|
|
int? accountId = null)
|
|
{
|
|
search ??= "";
|
|
|
|
var query = _context.workOrders
|
|
.Include(w => w.Locations)
|
|
.Include(w => w.AssignToUser)
|
|
.Where(w => w.istemplate != true);
|
|
|
|
if (accountId.HasValue)
|
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
|
|
|
if (locationId.HasValue)
|
|
query = query.Where(w => w.LocationId == locationId.Value);
|
|
|
|
if (!string.IsNullOrWhiteSpace(status))
|
|
query = query.Where(w => w.Status == status);
|
|
|
|
if (assignee == "__unassigned")
|
|
query = query.Where(w => w.AssignTo == null || w.AssignTo == "");
|
|
else if (!string.IsNullOrWhiteSpace(assignee))
|
|
query = query.Where(w => w.AssignToUser != null && (w.AssignToUser.FirstName + " " + w.AssignToUser.LastName).Contains(assignee));
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
query = query.Where(w =>
|
|
(w.InternalWONumber != null && w.InternalWONumber.Contains(search)) ||
|
|
(w.WorkerOrderNumber != null && w.WorkerOrderNumber.Contains(search)) ||
|
|
(w.WorkerOrderTitle != null && w.WorkerOrderTitle.Contains(search)) ||
|
|
(w.Locations != null && w.Locations.Title != null && w.Locations.Title.Contains(search)) ||
|
|
(w.Locations != null && w.Locations.Name != null && w.Locations.Name.Contains(search))
|
|
);
|
|
}
|
|
|
|
var totalCount = await query.CountAsync();
|
|
|
|
var isAsc = sortDir?.ToLower() == "asc";
|
|
IOrderedQueryable<WorkOrder> ordered = (sortBy?.ToLower()) switch
|
|
{
|
|
"number" => isAsc ? query.OrderBy(w => w.InternalWONumber) : query.OrderByDescending(w => w.InternalWONumber),
|
|
"title" => isAsc ? query.OrderBy(w => w.WorkerOrderTitle) : query.OrderByDescending(w => w.WorkerOrderTitle),
|
|
"location" => isAsc ? query.OrderBy(w => w.Locations != null ? w.Locations.Name : "") : query.OrderByDescending(w => w.Locations != null ? w.Locations.Name : ""),
|
|
"priority" => isAsc ? query.OrderBy(w => w.Priority) : query.OrderByDescending(w => w.Priority),
|
|
"status" => isAsc ? query.OrderBy(w => w.Status) : query.OrderByDescending(w => w.Status),
|
|
"createddate" => isAsc ? query.OrderBy(w => w.CreatedDate) : query.OrderByDescending(w => w.CreatedDate),
|
|
"assignedto" => isAsc ? query.OrderBy(w => w.AssignToUser != null ? w.AssignToUser.FirstName : "") : query.OrderByDescending(w => w.AssignToUser != null ? w.AssignToUser.FirstName : ""),
|
|
"lastupdated" => isAsc ? query.OrderBy(w => w.LastModificationTime) : query.OrderByDescending(w => w.LastModificationTime),
|
|
_ => query.OrderByDescending(w => w.Id)
|
|
};
|
|
|
|
var data = await ordered
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.Select(w => new WorkOrderListItemReadModel
|
|
{
|
|
Id = w.Id,
|
|
InternalWONumber = w.InternalWONumber,
|
|
WorkOrderNumber = w.WorkerOrderNumber,
|
|
Number = w.InternalWONumber ?? w.WorkerOrderNumber,
|
|
Title = w.WorkerOrderTitle,
|
|
Location = w.Locations != null ? w.Locations.Name : "",
|
|
LocationName = w.Locations != null ? w.Locations.Name : "",
|
|
Priority = w.Priority,
|
|
Status = w.Status,
|
|
CreatedDate = w.CreatedDate,
|
|
DueDate = w.DueDate,
|
|
AssignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
|
EmployeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
|
|
LastUpdated = new[] {
|
|
w.LastModificationTime,
|
|
w.Comments!.Max(c => (DateTime?)c.CreatedDate),
|
|
_context.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt)
|
|
}.Where(d => d.HasValue).Max()
|
|
})
|
|
.ToListAsync();
|
|
|
|
return (data, totalCount);
|
|
}
|
|
|
|
public async Task<WorkOrder> AddAsync(WorkOrder workOrder)
|
|
{
|
|
workOrder.CreatedDate ??= DateTime.UtcNow;
|
|
await _context.workOrders.AddAsync(workOrder);
|
|
await _context.SaveChangesAsync();
|
|
return workOrder;
|
|
}
|
|
|
|
public async Task UpdateAsync(WorkOrder workOrder)
|
|
{
|
|
workOrder.LastModificationTime = DateTime.UtcNow;
|
|
_context.workOrders.Update(workOrder);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task DeleteAsync(int id)
|
|
{
|
|
var entity = await GetByIdAsync(id);
|
|
if (entity != null)
|
|
{
|
|
_context.workOrders.Remove(entity);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrderContacts>> GetWorkOrderContactsAsync(int workOrderId)
|
|
{
|
|
return await _context.WorkOrderContacts
|
|
.Where(wc => wc.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrderCategories>> GetWorkOrderCategoriesAsync(int workOrderId)
|
|
{
|
|
return await _context.workOrderCategories
|
|
.Where(wc => wc.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrderAttachments>> GetWorkOrderAttachmentsAsync(int workOrderId)
|
|
{
|
|
return await _context.workOrderAttachments
|
|
.Where(wa => wa.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<Comments>> GetWorkOrderCommentsAsync(int workOrderId)
|
|
{
|
|
return await _context.Comments
|
|
.Where(c => c.WorkerOrderId == workOrderId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId)
|
|
{
|
|
return await _context.WorkOrderAuditLogs
|
|
.Where(a => a.WorkOrderId == workOrderId)
|
|
.OrderByDescending(a => a.CreatedAt)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds)
|
|
{
|
|
var contacts = contactIds.Select(cid => new WorkOrderContacts
|
|
{
|
|
WorkorderId = workOrderId,
|
|
ContactId = cid
|
|
});
|
|
|
|
await _context.WorkOrderContacts.AddRangeAsync(contacts);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds)
|
|
{
|
|
var categories = categoryIds.Select(catId => new WorkOrderCategories
|
|
{
|
|
WorkorderId = workOrderId,
|
|
CategoryId = catId
|
|
});
|
|
|
|
await _context.workOrderCategories.AddRangeAsync(categories);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment)
|
|
{
|
|
await _context.workOrderAttachments.AddAsync(attachment);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog)
|
|
{
|
|
await _context.WorkOrderAuditLogs.AddAsync(auditLog);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task AddAuditLogsAsync(IReadOnlyCollection<WorkOrderAuditLog> auditLogs)
|
|
{
|
|
if (auditLogs.Count == 0)
|
|
return;
|
|
|
|
await _context.WorkOrderAuditLogs.AddRangeAsync(auditLogs);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task DeleteWorkOrderContactsAsync(int workOrderId)
|
|
{
|
|
var contacts = await _context.WorkOrderContacts
|
|
.Where(wc => wc.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
|
|
if (contacts.Any())
|
|
{
|
|
_context.WorkOrderContacts.RemoveRange(contacts);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public async Task DeleteWorkOrderCategoriesAsync(int workOrderId)
|
|
{
|
|
var categories = await _context.workOrderCategories
|
|
.Where(wc => wc.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
|
|
if (categories.Any())
|
|
{
|
|
_context.workOrderCategories.RemoveRange(categories);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public async Task DeleteWorkOrderAttachmentsAsync(int workOrderId)
|
|
{
|
|
var attachments = await _context.workOrderAttachments
|
|
.Where(wa => wa.WorkorderId == workOrderId)
|
|
.ToListAsync();
|
|
|
|
if (attachments.Any())
|
|
{
|
|
_context.workOrderAttachments.RemoveRange(attachments);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public async Task<bool> ExistsAsync(int id)
|
|
{
|
|
return await _context.workOrders.AnyAsync(w => w.Id == id);
|
|
}
|
|
|
|
public async Task<bool> InternalNumberExistsAsync(string internalNumber, int? excludeId = null)
|
|
{
|
|
return await _context.workOrders.AnyAsync(w =>
|
|
w.InternalWONumber == internalNumber &&
|
|
(excludeId == null || w.Id != excludeId));
|
|
}
|
|
|
|
public async Task<string> GenerateNextInternalWONumberAsync()
|
|
{
|
|
return await Task.FromResult($"WO-{DateTime.Now:yyyyMMdd}-{new Random().Next(1000, 9999)}");
|
|
}
|
|
|
|
public async Task<int> CountAsync()
|
|
{
|
|
return await _context.workOrders.CountAsync();
|
|
}
|
|
|
|
public async Task<int> CountByStatusAsync(string status)
|
|
{
|
|
return await _context.workOrders.CountAsync(w => w.Status == status);
|
|
}
|
|
|
|
public async Task<int> CountByPriorityAsync(string priority)
|
|
{
|
|
return await _context.workOrders.CountAsync(w => w.Priority == priority);
|
|
}
|
|
|
|
public async Task<int> CountByLocationAsync(int locationId)
|
|
{
|
|
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
|
|
}
|
|
|
|
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync(int? accountId = null)
|
|
{
|
|
IQueryable<WorkOrder> query = _context.workOrders
|
|
.Where(w => w.istemplate != true);
|
|
|
|
if (accountId.HasValue)
|
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
|
|
|
return await query
|
|
.Include(w => w.Locations)
|
|
.Include(w => w.AssignToUser)
|
|
.AsSplitQuery()
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<WorkOrder>> GetByRequestedIdsWithDetailsAsync(
|
|
IReadOnlyCollection<int> ids,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var orderedIds = ids.Distinct().ToList();
|
|
if (orderedIds.Count == 0)
|
|
return Array.Empty<WorkOrder>();
|
|
|
|
var loaded = await _context.workOrders
|
|
.AsNoTracking()
|
|
.Where(w => orderedIds.Contains(w.Id) && w.istemplate != true)
|
|
.Include(w => w.Locations)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var byId = loaded.ToDictionary(w => w.Id);
|
|
var ordered = new List<WorkOrder>(orderedIds.Count);
|
|
foreach (var id in orderedIds)
|
|
{
|
|
if (byId.TryGetValue(id, out var workOrder))
|
|
ordered.Add(workOrder);
|
|
}
|
|
|
|
return ordered;
|
|
}
|
|
|
|
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null)
|
|
{
|
|
var query = _context.workOrders
|
|
.AsNoTracking()
|
|
.Where(w => w.Id == id);
|
|
|
|
if (accountId.HasValue)
|
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
|
|
|
return await query
|
|
.Select(s => new WorkOrderDetailReadModel
|
|
{
|
|
Id = s.Id,
|
|
InternalWONumber = s.InternalWONumber,
|
|
WorkerOrderNumber = s.WorkerOrderNumber,
|
|
Title = s.WorkerOrderTitle,
|
|
Description = s.Description,
|
|
CreatedDate = s.CreatedDate,
|
|
DueDate = s.DueDate,
|
|
ScheduledDate = s.ScheduledDate,
|
|
CompletedDate = s.CompletedDate,
|
|
Status = s.Status,
|
|
Assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
|
|
Location = s.Locations != null ? s.Locations.Name : "",
|
|
LocationId = s.LocationId,
|
|
LocationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
|
|
Priority = s.Priority,
|
|
Problem = s.Problem,
|
|
Trade = s.Trade,
|
|
SubTrade = s.SubTrade,
|
|
VendorNTE = s.VendorNTE,
|
|
Source = s.Source,
|
|
PO = s.PO,
|
|
TT = s.TT,
|
|
Poc = s.WorkOrderContacts!.Select(c => new ContactReadModel
|
|
{
|
|
Id = c.Id,
|
|
Name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "",
|
|
Email = c.POC != null ? c.POC.Email : "",
|
|
Phone = c.POC != null ? c.POC.PhoneNumber : ""
|
|
}).ToList(),
|
|
Category = s.WorkOrderCategories!.Select(c => new CategoryReadModel
|
|
{
|
|
Id = c.Id,
|
|
Name = c.Category != null ? c.Category.Name : ""
|
|
}).ToList(),
|
|
Attachments = s.workOrderAttachments!.Select(a => new AttachmentReadModel
|
|
{
|
|
Id = a.Id,
|
|
Attachment = a.Attachments
|
|
}).ToList(),
|
|
BeforPhotoIssue = s.BeforPhotoIssue,
|
|
BeforPhotoAttachment = s.BeforPhotoAttachment,
|
|
AfterPhotoIssue = s.AfterPhotoIssue,
|
|
AfterPhotoAttachment = s.AfterPhotoAttachment,
|
|
SignOffName = s.SignOffName,
|
|
SignOffAttachment = s.SignOffAttachment,
|
|
SignOffSignature = s.SignOffSignature,
|
|
Comments = s.Comments!.Select(c => new CommentReadModel
|
|
{
|
|
Id = c.Id,
|
|
CreatedDate = c.CreatedDate,
|
|
Commenttext = c.Commenttext,
|
|
Commenter = c.Commenter,
|
|
RecordType = c.RecordType,
|
|
CommentType = c.CommentType,
|
|
Documents = c.Documents,
|
|
UserName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : ""
|
|
}).ToList(),
|
|
Quotes = s.Quotes!.Select(q => new QuoteRefReadModel
|
|
{
|
|
Code = q.QuotId,
|
|
Id = q.Id
|
|
}).ToList(),
|
|
AuditLog = _context.WorkOrderAuditLogs
|
|
.Where(a => a.WorkOrderId == s.Id)
|
|
.OrderByDescending(a => a.CreatedAt)
|
|
.Select(a => new AuditLogReadModel
|
|
{
|
|
Id = a.Id,
|
|
FieldName = a.FieldName,
|
|
OldValue = a.OldValue,
|
|
NewValue = a.NewValue,
|
|
Action = a.Action,
|
|
CreatedAt = a.CreatedAt,
|
|
UserName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
|
|
}).ToList(),
|
|
Dispatches = s.Dispatches!.Select(d => new DispatchReadModel
|
|
{
|
|
Id = d.Id,
|
|
DispatchNumber = d.DispatchNumber,
|
|
PONumber = d.PONumber,
|
|
NTEAmount = d.NTEAmount,
|
|
Description = d.Description,
|
|
Status = d.Status,
|
|
ScheduledDate = d.ScheduledDate,
|
|
CompletedDate = d.CompletedDate,
|
|
DispatchedAt = d.DispatchedAt,
|
|
EmailSent = d.EmailSent,
|
|
VendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
|
|
VendorEmail = d.Vendor != null ? d.Vendor.Email : "",
|
|
VendorId = d.VendorId
|
|
}).ToList()
|
|
}).FirstOrDefaultAsync();
|
|
}
|
|
|
|
public async Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync(int? accountId = null)
|
|
{
|
|
IQueryable<WorkOrder> query = _context.workOrders
|
|
.Where(w => w.istemplate != true);
|
|
|
|
if (accountId.HasValue)
|
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
|
|
|
return await query
|
|
.Include(w => w.Locations)
|
|
.ToListAsync();
|
|
}
|
|
|
|
private static string? ExtractZip(string? address)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(address)) return null;
|
|
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
|
|
return match.Success ? match.Groups[1].Value : null;
|
|
}
|
|
}
|
|
}
|