diff --git a/Services/ServiceTrackerService.cs b/Services/ServiceTrackerService.cs index 35bc872..9b9237c 100644 --- a/Services/ServiceTrackerService.cs +++ b/Services/ServiceTrackerService.cs @@ -9,7 +9,7 @@ namespace Disco.Plugins.ServiceTracker.Services { public class ServiceTrackerService { - public const string PluginVersion = "1.1.0"; + public const string PluginVersion = "1.2.0"; private readonly DiscoDataContext _database; private readonly ServiceTrackerDataStore _dataStore; @@ -22,15 +22,20 @@ namespace Disco.Plugins.ServiceTracker.Services _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(); + .Where(j => j.ClosedDate == null).ToList(); var allTickets = _dataStore.LoadAllTickets(); var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t); @@ -48,55 +53,100 @@ namespace Disco.Plugins.ServiceTracker.Services } } - var tiles = new List(); foreach (var job in openJobs) { ServiceTicket ticket; ticketLookup.TryGetValue(job.Id, out ticket); - tiles.Add(BuildTile(job, ticket)); + tiles.Add(BuildDiscoTile(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(); + // --- 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": - 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; + 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 = tiles, - Stats = BuildStats(tiles), - Config = _config, + Tiles = mainTiles, ReadyForReturn = readyForReturn, + Stats = stats, Config = _config, CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", - SortBy = sortBy + SortBy = sortBy, GoogleSheetError = sheetError }; } - private DashboardTile BuildTile(Job job, ServiceTicket ticket) + // --- Build Disco Tile --- + private DashboardTile BuildDiscoTile(Job job, ServiceTicket ticket) { var now = DateTime.Now; var priorityId = (ticket != null ? ticket.PriorityId : null) ?? _config.DefaultPriorityId; @@ -112,47 +162,29 @@ namespace Disco.Plugins.ServiceTracker.Services && 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 = "\u2014"; - 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; + 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) + "..."; + 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, + 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, @@ -161,169 +193,286 @@ namespace Disco.Plugins.ServiceTracker.Services 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, + 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, + NoteCount = noteCount, LatestNote = latestNote, LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate, - IsSlaBreached = slaBreached, - IsSlaWarning = slaWarning, - AgeBadge = ageBadge, - AgeDays = ageDays, - EtaDisplay = etaDisplay, - SortDate = sortDate + 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 stats = new DashboardStats + var s = new DashboardStats { - TotalOpen = tiles.Count, - SlaBreached = tiles.Count(t => t.IsSlaBreached), + 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 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 status = tile.StatusOverride ?? "Open"; - if (!stats.ByStatus.ContainsKey(status)) stats.ByStatus[status] = 0; - stats.ByStatus[status]++; + 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 tech = tile.AssignedTechName ?? tile.AssignedTechId; - if (!stats.ByTech.ContainsKey(tech)) stats.ByTech[tech] = 0; - stats.ByTech[tech]++; + var tn = tile.AssignedTechName ?? tile.AssignedTechId; + if (!s.ByTech.ContainsKey(tn)) s.ByTech[tn] = 0; s.ByTech[tn]++; } - return stats; + return s; } 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; + 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 matchedLoc = _config.Locations.FirstOrDefault( - l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase)); - if (matchedLoc != null) locationId = matchedLoc.Id; + 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, PriorityId = _config.DefaultPriorityId, LocationId = locationId, - AssignedTechId = job.OpenedTechUserId, EstimatedCompletion = job.ExpectedClosedDate, + 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 ResolveUserName(string userId) + private string ResolveDiscoTechToId(string discoUserId) { - if (string.IsNullOrEmpty(userId)) return null; - try + if (string.IsNullOrEmpty(discoUserId)) return null; + foreach (var tech in _config.Technicians) { - 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 (ticket.ChangeLog == null) ticket.ChangeLog = new List(); - - // Track changes - if (priorityId != null && priorityId != ticket.PriorityId) - { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Priority", OldValue = ticket.PriorityId, NewValue = priorityId }); - ticket.PriorityId = priorityId; - } - if (locationId != null && locationId != ticket.LocationId) - { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Location", OldValue = ticket.LocationId, NewValue = locationId }); - ticket.LocationId = locationId; - } - if (assignedTechId != null && assignedTechId != ticket.AssignedTechId) - { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Assigned Tech", OldValue = ticket.AssignedTechId, NewValue = assignedTechId }); - ticket.AssignedTechId = assignedTechId; - } - 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) + if (tech.DiscoUserIds != null) { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "ETA", OldValue = oldEta, NewValue = newEta }); - ticket.EstimatedCompletion = eta; + foreach (var did in tech.DiscoUserIds) + { + if (did.Equals(discoUserId, StringComparison.OrdinalIgnoreCase)) return tech.Id; + } } } - if (status != null && status != ticket.StatusOverride) - { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Status", OldValue = ticket.StatusOverride ?? "(Disco default)", NewValue = string.IsNullOrEmpty(status) ? "(Disco default)" : status }); - ticket.StatusOverride = status; - } - if (summary != null && summary != ticket.Summary) - { - ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Summary", OldValue = null, NewValue = "(updated)" }); - ticket.Summary = summary; - } - - ticket.LastModifiedBy = modifiedBy; - - // Recalculate SLA if priority changed - 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); + return discoUserId; } - - 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); } } }