diff --git a/Services/ServiceTrackerService.cs b/Services/ServiceTrackerService.cs deleted file mode 100644 index 9b9237c..0000000 --- a/Services/ServiceTrackerService.cs +++ /dev/null @@ -1,478 +0,0 @@ -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 - { - public const string PluginVersion = "1.2.0"; - - 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 ServiceTrackerConfig Config { get { return _config; } } - - public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null, - string filterStatus = null, string filterTech = null, string sortBy = "due") - { - var tiles = new List(); - string sheetError = null; - - // --- Disco Jobs --- - 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; - } - } - } - - foreach (var job in openJobs) - { - ServiceTicket ticket; - ticketLookup.TryGetValue(job.Id, out ticket); - tiles.Add(BuildDiscoTile(job, ticket)); - } - - // --- Google Sheet --- - int sheetCount = 0; - if (_config.GoogleSheet != null && _config.GoogleSheet.Enabled) - { - try - { - var sheetSvc = new GoogleSheetService(_config.GoogleSheet, _dataStore.DataDirectory); - var sheetResult = sheetSvc.FetchTickets(); - if (sheetResult.Error != null) - { - sheetError = sheetResult.Error; - } - else - { - if (sheetResult.Warning != null) sheetError = sheetResult.Warning; - var extTickets = _dataStore.LoadExternalTickets(); - var extLookup = extTickets.ToDictionary(t => t.JobId, t => t); - - foreach (var ext in sheetResult.Tickets) - { - ServiceTicket st; - if (!extLookup.TryGetValue(ext.InternalId, out st)) - { - st = new ServiceTicket - { - JobId = ext.InternalId, Source = "ntt", - PriorityId = MapSheetPriority(ext.RawPriority), - LocationId = _config.DefaultLocationId, - AssignedTechId = ResolveSheetTech(ext.AssignedTo), - Summary = ext.IssueDescription, - CreatedDate = ext.Timestamp, LastModifiedDate = DateTime.Now - }; - _dataStore.SaveExternalTicket(st); - extLookup[ext.InternalId] = st; - } - tiles.Add(BuildSheetTile(ext, st)); - sheetCount++; - } - } - } - catch (Exception ex) - { - sheetError = "Sheet error: " + ex.Message; - } - } - - // --- Ready for Return --- - var readyForReturn = tiles.Where(t => - { - var status = (t.StatusOverride ?? t.DiscoStatus ?? "").ToLower(); - return status.Contains("ready for return") || status.Contains("ready for pickup"); - }).ToList(); - - // Remove from main tiles - var mainTiles = tiles.Where(t => !readyForReturn.Contains(t)).ToList(); - - // --- Filter --- - if (!string.IsNullOrEmpty(filterPriority)) mainTiles = mainTiles.Where(t => t.PriorityId == filterPriority).ToList(); - if (!string.IsNullOrEmpty(filterLocation)) mainTiles = mainTiles.Where(t => t.LocationId == filterLocation).ToList(); - if (!string.IsNullOrEmpty(filterStatus)) mainTiles = mainTiles.Where(t => t.StatusOverride == filterStatus).ToList(); - if (!string.IsNullOrEmpty(filterTech)) mainTiles = mainTiles.Where(t => t.AssignedTechId == filterTech).ToList(); - - // --- Sort --- - switch (sortBy) - { - case "priority": mainTiles = mainTiles.OrderBy(t => t.PrioritySortOrder).ThenBy(t => t.SortDate).ToList(); break; - case "age": mainTiles = mainTiles.OrderByDescending(t => t.AgeDays).ToList(); break; - case "modified": mainTiles = mainTiles.OrderByDescending(t => t.LastModifiedDate).ToList(); break; - case "sla": mainTiles = mainTiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2) - .ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList(); break; - default: mainTiles = mainTiles.OrderBy(t => t.SortDate).ToList(); break; - } - - var stats = BuildStats(mainTiles); - stats.FromGoogleSheet = sheetCount; - - return new DashboardViewModel - { - Tiles = mainTiles, ReadyForReturn = readyForReturn, - Stats = stats, Config = _config, - CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", - SortBy = sortBy, GoogleSheetError = sheetError - }; - } - - // --- Build Disco Tile --- - private DashboardTile BuildDiscoTile(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; - var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate; - - 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) + "..."; - } - - // Resolve tech display name - var techId = ticket != null ? ticket.AssignedTechId : job.OpenedTechUserId; - var techName = ResolveTechName(techId); - - 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 = 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 = techId, AssignedTechName = techName, - 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 = FormatAge(ageDays), AgeDays = ageDays, EtaDisplay = FormatEta(eta), - SortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value : eta.HasValue ? eta.Value : job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate - }; - } - - // --- Build Sheet Tile --- - private DashboardTile BuildSheetTile(ExternalTicket ext, ServiceTicket st) - { - var now = DateTime.Now; - var priorityId = st != null ? st.PriorityId : MapSheetPriority(ext.RawPriority); - var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId) ?? _config.Priorities.FirstOrDefault(); - var locationId = st != null ? st.LocationId : _config.DefaultLocationId; - var location = _config.Locations.FirstOrDefault(l => l.Id == locationId) ?? _config.Locations.FirstOrDefault(); - int ageDays = (int)(now - ext.Timestamp).TotalDays; - var techId = st != null ? st.AssignedTechId : ResolveSheetTech(ext.AssignedTo); - - string latestNote = null; int noteCount = 0; - if (st != null && st.Notes != null && st.Notes.Count > 0) - { - noteCount = st.Notes.Count; - latestNote = st.Notes.OrderByDescending(n => n.Timestamp).First().Content; - if (latestNote != null && latestNote.Length > 80) latestNote = latestNote.Substring(0, 77) + "..."; - } - - // Extract requester name from email - var requesterName = ext.RequesterEmail ?? "Unknown"; - if (requesterName.Contains("@")) - requesterName = requesterName.Split('@')[0].Replace(".", " "); - - return new DashboardTile - { - JobId = ext.InternalId, Source = "ntt", DisplayId = "NTT#" + ext.ExternalId.Replace("NTT", ""), - JobTypeDescription = "Google Sheet", - DeviceSerialNumber = ext.DeviceName ?? "\u2014", - DeviceComputerName = ext.DeviceName, - UserId = ext.RequesterEmail, UserDisplayName = requesterName, - OpenedDate = ext.Timestamp, DiscoStatus = ext.RawStatus ?? "Open", - 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 = techId, AssignedTechName = ResolveTechName(techId), - StatusOverride = st != null && st.StatusOverride != null ? st.StatusOverride : (ext.RawStatus ?? "Open"), - Summary = ext.IssueDescription, NoteCount = noteCount, LatestNote = latestNote, - LastModifiedDate = st != null ? st.LastModifiedDate : ext.Timestamp, - AgeBadge = FormatAge(ageDays), AgeDays = ageDays, EtaDisplay = FormatEta(st != null ? st.EstimatedCompletion : null), - SortDate = st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion.Value : ext.Timestamp - }; - } - - // --- Tech Resolution --- - public string ResolveTechName(string techIdOrEmail) - { - if (string.IsNullOrEmpty(techIdOrEmail)) return null; - foreach (var tech in _config.Technicians) - { - if (tech.Id == techIdOrEmail || tech.DisplayName == techIdOrEmail) return tech.DisplayName; - if (!string.IsNullOrEmpty(tech.Email) && tech.Email.Equals(techIdOrEmail, StringComparison.OrdinalIgnoreCase)) return tech.DisplayName; - if (tech.DiscoUserIds != null) - { - foreach (var did in tech.DiscoUserIds) - { - if (did.Equals(techIdOrEmail, StringComparison.OrdinalIgnoreCase)) return tech.DisplayName; - } - } - } - // Fallback to Disco user lookup - try - { - var user = _database.Users.FirstOrDefault(u => u.UserId == techIdOrEmail); - if (user != null) return user.DisplayName; - } - catch { } - return techIdOrEmail; - } - - private string ResolveSheetTech(string assignedTo) - { - if (string.IsNullOrEmpty(assignedTo)) return null; - foreach (var tech in _config.Technicians) - { - if (!string.IsNullOrEmpty(tech.Email) && tech.Email.Equals(assignedTo, StringComparison.OrdinalIgnoreCase)) return tech.Id; - if (tech.DisplayName.Equals(assignedTo, StringComparison.OrdinalIgnoreCase)) return tech.Id; - } - return assignedTo; - } - - private string MapSheetPriority(string raw) - { - if (string.IsNullOrEmpty(raw)) return _config.DefaultPriorityId; - raw = raw.ToLower().Trim(); - foreach (var p in _config.Priorities) - { - if (p.Name.ToLower() == raw || p.Id == raw) return p.Id; - } - if (raw.Contains("critical") || raw.Contains("urgent")) return "critical"; - if (raw.Contains("high")) return "high"; - if (raw.Contains("low")) return "low"; - return _config.DefaultPriorityId; - } - - // --- CRUD --- - public void UpdateTicket(int jobId, string source, string priorityId, string locationId, - string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy) - { - ServiceTicket ticket; - if (source == "ntt") - ticket = _dataStore.GetExternalTicket(jobId); - else - ticket = _dataStore.GetTicket(jobId); - - if (ticket == null) ticket = new ServiceTicket { JobId = jobId, Source = source ?? "disco" }; - if (ticket.ChangeLog == null) ticket.ChangeLog = new List(); - - Action trackChange = (field, oldVal, newVal) => - { - if (oldVal != newVal) ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = field, OldValue = oldVal, NewValue = newVal }); - }; - - if (priorityId != null && priorityId != ticket.PriorityId) { trackChange("Priority", ticket.PriorityId, priorityId); ticket.PriorityId = priorityId; } - if (locationId != null && locationId != ticket.LocationId) { trackChange("Location", ticket.LocationId, locationId); ticket.LocationId = locationId; } - if (assignedTechId != null && assignedTechId != ticket.AssignedTechId) { trackChange("Assigned Tech", ticket.AssignedTechId, assignedTechId); ticket.AssignedTechId = assignedTechId; } - if (status != null && status != ticket.StatusOverride) { trackChange("Status", ticket.StatusOverride ?? "(default)", string.IsNullOrEmpty(status) ? "(default)" : status); ticket.StatusOverride = status; } - if (summary != null && summary != ticket.Summary) { trackChange("Summary", null, "(updated)"); ticket.Summary = summary; } - if (eta.HasValue) - { - var oldEta = ticket.EstimatedCompletion.HasValue ? ticket.EstimatedCompletion.Value.ToString("dd MMM yyyy") : "none"; - var newEta = eta.Value.ToString("dd MMM yyyy"); - if (oldEta != newEta) { trackChange("ETA", oldEta, newEta); ticket.EstimatedCompletion = eta; } - } - - 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); - } - - public void AddNote(int jobId, string source, string authorId, string authorName, string content, string noteType) - { - 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); - // Write back to Disco - WriteNoteToDisco(jobId, authorId, content); - } - } - - private void WriteNoteToDisco(int jobId, string techUserId, string content) - { - try - { - var job = _database.Jobs.FirstOrDefault(j => j.Id == jobId); - if (job == null) return; - var log = new JobLog - { - JobId = jobId, - TechUserId = techUserId, - Timestamp = DateTime.Now, - Comments = "[Service Tracker] " + content - }; - _database.JobLogs.Add(log); - _database.SaveChanges(); - } - catch { } - } - - public void MarkCollected(int jobId, string source, string userId) - { - UpdateTicket(jobId, source, null, null, null, null, "Resolved", null, userId); - } - - public ServiceTicket GetTicketDetail(int jobId, string source) - { - if (source == "ntt") return _dataStore.GetExternalTicket(jobId); - return _dataStore.GetTicket(jobId); - } - - // --- Helpers --- - private string FormatAge(int ageDays) - { - if (ageDays == 0) return "Today"; - if (ageDays == 1) return "1 day"; - if (ageDays < 7) return ageDays + " days"; - if (ageDays < 30) return (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : ""); - return (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : ""); - } - - private string FormatEta(DateTime? eta) - { - if (!eta.HasValue) return "\u2014"; - var days = (int)(eta.Value - DateTime.Now).TotalDays; - if (days < 0) return Math.Abs(days) + "d overdue"; - if (days == 0) return "Today"; - if (days == 1) return "Tomorrow"; - return eta.Value.ToString("dd MMM"); - } - - private DashboardStats BuildStats(List tiles) - { - var s = 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) s.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id); - s.Critical = tiles.Count(t => t.PriorityId == "critical"); s.High = tiles.Count(t => t.PriorityId == "high"); - s.Medium = tiles.Count(t => t.PriorityId == "medium"); s.Low = tiles.Count(t => t.PriorityId == "low"); - s.Scheduled = tiles.Count(t => t.PriorityId == "scheduled"); - foreach (var l in _config.Locations) s.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id); - s.InItOffice = tiles.Count(t => t.LocationId == "it-office"); s.WithUser = tiles.Count(t => t.LocationId == "with-user"); - s.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer"); - foreach (var tile in tiles) - { - var st = tile.StatusOverride ?? "Open"; - if (!s.ByStatus.ContainsKey(st)) s.ByStatus[st] = 0; s.ByStatus[st]++; - } - foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId))) - { - var tn = tile.AssignedTechName ?? tile.AssignedTechId; - if (!s.ByTech.ContainsKey(tn)) s.ByTech[tn] = 0; s.ByTech[tn]++; - } - return s; - } - - private ServiceTicket CreateDefaultTicket(Job job) - { - var priority = _config.Priorities.FirstOrDefault(p => p.Id == _config.DefaultPriorityId); - DateTime? sla = (priority != null && priority.SlaHours > 0) ? job.OpenedDate.AddHours(priority.SlaHours) : (DateTime?)null; - string locId = _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) locId = ml.Id; - } - var techId = ResolveDiscoTechToId(job.OpenedTechUserId); - return new ServiceTicket - { - JobId = job.Id, Source = "disco", PriorityId = _config.DefaultPriorityId, LocationId = locId, - AssignedTechId = techId, EstimatedCompletion = job.ExpectedClosedDate, - SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now - }; - } - - private string ResolveDiscoTechToId(string discoUserId) - { - if (string.IsNullOrEmpty(discoUserId)) return null; - foreach (var tech in _config.Technicians) - { - if (tech.DiscoUserIds != null) - { - foreach (var did in tech.DiscoUserIds) - { - if (did.Equals(discoUserId, StringComparison.OrdinalIgnoreCase)) return tech.Id; - } - } - } - return discoUserId; - } - } -}