feat: integrate DashboardCache - skip full rebuild when data unchanged, invalidate on writes

This commit is contained in:
2026-05-11 09:04:58 +10:00
parent 0d5543a371
commit 2ca505eb52
+17 -42
View File
@@ -18,6 +18,12 @@ namespace Disco.Plugins.ServiceTracker.Services
public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null, string filterStatus = null, string filterTech = null, string sortBy = "newest")
{
// --- Cache check: return cached model if data files haven't changed ---
var filterKey = (filterPriority ?? "") + "|" + (filterLocation ?? "") + "|" + (filterStatus ?? "") + "|" + (filterTech ?? "");
var cached = DashboardCache.GetIfValid(_dataStore.DataDirectory, sortBy, filterKey);
if (cached != null) return cached;
// --- Full rebuild ---
var tiles = new List<DashboardTile>(); string sheetError = null;
var openJobs = _database.Jobs.Include("Device").Include("Device.DeviceModel").Include("User").Include("OpenedTechUser").Include("JobType").Include("JobSubTypes").Where(j => j.ClosedDate == null).ToList();
var allTickets = _dataStore.LoadAllTickets(); var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t);
@@ -43,12 +49,9 @@ namespace Disco.Plugins.ServiceTracker.Services
}
} catch (Exception ex) { sheetError = "Sheet error: " + ex.Message; }
}
// Separate Ready for Return and On Hold into sidebar lists
var rfr = tiles.Where(t => { var s = (t.StatusOverride ?? t.DiscoStatus ?? "").ToLower(); return s.Contains("ready for return") || s.Contains("ready for pickup"); }).ToList();
var onHold = tiles.Where(t => { var s = (t.StatusOverride ?? t.DiscoStatus ?? "").ToLower(); return s.Contains("on hold") && !rfr.Contains(t); }).ToList();
var main = tiles.Where(t => !rfr.Contains(t) && !onHold.Contains(t)).ToList();
if (!string.IsNullOrEmpty(filterPriority)) main = main.Where(t => t.PriorityId == filterPriority).ToList();
if (!string.IsNullOrEmpty(filterLocation)) main = main.Where(t => t.LocationId == filterLocation).ToList();
if (!string.IsNullOrEmpty(filterStatus)) main = main.Where(t => t.StatusOverride == filterStatus).ToList();
@@ -63,7 +66,11 @@ namespace Disco.Plugins.ServiceTracker.Services
case "newest": default: main = main.OrderByDescending(t => t.OpenedDate).ToList(); break;
}
var stats = BuildStats(main); stats.FromGoogleSheet = sheetCount;
return new DashboardViewModel { Tiles = main, ReadyForReturn = rfr, OnHold = onHold, Stats = stats, Config = _config, CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", SortBy = sortBy, GoogleSheetError = sheetError };
var result = new DashboardViewModel { Tiles = main, ReadyForReturn = rfr, OnHold = onHold, Stats = stats, Config = _config, CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", SortBy = sortBy, GoogleSheetError = sheetError };
// --- Store in cache ---
DashboardCache.Store(result, _dataStore.DataDirectory, sortBy, filterKey);
return result;
}
private DashboardTile BuildDiscoTile(Job job, ServiceTicket ticket)
@@ -75,8 +82,7 @@ namespace Disco.Plugins.ServiceTracker.Services
var loc = _config.Locations.FirstOrDefault(l => l.Id == lid) ?? _config.Locations.FirstOrDefault();
var sla = ticket != null ? ticket.SlaDeadline : (DateTime?)null;
if (!sla.HasValue && pri != null && pri.SlaHours > 0) sla = job.OpenedDate.AddHours(pri.SlaHours);
bool breach = sla.HasValue && now > sla.Value;
bool warn = !breach && sla.HasValue && pri != null && pri.SlaHours > 0 && now > sla.Value.AddHours(-pri.SlaHours * 0.25);
bool breach = sla.HasValue && now > sla.Value; bool warn = !breach && sla.HasValue && pri != null && pri.SlaHours > 0 && now > sla.Value.AddHours(-pri.SlaHours * 0.25);
int age = (int)(now - job.OpenedDate).TotalDays;
var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate;
string ds = "Open";
@@ -86,25 +92,7 @@ namespace Disco.Plugins.ServiceTracker.Services
string ln = null; int nc = 0;
if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0) { nc = ticket.Notes.Count; ln = ticket.Notes.OrderByDescending(n => n.Timestamp).First().Content; if (ln != null && ln.Length > 80) ln = ln.Substring(0, 77) + "..."; }
var tid = ticket != null ? ticket.AssignedTechId : job.OpenedTechUserId;
return new DashboardTile
{
JobId = job.Id, Source = "disco", DisplayId = "DIC#" + job.Id,
JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId,
DeviceSerialNumber = job.DeviceSerialNumber ?? "\u2014",
DeviceModelDescription = job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null,
DeviceComputerName = job.Device != null ? job.Device.DeviceDomainId : null,
UserId = job.UserId, UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId,
OpenedByTechId = job.OpenedTechUserId, OpenedByTechName = job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId,
OpenedDate = job.OpenedDate, ExpectedClosedDate = job.ExpectedClosedDate, DiscoStatus = ds,
PriorityId = pid, PriorityName = pri != null ? pri.Name : "Unknown", PriorityColor = pri != null ? pri.Color : "#999", PrioritySortOrder = pri != null ? pri.SortOrder : 99,
LocationId = lid, LocationName = loc != null ? loc.Name : "Unknown", LocationIcon = loc != null ? loc.Icon : "", LocationColor = loc != null ? loc.Color : "#999",
AssignedTechId = tid, AssignedTechName = ResolveTechName(tid), EstimatedCompletion = eta, SlaDeadline = sla,
StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : ds,
Summary = ticket != null ? ticket.Summary : null, NoteCount = nc, LatestNote = ln,
LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate,
IsSlaBreached = breach, IsSlaWarning = warn, AgeBadge = FmtAge(age), AgeDays = age, EtaDisplay = FmtEta(eta),
SortDate = breach && sla.HasValue ? sla.Value : eta.HasValue ? eta.Value : job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate
};
return new DashboardTile { JobId = job.Id, Source = "disco", DisplayId = "DIC#" + job.Id, JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId, DeviceSerialNumber = job.DeviceSerialNumber ?? "\u2014", DeviceModelDescription = job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null, DeviceComputerName = job.Device != null ? job.Device.DeviceDomainId : null, UserId = job.UserId, UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId, OpenedByTechId = job.OpenedTechUserId, OpenedByTechName = job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId, OpenedDate = job.OpenedDate, ExpectedClosedDate = job.ExpectedClosedDate, DiscoStatus = ds, PriorityId = pid, PriorityName = pri != null ? pri.Name : "Unknown", PriorityColor = pri != null ? pri.Color : "#999", PrioritySortOrder = pri != null ? pri.SortOrder : 99, LocationId = lid, LocationName = loc != null ? loc.Name : "Unknown", LocationIcon = loc != null ? loc.Icon : "", LocationColor = loc != null ? loc.Color : "#999", AssignedTechId = tid, AssignedTechName = ResolveTechName(tid), EstimatedCompletion = eta, SlaDeadline = sla, StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : ds, Summary = ticket != null ? ticket.Summary : null, NoteCount = nc, LatestNote = ln, LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate, IsSlaBreached = breach, IsSlaWarning = warn, AgeBadge = FmtAge(age), AgeDays = age, EtaDisplay = FmtEta(eta), SortDate = breach && sla.HasValue ? sla.Value : eta.HasValue ? eta.Value : job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate };
}
private DashboardTile BuildSheetTile(ExternalTicket ext, ServiceTicket st)
@@ -120,21 +108,7 @@ namespace Disco.Plugins.ServiceTracker.Services
if (st != null && st.Notes != null && st.Notes.Count > 0) { nc = st.Notes.Count; ln = st.Notes.OrderByDescending(n => n.Timestamp).First().Content; if (ln != null && ln.Length > 80) ln = ln.Substring(0, 77) + "..."; }
var rn = ext.RequesterName ?? ext.RequesterEmail ?? "Unknown";
if (string.IsNullOrEmpty(ext.RequesterName) && rn.Contains("@")) rn = rn.Split('@')[0].Replace(".", " ");
return new DashboardTile
{
JobId = ext.InternalId, Source = "ntt", DisplayId = "NTT#" + ext.InternalId,
JobTypeDescription = "NTT Sheet", DeviceSerialNumber = ext.TaskTitle ?? "\u2014", DeviceComputerName = ext.TaskTitle,
UserId = ext.RequesterEmail, UserDisplayName = rn, OpenedDate = ext.Timestamp, DiscoStatus = ext.RawStatus ?? "Open",
PriorityId = pid, PriorityName = pri != null ? pri.Name : "Unknown", PriorityColor = pri != null ? pri.Color : "#999", PrioritySortOrder = pri != null ? pri.SortOrder : 99,
LocationId = lid, LocationName = loc != null ? loc.Name : "Unknown", LocationIcon = loc != null ? loc.Icon : "", LocationColor = loc != null ? loc.Color : "#999",
AssignedTechId = tid, AssignedTechName = ResolveTechName(tid),
StatusOverride = st != null && st.StatusOverride != null ? st.StatusOverride : (ext.RawStatus ?? "Open"),
Summary = ext.IssueDescription, EstimatedCompletion = ext.PreferredDate, NoteCount = nc, LatestNote = ln,
LastModifiedDate = st != null ? st.LastModifiedDate : ext.Timestamp,
AgeBadge = FmtAge(age), AgeDays = age,
EtaDisplay = FmtEta(st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion : ext.PreferredDate),
SortDate = st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion.Value : ext.PreferredDate.HasValue ? ext.PreferredDate.Value : ext.Timestamp
};
return new DashboardTile { JobId = ext.InternalId, Source = "ntt", DisplayId = "NTT#" + ext.InternalId, JobTypeDescription = "NTT Sheet", DeviceSerialNumber = ext.TaskTitle ?? "\u2014", DeviceComputerName = ext.TaskTitle, UserId = ext.RequesterEmail, UserDisplayName = rn, OpenedDate = ext.Timestamp, DiscoStatus = ext.RawStatus ?? "Open", PriorityId = pid, PriorityName = pri != null ? pri.Name : "Unknown", PriorityColor = pri != null ? pri.Color : "#999", PrioritySortOrder = pri != null ? pri.SortOrder : 99, LocationId = lid, LocationName = loc != null ? loc.Name : "Unknown", LocationIcon = loc != null ? loc.Icon : "", LocationColor = loc != null ? loc.Color : "#999", AssignedTechId = tid, AssignedTechName = ResolveTechName(tid), StatusOverride = st != null && st.StatusOverride != null ? st.StatusOverride : (ext.RawStatus ?? "Open"), Summary = ext.IssueDescription, EstimatedCompletion = ext.PreferredDate, NoteCount = nc, LatestNote = ln, LastModifiedDate = st != null ? st.LastModifiedDate : ext.Timestamp, AgeBadge = FmtAge(age), AgeDays = age, EtaDisplay = FmtEta(st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion : ext.PreferredDate), SortDate = st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion.Value : ext.PreferredDate.HasValue ? ext.PreferredDate.Value : ext.Timestamp };
}
public string ResolveTechName(string id) { if (string.IsNullOrEmpty(id)) return null; foreach (var t in _config.Technicians) { if (t.Id == id || t.DisplayName == id) return t.DisplayName; if (!string.IsNullOrEmpty(t.Email) && t.Email.Equals(id, StringComparison.OrdinalIgnoreCase)) return t.DisplayName; if (t.DiscoUserIds != null) foreach (var d in t.DiscoUserIds) if (d.Equals(id, StringComparison.OrdinalIgnoreCase)) return t.DisplayName; } try { var u = _database.Users.FirstOrDefault(u2 => u2.UserId == id); if (u != null) return u.DisplayName; } catch { } return id; }
@@ -154,6 +128,7 @@ namespace Disco.Plugins.ServiceTracker.Services
ticket.LastModifiedBy = modifiedBy;
if (priorityId != null) { var p = _config.Priorities.FirstOrDefault(x => x.Id == priorityId); ticket.SlaDeadline = (p != null && p.SlaHours > 0) ? ticket.CreatedDate.AddHours(p.SlaHours) : (DateTime?)null; }
if (source == "ntt") _dataStore.SaveExternalTicket(ticket); else _dataStore.SaveTicket(ticket);
DashboardCache.Invalidate();
}
public void AddNote(int jobId, string source, string authorId, string authorName, string content, string noteType)
@@ -161,6 +136,7 @@ namespace Disco.Plugins.ServiceTracker.Services
var note = new TicketNote { AuthorId = authorId, AuthorName = authorName, Content = content, NoteType = noteType ?? "general" };
if (source == "ntt") _dataStore.AddExternalNote(jobId, note);
else { _dataStore.AddNote(jobId, note); try { var job = _database.Jobs.FirstOrDefault(j => j.Id == jobId); if (job != null) { _database.JobLogs.Add(new JobLog { JobId = jobId, TechUserId = authorId, Timestamp = DateTime.Now, Comments = "[Service Tracker] " + content }); _database.SaveChanges(); } } catch { } }
DashboardCache.Invalidate();
}
public void MarkCollected(int jobId, string source, string userId) { UpdateTicket(jobId, source, null, null, null, null, "Resolved", null, userId); }
@@ -186,8 +162,7 @@ namespace Disco.Plugins.ServiceTracker.Services
DateTime? sla = (p != null && p.SlaHours > 0) ? job.OpenedDate.AddHours(p.SlaHours) : (DateTime?)null;
string lid = _config.DefaultLocationId;
if (job.DeviceHeld.HasValue && !string.IsNullOrEmpty(job.DeviceHeldLocation)) { var ml = _config.Locations.FirstOrDefault(l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase)); if (ml != null) lid = ml.Id; }
var tid = ResolveDiscoTechToId(job.OpenedTechUserId);
return new ServiceTicket { JobId = job.Id, Source = "disco", PriorityId = _config.DefaultPriorityId, LocationId = lid, AssignedTechId = tid, EstimatedCompletion = job.ExpectedClosedDate, SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now };
return new ServiceTicket { JobId = job.Id, Source = "disco", PriorityId = _config.DefaultPriorityId, LocationId = lid, AssignedTechId = ResolveDiscoTechToId(job.OpenedTechUserId), EstimatedCompletion = job.ExpectedClosedDate, SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now };
}
private string ResolveDiscoTechToId(string uid) { if (string.IsNullOrEmpty(uid)) return null; foreach (var t in _config.Technicians) if (t.DiscoUserIds != null) foreach (var d in t.DiscoUserIds) if (d.Equals(uid, StringComparison.OrdinalIgnoreCase)) return t.Id; return uid; }
}