From 5da6de3165a64932d473ec14d5918386f1e79339 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 8 May 2026 16:07:55 +1000 Subject: [PATCH] Update WebHandler/Servicetrackerwebhandler.cs --- WebHandler/Servicetrackerwebhandler.cs | 312 +++++++++++++------------ 1 file changed, 169 insertions(+), 143 deletions(-) diff --git a/WebHandler/Servicetrackerwebhandler.cs b/WebHandler/Servicetrackerwebhandler.cs index dff3d06..830c935 100644 --- a/WebHandler/Servicetrackerwebhandler.cs +++ b/WebHandler/Servicetrackerwebhandler.cs @@ -1,164 +1,190 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; using Disco.Plugins.ServiceTracker.Models; -using Disco.Plugins.ServiceTracker.Services; -using Disco.Services.Plugins; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Web; -using System.Web.Mvc; -namespace Disco.Plugins.ServiceTracker.WebHandler +namespace Disco.Plugins.ServiceTracker.Services { - public class ServiceTrackerWebHandler : PluginWebHandler + public class ServiceTrackerService { - private ServiceTrackerDataStore GetDataStore() { return new ServiceTrackerDataStore(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "Plugins", "Disco.Plugins.ServiceTracker")); } - public override ActionResult ExecuteAction(string ActionName) { var a = ActionName != null ? ActionName.ToLower() : ""; try { switch (a) { case "": case "index": case "dashboard": return Dashboard(); case "update": return UpdateTicket(); case "addnote": return AddNote(); case "detail": return TicketDetail(); case "export": return ExportCsv(); case "config": return ConfigEditor(); case "saveconfig": return SaveConfig(); case "markcollected": return MarkCollected(); default: return new HttpNotFoundResult(); } } catch (Exception ex) { return Err("Error: " + ex.GetType().Name + " - " + ex.Message + "\n" + ex.StackTrace); } } - private ActionResult Dashboard() { var ds = GetDataStore(); return Html(BuildDashboardPage(new ServiceTrackerService(Database, ds).BuildDashboard(HostController.Request.QueryString["priority"], HostController.Request.QueryString["location"], HostController.Request.QueryString["status"], HostController.Request.QueryString["tech"], HostController.Request.QueryString["sort"] ?? "newest"))); } - private ActionResult UpdateTicket() { if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); int jobId; if (!int.TryParse(F("jobId"), out jobId)) return new HttpStatusCodeResult(400); var src = F("source") ?? "disco"; DateTime? eta = null; DateTime ep; if (DateTime.TryParse(F("eta"), out ep)) eta = ep; svc.UpdateTicket(jobId, src, F("priority"), F("location"), F("tech"), eta, F("status"), F("summary"), GetUser()); return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId + "&src=" + src); } - private ActionResult AddNote() { if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); int jobId; if (!int.TryParse(F("jobId"), out jobId)) return new HttpStatusCodeResult(400); var src = F("source") ?? "disco"; var user = GetUser(); svc.AddNote(jobId, src, user, svc.ResolveTechName(user) ?? user, F("note"), F("noteType") ?? "general"); return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId + "&src=" + src); } - private ActionResult TicketDetail() { int jobId; if (!int.TryParse(HostController.Request.QueryString["id"], out jobId)) return new HttpStatusCodeResult(400); var src = HostController.Request.QueryString["src"] ?? "disco"; var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); var cfg = ds.LoadConfig(); var ticket = svc.GetTicketDetail(jobId, src); Disco.Models.Repository.Job job = null; if (src == "disco") { job = Database.Jobs.Include("Device").Include("Device.DeviceModel").Include("User").Include("OpenedTechUser").Include("JobType").FirstOrDefault(j => j.Id == jobId); if (job == null) return Err("Job #" + jobId + " not found."); } return Html(BuildDetailPage(job, ticket, cfg, src, jobId)); } - private ActionResult ExportCsv() { var ds = GetDataStore(); var m = new ServiceTrackerService(Database, ds).BuildDashboard(); var sb = new StringBuilder("Id,Source,Device,User,Priority,Location,Status,Tech,Opened,ETA,Breached,Age,Summary\n"); foreach (var t in m.Tiles) sb.AppendLine(string.Join(",", Q(t.DisplayId), Q(t.Source), Q(t.DeviceSerialNumber), Q(t.UserDisplayName), Q(t.PriorityName), Q(t.LocationName), Q(t.StatusOverride), Q(t.AssignedTechName), t.OpenedDate.ToString("yyyy-MM-dd"), t.EstimatedCompletion.HasValue ? t.EstimatedCompletion.Value.ToString("yyyy-MM-dd") : "", t.IsSlaBreached, t.AgeDays, Q(t.Summary))); HostController.Response.Headers.Add("Content-Disposition", "attachment; filename=\"ServiceTracker_" + DateTime.Now.ToString("yyyyMMdd") + ".csv\""); return new ContentResult { Content = sb.ToString(), ContentType = "text/csv", ContentEncoding = Encoding.UTF8 }; } - private ActionResult ConfigEditor() { return Html(BuildConfigPage(GetDataStore().LoadConfig(), HostController.Request.QueryString["saved"] == "1", null)); } - private ActionResult SaveConfig() + public const string PluginVersion = "1.3.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 = "newest") { - if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - var ds = GetDataStore(); var cfg = ds.LoadConfig(); - try + var tiles = new List(); 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); + if (_config.AutoCreateTicketsForNewJobs) { foreach (var job in openJobs) { if (!ticketLookup.ContainsKey(job.Id)) { var nt = CreateDefaultTicket(job); _dataStore.SaveTicket(nt); ticketLookup[job.Id] = nt; } } } + foreach (var job in openJobs) { ServiceTicket ticket; ticketLookup.TryGetValue(job.Id, out ticket); tiles.Add(BuildDiscoTile(job, ticket)); } + int sheetCount = 0; + if (_config.GoogleSheet != null && _config.GoogleSheet.Enabled) { - int r; if (int.TryParse(F("refreshSeconds"), out r) && r >= 10) cfg.DashboardRefreshSeconds = r; - int di; if (int.TryParse(F("inactivitySeconds"), out di) && di >= 60) cfg.DetailInactivitySeconds = di; - cfg.AutoCreateTicketsForNewJobs = F("autoCreate") == "on"; - if (!string.IsNullOrEmpty(F("discoBaseUrl"))) cfg.DiscoBaseUrl = F("discoBaseUrl").TrimEnd('/'); - if (!string.IsNullOrEmpty(F("defaultPriority"))) cfg.DefaultPriorityId = F("defaultPriority"); - if (!string.IsNullOrEmpty(F("defaultLocation"))) cfg.DefaultLocationId = F("defaultLocation"); - if (cfg.GoogleSheet == null) cfg.GoogleSheet = new GoogleSheetConfig(); - cfg.GoogleSheet.Enabled = F("gsEnabled") == "on"; - if (!string.IsNullOrEmpty(F("gsSpreadsheetId"))) cfg.GoogleSheet.SpreadsheetId = F("gsSpreadsheetId"); - if (!string.IsNullOrEmpty(F("gsGid"))) cfg.GoogleSheet.GId = F("gsGid"); - int gsr; if (int.TryParse(F("gsRefresh"), out gsr) && gsr >= 1) cfg.GoogleSheet.RefreshMinutes = gsr; - int c; - if (int.TryParse(F("gsColTimestamp"), out c)) cfg.GoogleSheet.ColTimestamp = c; - if (int.TryParse(F("gsColEmail"), out c)) cfg.GoogleSheet.ColEmail = c; - if (int.TryParse(F("gsColDevice"), out c)) cfg.GoogleSheet.ColDeviceName = c; - if (int.TryParse(F("gsColLocation"), out c)) cfg.GoogleSheet.ColLocation = c; - if (int.TryParse(F("gsColIssue"), out c)) cfg.GoogleSheet.ColIssue = c; - if (int.TryParse(F("gsColPriority"), out c)) cfg.GoogleSheet.ColPriority = c; - if (int.TryParse(F("gsColStatus"), out c)) cfg.GoogleSheet.ColStatus = c; - if (int.TryParse(F("gsColAssigned"), out c)) cfg.GoogleSheet.ColAssignedTo = c; - if (int.TryParse(F("gsColRequestedBy"), out c)) cfg.GoogleSheet.ColRequestedBy = c; - if (int.TryParse(F("gsColPreferredDate"), out c)) cfg.GoogleSheet.ColPreferredDate = c; - if (int.TryParse(F("gsColNotes"), out c)) cfg.GoogleSheet.ColNotes = c; - if (int.TryParse(F("gsColCompletion"), out c)) cfg.GoogleSheet.ColCompletionDate = c; - int hr; if (int.TryParse(F("gsHeaderRows"), out hr)) cfg.GoogleSheet.HeaderRows = hr; - var techs = PJL(F("techsJson")); if (techs != null) cfg.Technicians = techs; - var pris = PJL(F("prioritiesJson")); if (pris != null) cfg.Priorities = pris; - var locs = PJL(F("locationsJson")); if (locs != null) cfg.Locations = locs; - var sr = F("statusOptions"); if (sr != null) cfg.StatusOptions = sr.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => s.Length > 0).ToList(); - ds.SaveConfig(cfg); return Html(BuildConfigPage(cfg, true, null)); - } catch (Exception ex) { return Html(BuildConfigPage(cfg, false, "Save failed: " + ex.Message)); } + try + { + var sr = new GoogleSheetService(_config.GoogleSheet, _dataStore.DataDirectory).FetchTickets(); + if (sr.Error != null) sheetError = sr.Error; + else + { + if (sr.Warning != null) sheetError = sr.Warning; + var ext = _dataStore.LoadExternalTickets(); var el = ext.ToDictionary(t => t.JobId, t => t); + foreach (var e in sr.Tickets) + { + ServiceTicket st; if (!el.TryGetValue(e.InternalId, out st)) + { st = new ServiceTicket { JobId = e.InternalId, Source = "ntt", PriorityId = GoogleSheetService.MapPriority(e.RawPriority), LocationId = _config.DefaultLocationId, AssignedTechId = ResolveSheetTech(e.AssignedTo), Summary = e.IssueDescription, EstimatedCompletion = e.PreferredDate, CreatedDate = e.Timestamp, LastModifiedDate = DateTime.Now }; _dataStore.SaveExternalTicket(st); el[e.InternalId] = st; } + tiles.Add(BuildSheetTile(e, st)); sheetCount++; + } + } + } catch (Exception ex) { sheetError = "Sheet error: " + ex.Message; } + } + 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 main = tiles.Where(t => !rfr.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(); + if (!string.IsNullOrEmpty(filterTech)) main = main.Where(t => t.AssignedTechId == filterTech).ToList(); + switch (sortBy) + { + case "priority": main = main.OrderBy(t => t.PrioritySortOrder).ThenBy(t => t.SortDate).ToList(); break; + case "age": main = main.OrderByDescending(t => t.AgeDays).ToList(); break; + case "modified": main = main.OrderByDescending(t => t.LastModifiedDate).ToList(); break; + case "sla": main = main.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2).ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList(); break; + case "due": main = main.OrderBy(t => t.SortDate).ToList(); break; + 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, Stats = stats, Config = _config, CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "", SortBy = sortBy, GoogleSheetError = sheetError }; } - private ActionResult MarkCollected() { if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); int jobId; if (!int.TryParse(F("jobId"), out jobId)) return new HttpStatusCodeResult(400); svc.MarkCollected(jobId, F("source") ?? "disco", GetUser()); return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Dashboard"); } - private string GetUser() { return (HostController.HttpContext.User != null && HostController.HttpContext.User.Identity != null) ? (HostController.HttpContext.User.Identity.Name ?? "system") : "system"; } - private ActionResult Html(string h) { return new ContentResult { Content = h, ContentType = "text/html", ContentEncoding = Encoding.UTF8 }; } - private ActionResult Err(string msg) { return Html("

Error

" + H(msg) + "
Dashboard"); } - private string Q(string v) { return "\"" + (v ?? "").Replace("\"", "\"\"") + "\""; } - private string H(string v) { return string.IsNullOrEmpty(v) ? "" : HttpUtility.HtmlEncode(v); } - private string F(string n) { return HostController.Request.Form[n]; } - private List PJL(string json) { if (string.IsNullOrEmpty(json)) return null; try { var l = JsonConvert.DeserializeObject>(json); return (l != null && l.Count > 0) ? l : null; } catch { return null; } } - - private string BuildDashboardPage(DashboardViewModel m) + private DashboardTile BuildDiscoTile(Job job, ServiceTicket ticket) { - var u = "/Plugin/Disco.Plugins.ServiceTracker"; var s = new StringBuilder(); - s.Append("Service Tracker"); - s.Append("

🛠 Service Tracker

Open: " + m.Stats.TotalOpen + ""); - if (m.Stats.FromGoogleSheet > 0) s.Append(" (+" + m.Stats.FromGoogleSheet + " NTT)"); - s.Append("
"); if (m.Stats.ByTech.Count > 0) { foreach (var kv in m.Stats.ByTech.OrderByDescending(x => x.Value)) s.Append("" + H(kv.Key) + ": " + kv.Value + ""); } s.Append("
"); - if (m.GoogleSheetError != null) s.Append("
Sheet: " + H(m.GoogleSheetError) + "
"); - if (m.Stats.SlaBreached > 0) s.Append("
" + m.Stats.SlaBreached + " SLA BREACHED
"); - if (m.Stats.SlaWarning > 0) s.Append("
⏰ " + m.Stats.SlaWarning + " approaching SLA
"); - s.Append("
"); foreach (var p in m.Config.Priorities) { int ct; m.Stats.ByPriority.TryGetValue(p.Id, out ct); s.Append("
" + ct + "
" + H(p.Name) + "
"); } - s.Append("
" + m.Stats.SlaBreached + "
Breached
" + m.Stats.AvgAgeDays.ToString("0.0") + "
Avg Age
"); - s.Append("
"); foreach (var l in m.Config.Locations) { int ct; m.Stats.ByLocation.TryGetValue(l.Id, out ct); if (ct > 0) s.Append("" + l.Icon + " " + H(l.Name) + " " + ct + ""); } s.Append("
"); - s.Append("
Sort:"); foreach (var o in new[] { "newest|Newest", "due|Due", "priority|Priority", "age|Age", "sla|SLA", "modified|Updated" }) { var p = o.Split('|'); s.Append("" + p[1] + ""); } if (!string.IsNullOrEmpty(m.CurrentFilter)) s.Append(""); s.Append("
"); - s.Append("
"); if (m.Tiles.Count == 0) s.Append("
✅ No open jobs
"); else foreach (var t in m.Tiles) s.Append(TileHtml(t, u)); s.Append("
"); - s.Append("

📦 Ready for Return

"); if (m.ReadyForReturn.Count == 0) s.Append("

None

"); else foreach (var r in m.ReadyForReturn) s.Append("
" + H(r.DisplayId) + "
" + H(r.DeviceComputerName ?? r.DeviceSerialNumber) + "
" + H(r.UserDisplayName) + "
"); s.Append("
"); - var rs = m.Config.DashboardRefreshSeconds; - s.Append("
Refresh: " + rs + "s
"); - s.Append("
v" + ServiceTrackerService.PluginVersion + "
"); - s.Append(""); - return s.ToString(); + var now = DateTime.Now; + var pid = (ticket != null ? ticket.PriorityId : null) ?? _config.DefaultPriorityId; + var pri = _config.Priorities.FirstOrDefault(p => p.Id == pid) ?? _config.Priorities.FirstOrDefault(); + var lid = (ticket != null ? ticket.LocationId : null) ?? _config.DefaultLocationId; + 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); + int age = (int)(now - job.OpenedDate).TotalDays; + var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate; + string ds = "Open"; + if (job.WaitingForUserAction.HasValue) ds = "Awaiting User Action"; + else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) ds = "Device Held"; + else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) ds = "Ready for Return"; + 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 + }; } - private string TileHtml(DashboardTile t, string u) { var s = new StringBuilder(); s.Append("
"); s.Append("
" + H(t.DisplayId) + "
" + t.AgeBadge + "
"); if (t.IsSlaBreached) s.Append("
⚠ SLA BREACHED
"); else if (t.IsSlaWarning) s.Append("
⏰ Warning
"); s.Append("
" + H(t.DeviceComputerName ?? t.DeviceSerialNumber) + "
"); if (t.DeviceModelDescription != null) s.Append("
" + H(t.DeviceModelDescription) + "
"); s.Append("
👤" + H(t.UserDisplayName ?? "\u2014") + "
" + t.LocationIcon + " " + H(t.LocationName) + "
📋" + H(t.StatusOverride ?? t.DiscoStatus) + "
"); if (!string.IsNullOrEmpty(t.AssignedTechName)) s.Append("
🔧" + H(t.AssignedTechName) + "
"); s.Append("
📅ETA: " + H(t.EtaDisplay) + "
"); if (!string.IsNullOrEmpty(t.Summary)) { var sm = t.Summary.Length > 60 ? t.Summary.Substring(0, 57) + "..." : t.Summary; s.Append("
" + H(sm) + "
"); } if (!string.IsNullOrEmpty(t.LatestNote)) { s.Append("
💬 " + H(t.LatestNote)); if (t.NoteCount > 1) s.Append(" (+" + (t.NoteCount - 1) + ")"); s.Append("
"); } s.Append("
" + H(t.PriorityName) + "" + H(t.Source == "ntt" ? "NTT" : t.JobTypeDescription) + "
"); return s.ToString(); } - - private string BuildDetailPage(Disco.Models.Repository.Job job, ServiceTicket ticket, ServiceTrackerConfig cfg, string src, int jobId) + private DashboardTile BuildSheetTile(ExternalTicket ext, ServiceTicket st) { - var u = "/Plugin/Disco.Plugins.ServiceTracker"; var s = new StringBuilder(); var did = src == "ntt" ? "NTT#" + jobId : "DIC#" + jobId; - s.Append("" + did + ""); - var jobTitle = ""; - if (job != null) { jobTitle = job.Device != null && job.Device.DeviceDomainId != null ? job.Device.DeviceDomainId : (job.DeviceSerialNumber ?? ""); } else if (ticket != null) { jobTitle = ticket.Summary ?? ""; } - if (jobTitle.Length > 60) jobTitle = jobTitle.Substring(0, 57) + "..."; - s.Append("
← Dashboard

" + H(did) + "

" + H(jobTitle) + "
"); if (src == "disco") s.Append("📂 Open in Disco"); s.Append("

Details

"); - if (job != null) { var di2 = job.Device != null ? job.Device.DeviceDomainId : null; s.Append(""); if (job.ExpectedClosedDate.HasValue) s.Append(""); } - else { s.Append(""); if (ticket != null) s.Append(""); } - s.Append("
Device" + H(job.DeviceSerialNumber) + (di2 != null ? " (" + H(di2) + ")" : "") + "
Model" + H(job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null) + "
User" + H(job.User != null ? job.User.DisplayName : job.UserId) + "
Type" + H(job.JobType != null ? job.JobType.Description : job.JobTypeId) + "
Opened" + job.OpenedDate.ToString("dd MMM yyyy HH:mm") + " by " + H(job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId) + "
Expected" + job.ExpectedClosedDate.Value.ToString("dd MMM yyyy") + "
SourceNTT Google Sheet
Summary" + H(ticket.Summary) + "
Created" + ticket.CreatedDate.ToString("dd MMM yyyy HH:mm") + "
"); - var tP = (ticket != null ? ticket.PriorityId : null) ?? cfg.DefaultPriorityId; var tL = (ticket != null ? ticket.LocationId : null) ?? cfg.DefaultLocationId; var tS = ticket != null ? ticket.StatusOverride : null; var tT = ticket != null ? ticket.AssignedTechId : ""; var tSu = ticket != null ? ticket.Summary : ""; var tE = (ticket != null && ticket.EstimatedCompletion.HasValue) ? ticket.EstimatedCompletion.Value.ToString("yyyy-MM-dd") : ""; - s.Append("

Settings

"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("

Activity

"); - if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0) { s.Append("
"); foreach (var n in ticket.Notes.OrderByDescending(n2 => n2.Timestamp)) { var tc = "#337AB7"; switch (n.NoteType) { case "escalation": tc = "#DC3545"; break; case "resolution": tc = "#28A745"; break; case "update": tc = "#FFC107"; break; } s.Append("
" + H(n.AuthorName ?? n.AuthorId) + "" + H(n.NoteType) + "" + n.Timestamp.ToString("dd MMM HH:mm") + "
" + H(n.Content) + "
"); } s.Append("
"); } else s.Append("

No notes yet.

"); - s.Append("
"); - if (ticket != null && ticket.ChangeLog != null && ticket.ChangeLog.Count > 0) { s.Append("

Changes

"); foreach (var ch in ticket.ChangeLog.OrderByDescending(c => c.Timestamp)) s.Append(""); s.Append("
WhenWhoFieldFromTo
" + ch.Timestamp.ToString("dd MMM HH:mm") + "" + H(ch.UserId) + "" + H(ch.Field) + "" + H(ch.OldValue ?? "-") + "" + H(ch.NewValue ?? "-") + "
"); } - s.Append("
"); var inact = cfg.DetailInactivitySeconds; - s.Append("
v" + ServiceTrackerService.PluginVersion + "
"); - return s.ToString(); + var now = DateTime.Now; + var pid = st != null ? st.PriorityId : GoogleSheetService.MapPriority(ext.RawPriority); + var pri = _config.Priorities.FirstOrDefault(p => p.Id == pid) ?? _config.Priorities.FirstOrDefault(); + var lid = st != null ? st.LocationId : _config.DefaultLocationId; + var loc = _config.Locations.FirstOrDefault(l => l.Id == lid) ?? _config.Locations.FirstOrDefault(); + int age = (int)(now - ext.Timestamp).TotalDays; + var tid = st != null ? st.AssignedTechId : ResolveSheetTech(ext.AssignedTo); + string ln = null; int nc = 0; + 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.ExternalId.Replace("NTT", ""), + 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 + }; } - private string BuildConfigPage(ServiceTrackerConfig cfg, bool saved, string error) + 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; } + private string ResolveSheetTech(string a) { if (string.IsNullOrEmpty(a)) return null; foreach (var t in _config.Technicians) { if (!string.IsNullOrEmpty(t.Email) && t.Email.Equals(a, StringComparison.OrdinalIgnoreCase)) return t.Id; if (t.DisplayName.Equals(a, StringComparison.OrdinalIgnoreCase)) return t.Id; } return a; } + + public void UpdateTicket(int jobId, string source, string priorityId, string locationId, string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy) { - var u = "/Plugin/Disco.Plugins.ServiceTracker"; var s = new StringBuilder(); - s.Append("Config"); - s.Append("
← Dashboard

⚙ Configuration

v" + ServiceTrackerService.PluginVersion + "
"); - if (saved) s.Append("
✅ Saved!
"); if (error != null) s.Append("
" + H(error) + "
"); - s.Append("
"); - s.Append("

General

"); - s.Append("
"); - s.Append("
"); - var gs = cfg.GoogleSheet ?? new GoogleSheetConfig(); - s.Append("

📄 Google Sheet (NTT)

"); - s.Append("

Column indices (0-based):

"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("

Use the 2PACX-... published key. Publish as CSV.

"); - s.Append("

🔧 Technicians

"); foreach (var t in cfg.Technicians) { var ids = t.DiscoUserIds != null ? string.Join(", ", t.DiscoUserIds) : ""; s.Append("
"); } - s.Append("
"); - s.Append("

Priorities

"); foreach (var p in cfg.Priorities.OrderBy(x => x.SortOrder)) s.Append("" + H(p.Name) + " (" + p.SlaHours + "h)"); s.Append("
"); - s.Append("

Locations

"); foreach (var l in cfg.Locations) s.Append("" + l.Icon + " " + H(l.Name) + ""); s.Append("
"); - s.Append("

Status Options

"); - s.Append("
Cancel
v" + ServiceTrackerService.PluginVersion + "
"); - s.Append(""); - return s.ToString(); + ServiceTicket ticket = source == "ntt" ? _dataStore.GetExternalTicket(jobId) : _dataStore.GetTicket(jobId); + if (ticket == null) ticket = new ServiceTicket { JobId = jobId, Source = source ?? "disco" }; + if (ticket.ChangeLog == null) ticket.ChangeLog = new List(); + 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 = "Tech", OldValue = ticket.AssignedTechId, NewValue = assignedTechId }); ticket.AssignedTechId = assignedTechId; } + if (status != null && status != ticket.StatusOverride) { ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Status", OldValue = ticket.StatusOverride ?? "(default)", NewValue = string.IsNullOrEmpty(status) ? "(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; } + if (eta.HasValue) { var o = ticket.EstimatedCompletion.HasValue ? ticket.EstimatedCompletion.Value.ToString("dd MMM yyyy") : "none"; var n = eta.Value.ToString("dd MMM yyyy"); if (o != n) { ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "ETA", OldValue = o, NewValue = n }); 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); } - private string CSS() { return @"*{box-sizing:border-box;margin:0;padding:0}body{font-family:'Segoe UI',sans-serif;background:#f0f2f5;color:#333}.header{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;background:#fff;border-bottom:2px solid #337AB7;box-shadow:0 1px 3px rgba(0,0,0,.08)}.hl{display:flex;align-items:center;gap:14px}.hm{display:flex;align-items:center;gap:12px;flex:1;justify-content:center;flex-wrap:wrap}.wt{font-size:12px;color:#555}.wt b{color:#337AB7}.hr{display:flex;gap:6px}h1{font-size:20px;color:#333}.sub{color:#666;font-size:13px}.btn{display:inline-block;padding:7px 14px;font-size:12px;border:none;border-radius:4px;cursor:pointer;text-decoration:none;color:#fff}.bp{background:#337AB7}.bp:hover{background:#286090}.bd{background:#777}.bd:hover{background:#555}.bdi{background:#5B2D8E}.bdi:hover{background:#4A2475}.btn-sm{padding:4px 10px;font-size:11px}.btn-lg{padding:10px 20px;font-size:14px}.alert{margin:8px 20px;padding:8px 14px;border-radius:4px;font-size:12px}.ad{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}.aw{background:#fff3cd;color:#856404;border:1px solid #ffeeba}.as{background:#d4edda;color:#155724;border:1px solid #c3e6cb}.sbar{display:flex;gap:10px;padding:14px 20px;flex-wrap:wrap}.sc{text-align:center;padding:10px 16px;background:#fff;border-radius:6px;min-width:70px;box-shadow:0 1px 2px rgba(0,0,0,.06);text-decoration:none;color:inherit;transition:transform .15s}.sc:hover{transform:translateY(-2px)}.ss{border-left:2px solid #eee;margin-left:6px;padding-left:18px}.sn{font-size:24px;font-weight:700}.sl{font-size:10px;color:#888;margin-top:2px}.lbar{display:flex;gap:6px;padding:0 20px;flex-wrap:wrap;margin-bottom:6px}.lc{display:inline-flex;align-items:center;gap:3px;padding:4px 10px;border-radius:14px;font-size:11px;color:#fff;text-decoration:none}.lc:hover{opacity:.85}.ctrls{display:flex;align-items:center;gap:5px;padding:6px 20px}.cl{font-size:11px;color:#888}.sb{padding:4px 10px;border-radius:12px;font-size:11px;background:#e9ecef;color:#555;text-decoration:none}.sb:hover{background:#d0d5db}.sb.act{background:#337AB7;color:#fff}.clr{background:#DC3545;color:#fff}.dash-layout{display:flex;gap:16px;padding:12px 20px;align-items:flex-start}.tile-grid{flex:1;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.rfr-panel{width:240px;flex-shrink:0;background:#fff;border-radius:8px;padding:14px;box-shadow:0 1px 3px rgba(0,0,0,.08)}.rfr-panel h3{font-size:14px;margin-bottom:10px;color:#333;border-bottom:1px solid #eee;padding-bottom:6px}.rfr-item{padding:8px;border:1px solid #eee;border-radius:6px;margin-bottom:8px;background:#fafbfc}.rfr-id{font-weight:700;font-size:12px;color:#337AB7}.rfr-dev{font-size:13px;font-weight:500}.rfr-user{font-size:11px;color:#888}.tile{background:#fff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.08);cursor:pointer;transition:box-shadow .15s,transform .15s;overflow:hidden}.tile:hover{box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-2px)}.tb{border:2px solid #DC3545;animation:pu 2s infinite}.tw{border:2px solid #FFC107}@keyframes pu{0%,100%{border-color:#DC3545}50%{border-color:#f8d7da}}.tp{height:4px;width:100%}.th{display:flex;justify-content:space-between;padding:8px 12px 3px}.tid{font-weight:700;font-size:14px;color:#337AB7}.ta{font-size:10px;color:#888;padding:2px 6px;background:#f5f5f5;border-radius:8px}.slb{margin:0 12px 4px;padding:3px 6px;border-radius:3px;font-size:10px;font-weight:700;text-align:center}.slb-b{background:#f8d7da;color:#721c24}.slb-w{background:#fff3cd;color:#856404}.td{padding:0 12px 4px}.tdn{font-weight:600;font-size:13px}.tdm{font-size:10px;color:#888}.tr{display:flex;align-items:center;gap:5px;padding:2px 12px;font-size:12px}.ti{width:16px;text-align:center;font-size:11px}.tlb{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:10px;color:#fff}.tsu{padding:4px 12px;font-size:11px;color:#555;border-top:1px solid #f0f0f0;font-style:italic}.tn{padding:4px 12px;font-size:10px;color:#777;background:#fafbfc}.tf{display:flex;justify-content:space-between;align-items:center;padding:6px 12px;border-top:1px solid #f0f0f0;background:#fafbfc}.pl{display:inline-block;padding:2px 8px;border-radius:8px;font-size:10px;color:#fff;font-weight:600}.tt{font-size:10px;color:#aaa}.empty{grid-column:1/-1;text-align:center;padding:50px;color:#888}.wls{padding:0 20px 16px}.wls h3{font-size:13px;color:#888;margin-bottom:6px}.wlb{display:flex;gap:10px;flex-wrap:wrap}.wli{background:#fff;border-radius:6px;padding:6px 14px;box-shadow:0 1px 2px rgba(0,0,0,.06);display:flex;align-items:center;gap:6px;font-size:12px}.foot{text-align:center;padding:16px;font-size:11px;color:#aaa}.rbar{display:flex;align-items:center;gap:8px;padding:6px 20px;background:#fff;border-top:1px solid #eee;position:sticky;bottom:0;z-index:10}.rbar.pau{background:#fff3cd}.rtx{font-size:11px;color:#888;min-width:100px}.rprog{flex:1;height:3px;background:#e9ecef;border-radius:2px;overflow:hidden}.rfill{height:100%;background:#337AB7;border-radius:2px;transition:width 1s linear;width:0%}.rbar.pau .rfill{background:#FFC107}.rtog{padding:3px 10px;font-size:10px;border:1px solid #ddd;border-radius:10px;background:#fff;cursor:pointer;color:#555}.mu{color:#999;font-style:italic;font-size:12px}@media(max-width:900px){.dash-layout{flex-direction:column}.rfr-panel{width:100%}}"; } - private string DCSS() { return @".bk{font-size:12px;color:#337AB7;text-decoration:none}.bk:hover{text-decoration:underline}.dg{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:16px 20px}@media(max-width:900px){.dg{grid-template-columns:1fr}}.dl,.dr{display:flex;flex-direction:column;gap:14px}.dc{background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}.dc h3{font-size:14px;color:#333;margin-bottom:10px;border-bottom:1px solid #eee;padding-bottom:6px}.dt{width:100%;border-collapse:collapse}.dt th{text-align:left;padding:5px 6px;font-size:11px;color:#888;width:120px}.dt td{padding:5px 6px;font-size:12px}.fg{margin-bottom:10px}.fg label{display:block;font-size:11px;font-weight:600;color:#555;margin-bottom:3px}.fc{width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:12px;font-family:inherit}.fc:focus{border-color:#337AB7;outline:none}.fcsm{width:auto;padding:3px 6px;font-size:11px}.nf{margin-bottom:12px}.nc{display:flex;gap:6px;margin-top:4px;align-items:center}.tl{border-left:2px solid #e0e0e0;margin-left:6px}.tli{display:flex;gap:10px;padding:6px 0}.tld{width:8px;height:8px;border-radius:50%;margin-top:3px;flex-shrink:0;margin-left:-5px}.tlc{flex:1}.tlh{display:flex;gap:6px;align-items:center;font-size:11px;margin-bottom:2px}.tla{font-weight:600;color:#333}.tldt{color:#aaa;margin-left:auto}.tlb{font-size:12px;color:#555;line-height:1.3}.clt{width:100%;border-collapse:collapse;font-size:11px}.clt th{text-align:left;padding:4px 6px;background:#f8f9fa;color:#888;font-weight:600;border-bottom:1px solid #eee}.clt td{padding:4px 6px;border-bottom:1px solid #f5f5f5}.ov{color:#DC3545;text-decoration:line-through}.nv{color:#28A745;font-weight:500}"; } - private string CCSS() { return @".cg{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:16px 20px}@media(max-width:900px){.cg{grid-template-columns:1fr}}.cc{background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}.cc h3{font-size:14px;color:#333;margin-bottom:10px;border-bottom:1px solid #eee;padding-bottom:6px}.ch{font-size:11px;color:#888;margin:6px 0}.je{font-family:'Consolas',monospace;font-size:11px;line-height:1.4;background:#f8f9fa}.je:focus{background:#fff}.pv{margin-top:8px;display:flex;gap:4px;flex-wrap:wrap}.pc{display:inline-block;padding:2px 8px;border-radius:8px;font-size:10px;color:#fff;font-weight:500}.ca{padding:16px 20px;display:flex;gap:8px}.col-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.tech-row{display:flex;gap:4px;margin-bottom:4px;align-items:center}.tech-row .fc{flex:1;font-size:11px;padding:4px 6px}.btn-rm{background:none;border:none;color:#DC3545;cursor:pointer;font-size:14px;padding:0 4px}.btn-rm:hover{color:#a71d2a}.hid{display:none}"; } + 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); 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 { } } + } + + 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) { return source == "ntt" ? _dataStore.GetExternalTicket(jobId) : _dataStore.GetTicket(jobId); } + private string FmtAge(int d) { if (d == 0) return "Today"; if (d == 1) return "1 day"; if (d < 7) return d + " days"; if (d < 30) return (d/7) + " wk" + (d/7>1?"s":""); return (d/30) + " mo" + (d/30>1?"s":""); } + private string FmtEta(DateTime? e) { if (!e.HasValue) return "\u2014"; var d=(int)(e.Value-DateTime.Now).TotalDays; if(d<0) return Math.Abs(d)+"d overdue"; if(d==0) return "Today"; if(d==1) return "Tomorrow"; return e.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 t in tiles) { var st = t.StatusOverride ?? "Open"; if (!s.ByStatus.ContainsKey(st)) s.ByStatus[st] = 0; s.ByStatus[st]++; } + foreach (var t in tiles.Where(t2 => !string.IsNullOrEmpty(t2.AssignedTechId))) { var tn = t.AssignedTechName ?? t.AssignedTechId; if (!s.ByTech.ContainsKey(tn)) s.ByTech[tn] = 0; s.ByTech[tn]++; } + return s; + } + + private ServiceTicket CreateDefaultTicket(Job job) + { + var p = _config.Priorities.FirstOrDefault(x => x.Id == _config.DefaultPriorityId); + 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 }; + } + 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; } } -} +} \ No newline at end of file