using Disco.Data.Repository; using Disco.Models.Repository; using Disco.Plugins.ServiceTracker.Models; using System; using System.Collections.Generic; using System.Linq; namespace Disco.Plugins.ServiceTracker.Services { public class ServiceTrackerService { private readonly DiscoDataContext _database; private readonly ServiceTrackerDataStore _dataStore; private readonly ServiceTrackerConfig _config; public ServiceTrackerService(DiscoDataContext database, ServiceTrackerDataStore dataStore) { _database = database; _dataStore = dataStore; _config = dataStore.LoadConfig(); } public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null, string filterStatus = null, string filterTech = null, string sortBy = "due") { 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); if (_config.AutoCreateTicketsForNewJobs) { foreach (var job in openJobs) { if (!ticketLookup.ContainsKey(job.Id)) { var newTicket = CreateDefaultTicket(job); _dataStore.SaveTicket(newTicket); ticketLookup[job.Id] = newTicket; } } } var tiles = new List(); foreach (var job in openJobs) { ServiceTicket ticket; ticketLookup.TryGetValue(job.Id, out ticket); tiles.Add(BuildTile(job, ticket)); } if (!string.IsNullOrEmpty(filterPriority)) tiles = tiles.Where(t => t.PriorityId == filterPriority).ToList(); if (!string.IsNullOrEmpty(filterLocation)) tiles = tiles.Where(t => t.LocationId == filterLocation).ToList(); if (!string.IsNullOrEmpty(filterStatus)) tiles = tiles.Where(t => t.StatusOverride == filterStatus).ToList(); if (!string.IsNullOrEmpty(filterTech)) tiles = tiles.Where(t => t.AssignedTechId == filterTech).ToList(); switch (sortBy) { case "priority": tiles = tiles.OrderBy(t => t.PrioritySortOrder).ThenBy(t => t.SortDate).ToList(); break; case "age": tiles = tiles.OrderByDescending(t => t.AgeDays).ToList(); break; case "modified": tiles = tiles.OrderByDescending(t => t.LastModifiedDate).ToList(); break; case "sla": tiles = tiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2) .ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList(); break; case "due": default: tiles = tiles.OrderBy(t => t.SortDate).ToList(); break; } return new DashboardViewModel { Tiles = tiles, Stats = BuildStats(tiles), Config = _config, CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", SortBy = sortBy }; } private DashboardTile BuildTile(Job job, ServiceTicket ticket) { 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(); var slaDeadline = ticket != null ? ticket.SlaDeadline : (DateTime?)null; if (!slaDeadline.HasValue && priority != null && priority.SlaHours > 0) slaDeadline = job.OpenedDate.AddHours(priority.SlaHours); bool slaBreached = slaDeadline.HasValue && now > slaDeadline.Value; bool slaWarning = !slaBreached && slaDeadline.HasValue && priority != null && priority.SlaHours > 0 && now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25); int ageDays = (int)(now - job.OpenedDate).TotalDays; string ageBadge; if (ageDays == 0) ageBadge = "Today"; else if (ageDays == 1) ageBadge = "1 day"; else if (ageDays < 7) ageBadge = ageDays + " days"; else if (ageDays < 30) ageBadge = (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : ""); else ageBadge = (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : ""); var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate; string etaDisplay = "—"; if (eta.HasValue) { var etaDays = (int)(eta.Value - now).TotalDays; if (etaDays < 0) etaDisplay = Math.Abs(etaDays) + "d overdue"; else if (etaDays == 0) etaDisplay = "Today"; else if (etaDays == 1) etaDisplay = "Tomorrow"; else etaDisplay = eta.Value.ToString("dd MMM"); } DateTime sortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value : eta.HasValue ? eta.Value : job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate; string discoStatus = "Open"; if (job.WaitingForUserAction.HasValue) discoStatus = "Awaiting User Action"; else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) discoStatus = "Device Held"; else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) discoStatus = "Ready for Return"; string latestNote = null; int noteCount = 0; if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0) { noteCount = ticket.Notes.Count; var latest = ticket.Notes.OrderByDescending(n => n.Timestamp).First(); latestNote = latest.Content; if (latestNote != null && latestNote.Length > 80) latestNote = latestNote.Substring(0, 77) + "..."; } return new DashboardTile { JobId = job.Id, JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId, DeviceSerialNumber = job.DeviceSerialNumber ?? "—", 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 = discoStatus, PriorityId = priorityId, PriorityName = priority != null ? priority.Name : "Unknown", PriorityColor = priority != null ? priority.Color : "#999", PrioritySortOrder = priority != null ? priority.SortOrder : 99, LocationId = locationId, LocationName = location != null ? location.Name : "Unknown", LocationIcon = location != null ? location.Icon : "", LocationColor = location != null ? location.Color : "#999", AssignedTechId = ticket != null ? ticket.AssignedTechId : null, AssignedTechName = ResolveUserName(ticket != null ? ticket.AssignedTechId : null), EstimatedCompletion = eta, SlaDeadline = slaDeadline, StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : discoStatus, Summary = ticket != null ? ticket.Summary : null, NoteCount = noteCount, LatestNote = latestNote, LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate, IsSlaBreached = slaBreached, IsSlaWarning = slaWarning, AgeBadge = ageBadge, AgeDays = ageDays, EtaDisplay = etaDisplay, SortDate = sortDate }; } private DashboardStats BuildStats(List tiles) { var stats = new DashboardStats { TotalOpen = tiles.Count, SlaBreached = tiles.Count(t => t.IsSlaBreached), SlaWarning = tiles.Count(t => t.IsSlaWarning), AvgAgeDays = tiles.Count > 0 ? Math.Round(tiles.Average(t => (double)t.AgeDays), 1) : 0, OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0 }; foreach (var p in _config.Priorities) stats.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id); stats.Critical = tiles.Count(t => t.PriorityId == "critical"); stats.High = tiles.Count(t => t.PriorityId == "high"); stats.Medium = tiles.Count(t => t.PriorityId == "medium"); stats.Low = tiles.Count(t => t.PriorityId == "low"); stats.Scheduled = tiles.Count(t => t.PriorityId == "scheduled"); foreach (var l in _config.Locations) stats.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id); stats.InItOffice = tiles.Count(t => t.LocationId == "it-office"); stats.WithUser = tiles.Count(t => t.LocationId == "with-user"); stats.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer"); foreach (var tile in tiles) { var status = tile.StatusOverride ?? "Open"; if (!stats.ByStatus.ContainsKey(status)) stats.ByStatus[status] = 0; stats.ByStatus[status]++; } foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId))) { var tech = tile.AssignedTechName ?? tile.AssignedTechId; if (!stats.ByTech.ContainsKey(tech)) stats.ByTech[tech] = 0; stats.ByTech[tech]++; } return stats; } private ServiceTicket CreateDefaultTicket(Job job) { var priority = _config.Priorities.FirstOrDefault(p => p.Id == _config.DefaultPriorityId); DateTime? sla = null; if (priority != null && priority.SlaHours > 0) sla = job.OpenedDate.AddHours(priority.SlaHours); string locationId = _config.DefaultLocationId; if (job.DeviceHeld.HasValue && !string.IsNullOrEmpty(job.DeviceHeldLocation)) { var matchedLoc = _config.Locations.FirstOrDefault( l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase)); if (matchedLoc != null) locationId = matchedLoc.Id; } return new ServiceTicket { JobId = job.Id, PriorityId = _config.DefaultPriorityId, LocationId = locationId, AssignedTechId = job.OpenedTechUserId, EstimatedCompletion = job.ExpectedClosedDate, SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now }; } private string ResolveUserName(string userId) { if (string.IsNullOrEmpty(userId)) return null; try { var user = _database.Users.FirstOrDefault(u => u.UserId == userId); return user != null ? user.DisplayName : userId; } catch { return userId; } } public void UpdateTicket(int jobId, string priorityId, string locationId, string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy) { var ticket = _dataStore.GetTicket(jobId); if (ticket == null) ticket = new ServiceTicket { JobId = jobId }; if (priorityId != null) ticket.PriorityId = priorityId; if (locationId != null) ticket.LocationId = locationId; if (assignedTechId != null) ticket.AssignedTechId = assignedTechId; if (eta.HasValue) ticket.EstimatedCompletion = eta; if (status != null) ticket.StatusOverride = status; if (summary != null) ticket.Summary = summary; ticket.LastModifiedBy = modifiedBy; if (priorityId != null) { var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId); if (priority != null && priority.SlaHours > 0) ticket.SlaDeadline = ticket.CreatedDate.AddHours(priority.SlaHours); else ticket.SlaDeadline = null; } _dataStore.SaveTicket(ticket); } public void AddNote(int jobId, string authorId, string authorName, string content, string noteType) { _dataStore.AddNote(jobId, new TicketNote { AuthorId = authorId, AuthorName = authorName, Content = content, NoteType = noteType ?? "general" }); } public ServiceTicket GetTicketDetail(int jobId) { return _dataStore.GetTicket(jobId); } } }