fix: C#5 compat - replace all ?. with explicit null checks

This commit is contained in:
2026-05-05 16:09:28 +10:00
parent 9972909c4e
commit 28505b3c98
+46 -128
View File
@@ -7,10 +7,6 @@ using System.Linq;
namespace Disco.Plugins.ServiceTracker.Services namespace Disco.Plugins.ServiceTracker.Services
{ {
/// <summary>
/// Core service that combines Disco Job data with ServiceTracker metadata
/// to produce the dashboard view model.
/// </summary>
public class ServiceTrackerService public class ServiceTrackerService
{ {
private readonly DiscoDataContext _database; private readonly DiscoDataContext _database;
@@ -27,22 +23,16 @@ namespace Disco.Plugins.ServiceTracker.Services
public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null, public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null,
string filterStatus = null, string filterTech = null, string sortBy = "due") string filterStatus = null, string filterTech = null, string sortBy = "due")
{ {
// Get all open jobs from Disco
var openJobs = _database.Jobs var openJobs = _database.Jobs
.Include("Device") .Include("Device").Include("Device.DeviceModel")
.Include("Device.DeviceModel") .Include("User").Include("OpenedTechUser")
.Include("User") .Include("JobType").Include("JobSubTypes")
.Include("OpenedTechUser")
.Include("JobType")
.Include("JobSubTypes")
.Where(j => j.ClosedDate == null) .Where(j => j.ClosedDate == null)
.ToList(); .ToList();
// Load all service tracker tickets
var allTickets = _dataStore.LoadAllTickets(); var allTickets = _dataStore.LoadAllTickets();
var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t); var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t);
// Auto-create tickets for jobs that don't have one (if enabled)
if (_config.AutoCreateTicketsForNewJobs) if (_config.AutoCreateTicketsForNewJobs)
{ {
foreach (var job in openJobs) foreach (var job in openJobs)
@@ -56,7 +46,6 @@ namespace Disco.Plugins.ServiceTracker.Services
} }
} }
// Build tiles
var tiles = new List<DashboardTile>(); var tiles = new List<DashboardTile>();
foreach (var job in openJobs) foreach (var job in openJobs)
{ {
@@ -65,7 +54,6 @@ namespace Disco.Plugins.ServiceTracker.Services
tiles.Add(BuildTile(job, ticket)); tiles.Add(BuildTile(job, ticket));
} }
// Apply filters
if (!string.IsNullOrEmpty(filterPriority)) if (!string.IsNullOrEmpty(filterPriority))
tiles = tiles.Where(t => t.PriorityId == filterPriority).ToList(); tiles = tiles.Where(t => t.PriorityId == filterPriority).ToList();
if (!string.IsNullOrEmpty(filterLocation)) if (!string.IsNullOrEmpty(filterLocation))
@@ -75,7 +63,6 @@ namespace Disco.Plugins.ServiceTracker.Services
if (!string.IsNullOrEmpty(filterTech)) if (!string.IsNullOrEmpty(filterTech))
tiles = tiles.Where(t => t.AssignedTechId == filterTech).ToList(); tiles = tiles.Where(t => t.AssignedTechId == filterTech).ToList();
// Sort
switch (sortBy) switch (sortBy)
{ {
case "priority": case "priority":
@@ -89,7 +76,7 @@ namespace Disco.Plugins.ServiceTracker.Services
break; break;
case "sla": case "sla":
tiles = tiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2) tiles = tiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2)
.ThenBy(t => t.SlaDeadline ?? DateTime.MaxValue).ToList(); .ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList();
break; break;
case "due": case "due":
default: default:
@@ -97,13 +84,10 @@ namespace Disco.Plugins.ServiceTracker.Services
break; break;
} }
// Build stats
var stats = BuildStats(tiles);
return new DashboardViewModel return new DashboardViewModel
{ {
Tiles = tiles, Tiles = tiles,
Stats = stats, Stats = BuildStats(tiles),
Config = _config, Config = _config,
CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "",
SortBy = sortBy SortBy = sortBy
@@ -113,29 +97,18 @@ namespace Disco.Plugins.ServiceTracker.Services
private DashboardTile BuildTile(Job job, ServiceTicket ticket) private DashboardTile BuildTile(Job job, ServiceTicket ticket)
{ {
var now = DateTime.Now; var now = DateTime.Now;
var priorityId = (ticket != null ? ticket.PriorityId : null) ?? _config.DefaultPriorityId;
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId) ?? _config.Priorities.FirstOrDefault();
var locationId = (ticket != null ? ticket.LocationId : null) ?? _config.DefaultLocationId;
var location = _config.Locations.FirstOrDefault(l => l.Id == locationId) ?? _config.Locations.FirstOrDefault();
// Resolve priority var slaDeadline = ticket != null ? ticket.SlaDeadline : (DateTime?)null;
var priorityId = ticket?.PriorityId ?? _config.DefaultPriorityId;
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId)
?? _config.Priorities.FirstOrDefault();
// Resolve location
var locationId = ticket?.LocationId ?? _config.DefaultLocationId;
var location = _config.Locations.FirstOrDefault(l => l.Id == locationId)
?? _config.Locations.FirstOrDefault();
// Compute SLA
var slaDeadline = ticket?.SlaDeadline;
if (!slaDeadline.HasValue && priority != null && priority.SlaHours > 0) if (!slaDeadline.HasValue && priority != null && priority.SlaHours > 0)
{
slaDeadline = job.OpenedDate.AddHours(priority.SlaHours); slaDeadline = job.OpenedDate.AddHours(priority.SlaHours);
}
bool slaBreached = slaDeadline.HasValue && now > slaDeadline.Value; bool slaBreached = slaDeadline.HasValue && now > slaDeadline.Value;
bool slaWarning = !slaBreached && slaDeadline.HasValue && bool slaWarning = !slaBreached && slaDeadline.HasValue && priority != null && priority.SlaHours > 0
priority != null && priority.SlaHours > 0 && && now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25);
now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25);
// Compute age
int ageDays = (int)(now - job.OpenedDate).TotalDays; int ageDays = (int)(now - job.OpenedDate).TotalDays;
string ageBadge; string ageBadge;
if (ageDays == 0) ageBadge = "Today"; if (ageDays == 0) ageBadge = "Today";
@@ -144,8 +117,7 @@ namespace Disco.Plugins.ServiceTracker.Services
else if (ageDays < 30) ageBadge = (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : ""); else if (ageDays < 30) ageBadge = (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : "");
else ageBadge = (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : ""); else ageBadge = (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : "");
// ETA display var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate;
var eta = ticket?.EstimatedCompletion ?? job.ExpectedClosedDate;
string etaDisplay = "—"; string etaDisplay = "—";
if (eta.HasValue) if (eta.HasValue)
{ {
@@ -156,20 +128,18 @@ namespace Disco.Plugins.ServiceTracker.Services
else etaDisplay = eta.Value.ToString("dd MMM"); else etaDisplay = eta.Value.ToString("dd MMM");
} }
// Sort date: use SLA deadline if breached, then ETA, then expected close, then opened
DateTime sortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value DateTime sortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value
: eta ?? job.ExpectedClosedDate ?? job.OpenedDate; : eta.HasValue ? eta.Value
: job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate;
// Determine Disco status string
string discoStatus = "Open"; string discoStatus = "Open";
if (job.WaitingForUserAction.HasValue) discoStatus = "Awaiting User Action"; if (job.WaitingForUserAction.HasValue) discoStatus = "Awaiting User Action";
else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) discoStatus = "Device Held"; else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) discoStatus = "Device Held";
else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) discoStatus = "Ready for Return"; else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) discoStatus = "Ready for Return";
// Latest note
string latestNote = null; string latestNote = null;
int noteCount = 0; int noteCount = 0;
if (ticket?.Notes != null && ticket.Notes.Count > 0) if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0)
{ {
noteCount = ticket.Notes.Count; noteCount = ticket.Notes.Count;
var latest = ticket.Notes.OrderByDescending(n => n.Timestamp).First(); var latest = ticket.Notes.OrderByDescending(n => n.Timestamp).First();
@@ -183,8 +153,8 @@ namespace Disco.Plugins.ServiceTracker.Services
JobId = job.Id, JobId = job.Id,
JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId, JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId,
DeviceSerialNumber = job.DeviceSerialNumber ?? "—", DeviceSerialNumber = job.DeviceSerialNumber ?? "—",
DeviceModelDescription = job.Device?.DeviceModel != null ? job.Device.DeviceModel.Description : null, DeviceModelDescription = job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null,
DeviceComputerName = job.Device?.DeviceDomainId, DeviceComputerName = job.Device != null ? job.Device.DeviceDomainId : null,
UserId = job.UserId, UserId = job.UserId,
UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId, UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId,
OpenedByTechId = job.OpenedTechUserId, OpenedByTechId = job.OpenedTechUserId,
@@ -193,22 +163,22 @@ namespace Disco.Plugins.ServiceTracker.Services
ExpectedClosedDate = job.ExpectedClosedDate, ExpectedClosedDate = job.ExpectedClosedDate,
DiscoStatus = discoStatus, DiscoStatus = discoStatus,
PriorityId = priorityId, PriorityId = priorityId,
PriorityName = priority?.Name ?? "Unknown", PriorityName = priority != null ? priority.Name : "Unknown",
PriorityColor = priority?.Color ?? "#999", PriorityColor = priority != null ? priority.Color : "#999",
PrioritySortOrder = priority?.SortOrder ?? 99, PrioritySortOrder = priority != null ? priority.SortOrder : 99,
LocationId = locationId, LocationId = locationId,
LocationName = location?.Name ?? "Unknown", LocationName = location != null ? location.Name : "Unknown",
LocationIcon = location?.Icon ?? "", LocationIcon = location != null ? location.Icon : "",
LocationColor = location?.Color ?? "#999", LocationColor = location != null ? location.Color : "#999",
AssignedTechId = ticket?.AssignedTechId, AssignedTechId = ticket != null ? ticket.AssignedTechId : null,
AssignedTechName = ResolveUserName(ticket?.AssignedTechId), AssignedTechName = ResolveUserName(ticket != null ? ticket.AssignedTechId : null),
EstimatedCompletion = eta, EstimatedCompletion = eta,
SlaDeadline = slaDeadline, SlaDeadline = slaDeadline,
StatusOverride = ticket?.StatusOverride ?? discoStatus, StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : discoStatus,
Summary = ticket?.Summary, Summary = ticket != null ? ticket.Summary : null,
NoteCount = noteCount, NoteCount = noteCount,
LatestNote = latestNote, LatestNote = latestNote,
LastModifiedDate = ticket?.LastModifiedDate ?? job.OpenedDate, LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate,
IsSlaBreached = slaBreached, IsSlaBreached = slaBreached,
IsSlaWarning = slaWarning, IsSlaWarning = slaWarning,
AgeBadge = ageBadge, AgeBadge = ageBadge,
@@ -225,48 +195,33 @@ namespace Disco.Plugins.ServiceTracker.Services
TotalOpen = tiles.Count, TotalOpen = tiles.Count,
SlaBreached = tiles.Count(t => t.IsSlaBreached), SlaBreached = tiles.Count(t => t.IsSlaBreached),
SlaWarning = tiles.Count(t => t.IsSlaWarning), SlaWarning = tiles.Count(t => t.IsSlaWarning),
AvgAgeDays = tiles.Count > 0 ? Math.Round(tiles.Average(t => t.AgeDays), 1) : 0, AvgAgeDays = tiles.Count > 0 ? Math.Round(tiles.Average(t => (double)t.AgeDays), 1) : 0,
OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0 OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0
}; };
// By priority
foreach (var p in _config.Priorities) foreach (var p in _config.Priorities)
{
stats.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id); stats.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id);
}
stats.Critical = tiles.Count(t => t.PriorityId == "critical"); stats.Critical = tiles.Count(t => t.PriorityId == "critical");
stats.High = tiles.Count(t => t.PriorityId == "high"); stats.High = tiles.Count(t => t.PriorityId == "high");
stats.Medium = tiles.Count(t => t.PriorityId == "medium"); stats.Medium = tiles.Count(t => t.PriorityId == "medium");
stats.Low = tiles.Count(t => t.PriorityId == "low"); stats.Low = tiles.Count(t => t.PriorityId == "low");
stats.Scheduled = tiles.Count(t => t.PriorityId == "scheduled"); stats.Scheduled = tiles.Count(t => t.PriorityId == "scheduled");
// By location
foreach (var l in _config.Locations) foreach (var l in _config.Locations)
{
stats.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id); stats.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id);
}
stats.InItOffice = tiles.Count(t => t.LocationId == "it-office"); stats.InItOffice = tiles.Count(t => t.LocationId == "it-office");
stats.WithUser = tiles.Count(t => t.LocationId == "with-user"); stats.WithUser = tiles.Count(t => t.LocationId == "with-user");
stats.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer"); stats.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer");
// By status
foreach (var tile in tiles) foreach (var tile in tiles)
{ {
var status = tile.StatusOverride ?? "Open"; var status = tile.StatusOverride ?? "Open";
if (!stats.ByStatus.ContainsKey(status)) if (!stats.ByStatus.ContainsKey(status)) stats.ByStatus[status] = 0;
stats.ByStatus[status] = 0;
stats.ByStatus[status]++; stats.ByStatus[status]++;
} }
// By tech
foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId))) foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId)))
{ {
var tech = tile.AssignedTechName ?? tile.AssignedTechId; var tech = tile.AssignedTechName ?? tile.AssignedTechId;
if (!stats.ByTech.ContainsKey(tech)) if (!stats.ByTech.ContainsKey(tech)) stats.ByTech[tech] = 0;
stats.ByTech[tech] = 0;
stats.ByTech[tech]++; stats.ByTech[tech]++;
} }
return stats; return stats;
} }
@@ -276,33 +231,18 @@ namespace Disco.Plugins.ServiceTracker.Services
DateTime? sla = null; DateTime? sla = null;
if (priority != null && priority.SlaHours > 0) if (priority != null && priority.SlaHours > 0)
sla = job.OpenedDate.AddHours(priority.SlaHours); sla = job.OpenedDate.AddHours(priority.SlaHours);
// Determine default location based on DeviceHeld
string locationId = _config.DefaultLocationId; string locationId = _config.DefaultLocationId;
if (job.DeviceHeld.HasValue) if (job.DeviceHeld.HasValue && !string.IsNullOrEmpty(job.DeviceHeldLocation))
{ {
if (!string.IsNullOrEmpty(job.DeviceHeldLocation)) var matchedLoc = _config.Locations.FirstOrDefault(
{ l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase));
// Try to match the Disco location to a configured location if (matchedLoc != null) locationId = matchedLoc.Id;
var matchedLoc = _config.Locations.FirstOrDefault(
l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase));
if (matchedLoc != null)
locationId = matchedLoc.Id;
}
} }
return new ServiceTicket return new ServiceTicket
{ {
JobId = job.Id, JobId = job.Id, PriorityId = _config.DefaultPriorityId, LocationId = locationId,
PriorityId = _config.DefaultPriorityId, AssignedTechId = job.OpenedTechUserId, EstimatedCompletion = job.ExpectedClosedDate,
LocationId = locationId, SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now
AssignedTechId = job.OpenedTechUserId,
EstimatedCompletion = job.ExpectedClosedDate,
SlaDeadline = sla,
StatusOverride = null,
Summary = null,
CreatedDate = DateTime.Now,
LastModifiedDate = DateTime.Now
}; };
} }
@@ -314,23 +254,14 @@ namespace Disco.Plugins.ServiceTracker.Services
var user = _database.Users.FirstOrDefault(u => u.UserId == userId); var user = _database.Users.FirstOrDefault(u => u.UserId == userId);
return user != null ? user.DisplayName : userId; return user != null ? user.DisplayName : userId;
} }
catch catch { return userId; }
{
return userId;
}
} }
// --- CRUD operations for tickets ---
public void UpdateTicket(int jobId, string priorityId, string locationId, public void UpdateTicket(int jobId, string priorityId, string locationId,
string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy) string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy)
{ {
var ticket = _dataStore.GetTicket(jobId); var ticket = _dataStore.GetTicket(jobId);
if (ticket == null) if (ticket == null) ticket = new ServiceTicket { JobId = jobId };
{
ticket = new ServiceTicket { JobId = jobId };
}
if (priorityId != null) ticket.PriorityId = priorityId; if (priorityId != null) ticket.PriorityId = priorityId;
if (locationId != null) ticket.LocationId = locationId; if (locationId != null) ticket.LocationId = locationId;
if (assignedTechId != null) ticket.AssignedTechId = assignedTechId; if (assignedTechId != null) ticket.AssignedTechId = assignedTechId;
@@ -338,39 +269,26 @@ namespace Disco.Plugins.ServiceTracker.Services
if (status != null) ticket.StatusOverride = status; if (status != null) ticket.StatusOverride = status;
if (summary != null) ticket.Summary = summary; if (summary != null) ticket.Summary = summary;
ticket.LastModifiedBy = modifiedBy; ticket.LastModifiedBy = modifiedBy;
// Recalculate SLA if priority changed
if (priorityId != null) if (priorityId != null)
{ {
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId); var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId);
if (priority != null && priority.SlaHours > 0) if (priority != null && priority.SlaHours > 0)
{
ticket.SlaDeadline = ticket.CreatedDate.AddHours(priority.SlaHours); ticket.SlaDeadline = ticket.CreatedDate.AddHours(priority.SlaHours);
}
else else
{
ticket.SlaDeadline = null; ticket.SlaDeadline = null;
}
} }
_dataStore.SaveTicket(ticket); _dataStore.SaveTicket(ticket);
} }
public void AddNote(int jobId, string authorId, string authorName, string content, string noteType) public void AddNote(int jobId, string authorId, string authorName, string content, string noteType)
{ {
var note = new TicketNote _dataStore.AddNote(jobId, new TicketNote
{ {
AuthorId = authorId, AuthorId = authorId, AuthorName = authorName,
AuthorName = authorName, Content = content, NoteType = noteType ?? "general"
Content = content, });
NoteType = noteType ?? "general"
};
_dataStore.AddNote(jobId, note);
} }
public ServiceTicket GetTicketDetail(int jobId) public ServiceTicket GetTicketDetail(int jobId) { return _dataStore.GetTicket(jobId); }
{
return _dataStore.GetTicket(jobId);
}
} }
} }