diff --git a/WebHandler/ServiceTrackerWebHandler.cs b/WebHandler/ServiceTrackerWebHandler.cs index 3f2b664..b5e3a0f 100644 --- a/WebHandler/ServiceTrackerWebHandler.cs +++ b/WebHandler/ServiceTrackerWebHandler.cs @@ -16,7 +16,6 @@ namespace Disco.Plugins.ServiceTracker.WebHandler var dataPath = PluginConfigurationHandler.GetPluginDataDirectory( HostController.HttpContext.Application["Disco.Plugins.ServiceTracker"] as Plugin ?? new ServiceTrackerPlugin()); - // Fallback: use AppData path if (string.IsNullOrEmpty(dataPath)) { dataPath = System.IO.Path.Combine( @@ -51,13 +50,11 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { var dataStore = GetDataStore(); var service = new ServiceTrackerService(Database, dataStore); - var filterPriority = HostController.Request.QueryString["priority"]; var filterLocation = HostController.Request.QueryString["location"]; var filterStatus = HostController.Request.QueryString["status"]; var filterTech = HostController.Request.QueryString["tech"]; var sortBy = HostController.Request.QueryString["sort"] ?? "due"; - var model = service.BuildDashboard(filterPriority, filterLocation, filterStatus, filterTech, sortBy); return HtmlResult(BuildDashboardPage(model)); } @@ -66,14 +63,11 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - var dataStore = GetDataStore(); var service = new ServiceTrackerService(Database, dataStore); - int jobId; if (!int.TryParse(HostController.Request.Form["jobId"], out jobId)) return new HttpStatusCodeResult(400); - var priorityId = HostController.Request.Form["priority"]; var locationId = HostController.Request.Form["location"]; var techId = HostController.Request.Form["tech"]; @@ -83,10 +77,8 @@ namespace Disco.Plugins.ServiceTracker.WebHandler DateTime etaParsed; if (DateTime.TryParse(HostController.Request.Form["eta"], out etaParsed)) eta = etaParsed; - var currentUser = HostController.HttpContext.User?.Identity?.Name ?? "system"; service.UpdateTicket(jobId, priorityId, locationId, techId, eta, status, summary, currentUser); - return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Dashboard"); } @@ -94,20 +86,15 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - var dataStore = GetDataStore(); var service = new ServiceTrackerService(Database, dataStore); - int jobId; if (!int.TryParse(HostController.Request.Form["jobId"], out jobId)) return new HttpStatusCodeResult(400); - var content = HostController.Request.Form["note"]; var noteType = HostController.Request.Form["noteType"] ?? "general"; var currentUser = HostController.HttpContext.User?.Identity?.Name ?? "system"; - var userName = currentUser; // Could resolve display name from DB - - service.AddNote(jobId, currentUser, userName, content, noteType); + service.AddNote(jobId, currentUser, currentUser, content, noteType); return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId); } @@ -116,22 +103,17 @@ namespace Disco.Plugins.ServiceTracker.WebHandler int jobId; if (!int.TryParse(HostController.Request.QueryString["id"], out jobId)) return new HttpStatusCodeResult(400); - var dataStore = GetDataStore(); var service = new ServiceTrackerService(Database, dataStore); var config = dataStore.LoadConfig(); - - // Get the job var job = Database.Jobs .Include("Device").Include("Device.DeviceModel") .Include("User").Include("OpenedTechUser") .Include("JobType").Include("JobSubTypes") .Include("JobLogs") .FirstOrDefault(j => j.Id == jobId); - if (job == null) return new HttpNotFoundResult(); - var ticket = service.GetTicketDetail(jobId); return HtmlResult(BuildDetailPage(job, ticket, config)); } @@ -141,7 +123,6 @@ namespace Disco.Plugins.ServiceTracker.WebHandler var dataStore = GetDataStore(); var service = new ServiceTrackerService(Database, dataStore); var model = service.BuildDashboard(); - var sb = new StringBuilder(); sb.AppendLine("JobId,Device,User,Priority,Location,Status,AssignedTech,OpenedDate,ETA,SlaDeadline,SlaBreached,AgeDays,Summary,NoteCount"); foreach (var t in model.Tiles) @@ -154,24 +135,15 @@ namespace Disco.Plugins.ServiceTracker.WebHandler t.SlaDeadline?.ToString("yyyy-MM-dd HH:mm") ?? "", t.IsSlaBreached, t.AgeDays, Csv(t.Summary), t.NoteCount)); } - var fileName = "ServiceTracker_Export_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv"; HostController.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + fileName + "\""); - return new ContentResult - { - Content = sb.ToString(), - ContentType = "text/csv", - ContentEncoding = Encoding.UTF8 - }; + return new ContentResult { Content = sb.ToString(), ContentType = "text/csv", ContentEncoding = Encoding.UTF8 }; } - // --- HTML Builders --- - private ActionResult HtmlResult(string html) { return new ContentResult { Content = html, ContentType = "text/html", ContentEncoding = Encoding.UTF8 }; } - private string Csv(string v) { return "\"" + (v ?? "").Replace("\"", "\"\"") + "\""; } private string H(string v) { return string.IsNullOrEmpty(v) ? "" : HttpUtility.HtmlEncode(v); } @@ -179,97 +151,65 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { var pluginUrl = "/Plugin/Disco.Plugins.ServiceTracker"; var sb = new StringBuilder(); - - // --- Head --- sb.Append(""); sb.Append("Service Tracker Dashboard"); sb.Append(""); - // --- Header --- - sb.Append("
"); - sb.Append("
"); + // Header + sb.Append("
"); sb.Append("

🛠 Service Tracker

"); sb.Append("Open Jobs: " + model.Stats.TotalOpen + ""); - sb.Append("
"); - sb.Append("
"); + sb.Append("
"); sb.Append("⬇ Export CSV"); sb.Append("↻ Refresh"); sb.Append("
"); - // --- SLA Alert Banner --- + // SLA Alerts if (model.Stats.SlaBreached > 0) - { - sb.Append("
"); - sb.Append("⚠ " + model.Stats.SlaBreached + " job(s) have BREACHED SLA — immediate attention required!"); - sb.Append("
"); - } + sb.Append("
" + model.Stats.SlaBreached + " job(s) have BREACHED SLA — immediate attention required!
"); if (model.Stats.SlaWarning > 0) - { - sb.Append("
"); - sb.Append("⏰ " + model.Stats.SlaWarning + " job(s) approaching SLA deadline"); - sb.Append("
"); - } + sb.Append("
" + model.Stats.SlaWarning + " job(s) approaching SLA deadline
"); - // --- Stats Bar --- + // Stats Bar sb.Append("
"); foreach (var p in model.Config.Priorities) { - int count; - model.Stats.ByPriority.TryGetValue(p.Id, out count); + int count; model.Stats.ByPriority.TryGetValue(p.Id, out count); sb.Append(""); sb.Append("
" + count + "
"); - sb.Append("
" + H(p.Name) + "
"); - sb.Append("
"); + sb.Append("
" + H(p.Name) + "
"); } - sb.Append("
"); - sb.Append("
" + model.Stats.SlaBreached + "
"); - sb.Append("
SLA Breached
"); - sb.Append("
"); - sb.Append("
" + model.Stats.AvgAgeDays.ToString("0.0") + "
"); - sb.Append("
Avg Age (days)
"); + sb.Append("
" + model.Stats.SlaBreached + "
SLA Breached
"); + sb.Append("
" + model.Stats.AvgAgeDays.ToString("0.0") + "
Avg Age (days)
"); sb.Append("
"); - // --- Location Summary --- + // Location Summary sb.Append("
"); foreach (var loc in model.Config.Locations) { - int count; - model.Stats.ByLocation.TryGetValue(loc.Id, out count); + int count; model.Stats.ByLocation.TryGetValue(loc.Id, out count); if (count > 0) - { - sb.Append(""); - sb.Append(loc.Icon + " " + H(loc.Name) + " " + count + ""); - sb.Append(""); - } + sb.Append("" + loc.Icon + " " + H(loc.Name) + " " + count + ""); } sb.Append("
"); - // --- Sort Controls --- - sb.Append("
"); - sb.Append("Sort by:"); + // Sort Controls + sb.Append("
Sort by:"); string[] sortOptions = { "due|Due Date", "priority|Priority", "age|Age", "sla|SLA Status", "modified|Last Updated" }; foreach (var opt in sortOptions) { var parts = opt.Split('|'); var active = parts[0] == model.SortBy ? " active" : ""; - sb.Append("" + parts[1] + ""); + sb.Append("" + parts[1] + ""); } if (!string.IsNullOrEmpty(model.CurrentFilter)) - { sb.Append("✖ Clear Filter"); - } sb.Append("
"); - // --- Priority Legend --- - sb.Append("
"); - sb.Append("Priority:"); + // Priority Legend + sb.Append("
Priority:"); foreach (var p in model.Config.Priorities) { sb.Append("" + H(p.Name)); @@ -278,41 +218,51 @@ namespace Disco.Plugins.ServiceTracker.WebHandler } sb.Append("
"); - // --- Tile Grid --- + // Tile Grid sb.Append("
"); if (model.Tiles.Count == 0) - { - sb.Append("
"); - sb.Append("
"); - sb.Append("
No open jobs found
"); - sb.Append("
"); - } + sb.Append("
No open jobs found
"); else - { foreach (var tile in model.Tiles) - { sb.Append(BuildTileHtml(tile, pluginUrl)); - } - } sb.Append("
"); - // --- Tech Workload --- + // Tech Workload if (model.Stats.ByTech.Count > 0) { - sb.Append("
"); - sb.Append("

Tech Workload

"); - sb.Append("
"); + sb.Append("

Tech Workload

"); foreach (var kv in model.Stats.ByTech.OrderByDescending(x => x.Value)) - { - sb.Append("
"); - sb.Append("" + H(kv.Key) + ""); - sb.Append("" + kv.Value + ""); - sb.Append("
"); - } + sb.Append("
" + H(kv.Key) + "" + kv.Value + "
"); sb.Append("
"); } + // Auto-refresh bar + var refreshSeconds = model.Config.DashboardRefreshSeconds; + sb.Append("
"); + sb.Append("Auto-refresh in " + refreshSeconds + "s"); + sb.Append("
"); + sb.Append(""); + sb.Append("
"); + sb.Append(""); + + // Auto-refresh JavaScript + sb.Append(""); sb.Append(""); return sb.ToString(); } @@ -321,88 +271,33 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { var sb = new StringBuilder(); string borderClass = tile.IsSlaBreached ? "tile-breached" : tile.IsSlaWarning ? "tile-warning" : ""; - sb.Append("
"); - - // Priority strip sb.Append("
"); - - // Tile header - sb.Append("
"); - sb.Append("
#" + tile.JobId + "
"); - sb.Append("
" + tile.AgeBadge + "
"); - sb.Append("
"); - - // SLA badge + sb.Append("
#" + tile.JobId + "
" + tile.AgeBadge + "
"); if (tile.IsSlaBreached) - { sb.Append("
⚠ SLA BREACHED
"); - } else if (tile.IsSlaWarning) - { sb.Append("
⏰ SLA Warning
"); - } - - // Device info - sb.Append("
"); - sb.Append("
" + H(tile.DeviceComputerName ?? tile.DeviceSerialNumber) + "
"); + sb.Append("
" + H(tile.DeviceComputerName ?? tile.DeviceSerialNumber) + "
"); if (tile.DeviceModelDescription != null) sb.Append("
" + H(tile.DeviceModelDescription) + "
"); sb.Append("
"); - - // User - sb.Append("
"); - sb.Append("👤"); - sb.Append("" + H(tile.UserDisplayName ?? "—") + ""); - sb.Append("
"); - - // Location - sb.Append("
"); - sb.Append(""); - sb.Append(tile.LocationIcon + " " + H(tile.LocationName)); - sb.Append("
"); - - // Status - sb.Append("
"); - sb.Append("📋"); - sb.Append("" + H(tile.StatusOverride ?? tile.DiscoStatus) + ""); - sb.Append("
"); - - // Assigned tech + sb.Append("
👤" + H(tile.UserDisplayName ?? "—") + "
"); + sb.Append("
" + tile.LocationIcon + " " + H(tile.LocationName) + "
"); + sb.Append("
📋" + H(tile.StatusOverride ?? tile.DiscoStatus) + "
"); if (!string.IsNullOrEmpty(tile.AssignedTechName)) - { - sb.Append("
"); - sb.Append("🔧"); - sb.Append("" + H(tile.AssignedTechName) + ""); - sb.Append("
"); - } - - // ETA - sb.Append("
"); - sb.Append("📅"); - sb.Append("ETA: " + H(tile.EtaDisplay) + ""); - sb.Append("
"); - - // Summary / Latest note + sb.Append("
🔧" + H(tile.AssignedTechName) + "
"); + sb.Append("
📅ETA: " + H(tile.EtaDisplay) + "
"); if (!string.IsNullOrEmpty(tile.Summary)) - { sb.Append("
" + H(tile.Summary) + "
"); - } if (!string.IsNullOrEmpty(tile.LatestNote)) { - sb.Append("
"); - sb.Append("💬 " + H(tile.LatestNote)); - if (tile.NoteCount > 1) - sb.Append(" (+" + (tile.NoteCount - 1) + " more)"); + sb.Append("
💬 " + H(tile.LatestNote)); + if (tile.NoteCount > 1) sb.Append(" (+" + (tile.NoteCount - 1) + " more)"); sb.Append("
"); } - - // Priority label - sb.Append(""); - + sb.Append(""); sb.Append("
"); return sb.ToString(); } @@ -411,26 +306,16 @@ namespace Disco.Plugins.ServiceTracker.WebHandler { var pluginUrl = "/Plugin/Disco.Plugins.ServiceTracker"; var sb = new StringBuilder(); - sb.Append(""); sb.Append("Job #" + job.Id + " - Service Tracker"); sb.Append(""); - - // Header - sb.Append("
"); - sb.Append("
"); + sb.Append("
"); sb.Append("← Dashboard"); - sb.Append("

Job #" + job.Id + "

"); - sb.Append("
"); - - // Two-column layout + sb.Append("

Job #" + job.Id + "

"); sb.Append("
"); - // Left column - Job info + edit form - sb.Append("
"); - sb.Append("
"); - sb.Append("

Job Details

"); - sb.Append(""); + // Left column + sb.Append("

Job Details

"); sb.Append(""); sb.Append(""); sb.Append(""); @@ -443,108 +328,63 @@ namespace Disco.Plugins.ServiceTracker.WebHandler sb.Append("
Device" + H(job.DeviceSerialNumber) + (job.Device?.DeviceDomainId != null ? " (" + H(job.Device.DeviceDomainId) + ")" : "") + "
Model" + H(job.Device?.DeviceModel?.Description) + "
User" + H(job.User?.DisplayName ?? job.UserId) + "
"); // Edit form - sb.Append("
"); - sb.Append("

Service Tracker Settings

"); + sb.Append("

Service Tracker Settings

"); sb.Append("
"); sb.Append(""); - - // Priority dropdown sb.Append("
"); - - // Location dropdown sb.Append("
"); - - // Status dropdown - sb.Append("
"); foreach (var s in config.StatusOptions) { - var selected = ticket?.StatusOverride == s ? " selected" : ""; - sb.Append(""); + var sel = ticket?.StatusOverride == s ? " selected" : ""; + sb.Append(""); } sb.Append("
"); - - // Assigned tech - sb.Append("
"); - sb.Append("
"); - - // ETA - sb.Append("
"); - sb.Append("
"); - - // Summary - sb.Append("
"); - sb.Append("
"); - + sb.Append("
"); + sb.Append("
"); + sb.Append("
"); sb.Append(""); - sb.Append("
"); - sb.Append("
"); // end detail-left + sb.Append("
"); - // Right column - Notes/timeline - sb.Append("
"); - sb.Append("
"); - sb.Append("

Activity Log

"); - - // Add note form + // Right column - Notes + sb.Append("

Activity Log

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

No notes yet.

"); - } - - sb.Append("
"); // end detail-right - sb.Append("
"); // end detail-grid - + sb.Append("
"); sb.Append(""); return sb.ToString(); } @@ -616,6 +456,14 @@ body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;paddi .workload-item{background:#fff;border-radius:6px;padding:8px 16px;box-shadow:0 1px 2px rgba(0,0,0,.06);display:flex;align-items:center;gap:8px;} .wl-name{font-size:13px;} .wl-count{font-weight:700;font-size:16px;color:#337AB7;} .footer{text-align:center;padding:20px;font-size:12px;color:#aaa;} +.refresh-bar{display:flex;align-items:center;gap:10px;padding:8px 24px;background:#fff;border-top:1px solid #eee;position:sticky;bottom:0;z-index:10;} +.refresh-bar.paused{background:#fff3cd;} +.refresh-text{font-size:12px;color:#888;min-width:140px;} +.refresh-progress{flex:1;height:4px;background:#e9ecef;border-radius:2px;overflow:hidden;} +.refresh-fill{height:100%;background:#337AB7;border-radius:2px;transition:width 1s linear;width:0%;} +.refresh-bar.paused .refresh-fill{background:#FFC107;} +.refresh-toggle{padding:4px 12px;font-size:11px;border:1px solid #ddd;border-radius:12px;background:#fff;cursor:pointer;color:#555;} +.refresh-toggle:hover{background:#f5f5f5;} .muted{color:#999;font-style:italic;} "; } @@ -623,8 +471,7 @@ body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;paddi private string GetDetailCSS() { return @" -.back-link{font-size:13px;color:#337AB7;text-decoration:none;} -.back-link:hover{text-decoration:underline;} +.back-link{font-size:13px;color:#337AB7;text-decoration:none;} .back-link:hover{text-decoration:underline;} .detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;padding:20px 24px;} @media(max-width:900px){.detail-grid{grid-template-columns:1fr;}} .detail-left,.detail-right{display:flex;flex-direction:column;gap:16px;}