diff --git a/WebHandler/ServiceTrackerWebHandler.cs b/WebHandler/ServiceTrackerWebHandler.cs deleted file mode 100644 index d7884e2..0000000 --- a/WebHandler/ServiceTrackerWebHandler.cs +++ /dev/null @@ -1,599 +0,0 @@ -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 -{ - public class ServiceTrackerWebHandler : PluginWebHandler - { - 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 in action '" + a + "': " + ex.GetType().Name + " - " + ex.Message + "\n" + ex.StackTrace); - } - } - - // ===================== ACTIONS ===================== - - private ActionResult Dashboard() - { - var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); - var fp = HostController.Request.QueryString["priority"]; - var fl = HostController.Request.QueryString["location"]; - var fs = HostController.Request.QueryString["status"]; - var ft = HostController.Request.QueryString["tech"]; - var sb = HostController.Request.QueryString["sort"] ?? "due"; - return Html(BuildDashboardPage(svc.BuildDashboard(fp, fl, fs, ft, sb))); - } - - private ActionResult UpdateTicket() - { - if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - try - { - var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); - int jobId; if (!int.TryParse(HostController.Request.Form["jobId"], out jobId)) return new HttpStatusCodeResult(400); - var src = HostController.Request.Form["source"] ?? "disco"; - DateTime? eta = null; DateTime ep; if (DateTime.TryParse(HostController.Request.Form["eta"], out ep)) eta = ep; - svc.UpdateTicket(jobId, src, HostController.Request.Form["priority"], HostController.Request.Form["location"], - HostController.Request.Form["tech"], eta, HostController.Request.Form["status"], - HostController.Request.Form["summary"], GetUser()); - return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId + "&src=" + src); - } - catch (Exception ex) { return Err("Save error: " + ex.Message); } - } - - private ActionResult AddNote() - { - if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - try - { - var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); - int jobId; if (!int.TryParse(HostController.Request.Form["jobId"], out jobId)) return new HttpStatusCodeResult(400); - var src = HostController.Request.Form["source"] ?? "disco"; - var user = GetUser(); - svc.AddNote(jobId, src, user, svc.ResolveTechName(user) ?? user, - HostController.Request.Form["note"], HostController.Request.Form["noteType"] ?? "general"); - return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId + "&src=" + src); - } - catch (Exception ex) { return Err("Note error: " + ex.Message); } - } - - 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 config = ds.LoadConfig(); - ServiceTicket 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("Disco Job #" + jobId + " not found."); - } - return Html(BuildDetailPage(job, ticket, config, src, jobId)); - } - - private ActionResult ExportCsv() - { - var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); - var m = svc.BuildDashboard(); - var sb = new StringBuilder(); - sb.AppendLine("Id,Source,Device,User,Priority,Location,Status,Tech,Opened,ETA,SLA,Breached,Age,Summary,Notes"); - 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.SlaDeadline.HasValue ? t.SlaDeadline.Value.ToString("yyyy-MM-dd HH:mm") : "", - t.IsSlaBreached, t.AgeDays, Q(t.Summary), t.NoteCount)); - } - 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() - { - var ds = GetDataStore(); var config = ds.LoadConfig(); - return Html(BuildConfigPage(config, HostController.Request.QueryString["saved"] == "1", null)); - } - - private ActionResult SaveConfig() - { - if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - var ds = GetDataStore(); var config = ds.LoadConfig(); - try - { - int r; if (int.TryParse(F("refreshSeconds"), out r) && r >= 10) config.DashboardRefreshSeconds = r; - int di; if (int.TryParse(F("inactivitySeconds"), out di) && di >= 60) config.DetailInactivitySeconds = di; - config.AutoCreateTicketsForNewJobs = F("autoCreate") == "on"; - if (!string.IsNullOrEmpty(F("discoBaseUrl"))) config.DiscoBaseUrl = F("discoBaseUrl").TrimEnd('/'); - if (!string.IsNullOrEmpty(F("defaultPriority"))) config.DefaultPriorityId = F("defaultPriority"); - if (!string.IsNullOrEmpty(F("defaultLocation"))) config.DefaultLocationId = F("defaultLocation"); - - // Google Sheet - if (config.GoogleSheet == null) config.GoogleSheet = new GoogleSheetConfig(); - config.GoogleSheet.Enabled = F("gsEnabled") == "on"; - if (!string.IsNullOrEmpty(F("gsSpreadsheetId"))) config.GoogleSheet.SpreadsheetId = F("gsSpreadsheetId"); - if (!string.IsNullOrEmpty(F("gsGid"))) config.GoogleSheet.GId = F("gsGid"); - int gsr; if (int.TryParse(F("gsRefresh"), out gsr) && gsr >= 1) config.GoogleSheet.RefreshMinutes = gsr; - int c; if (int.TryParse(F("gsColTimestamp"), out c)) config.GoogleSheet.ColTimestamp = c; - if (int.TryParse(F("gsColEmail"), out c)) config.GoogleSheet.ColEmail = c; - if (int.TryParse(F("gsColDevice"), out c)) config.GoogleSheet.ColDeviceName = c; - if (int.TryParse(F("gsColLocation"), out c)) config.GoogleSheet.ColLocation = c; - if (int.TryParse(F("gsColIssue"), out c)) config.GoogleSheet.ColIssue = c; - if (int.TryParse(F("gsColPriority"), out c)) config.GoogleSheet.ColPriority = c; - if (int.TryParse(F("gsColStatus"), out c)) config.GoogleSheet.ColStatus = c; - if (int.TryParse(F("gsColAssigned"), out c)) config.GoogleSheet.ColAssignedTo = c; - int hr; if (int.TryParse(F("gsHeaderRows"), out hr)) config.GoogleSheet.HeaderRows = hr; - - // JSON lists - TryParseJson(F("techsJson"), ref config.Technicians); - TryParseJson(F("prioritiesJson"), ref config.Priorities); - TryParseJson(F("locationsJson"), ref config.Locations); - - var sr = F("statusOptions"); - if (sr != null) config.StatusOptions = sr.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => s.Length > 0).ToList(); - - ds.SaveConfig(config); - return Html(BuildConfigPage(config, true, null)); - } - catch (Exception ex) - { - return Html(BuildConfigPage(config, false, "Save failed: " + ex.Message)); - } - } - - private ActionResult MarkCollected() - { - if (HostController.Request.HttpMethod != "POST") return new HttpStatusCodeResult(405); - try - { - var ds = GetDataStore(); var svc = new ServiceTrackerService(Database, ds); - int jobId; if (!int.TryParse(HostController.Request.Form["jobId"], out jobId)) return new HttpStatusCodeResult(400); - var src = HostController.Request.Form["source"] ?? "disco"; - svc.MarkCollected(jobId, src, GetUser()); - return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Dashboard"); - } - catch (Exception ex) { return Err("Error: " + ex.Message); } - } - - // ===================== HELPERS ===================== - 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("

Service Tracker Error

" + H(msg) + "

Back to 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 void TryParseJson(string json, ref List target) { if (!string.IsNullOrEmpty(json)) { try { var l = JsonConvert.DeserializeObject>(json); if (l != null && l.Count > 0) target = l; } catch { } } } - - // ===================== DASHBOARD ===================== - private string BuildDashboardPage(DashboardViewModel m) - { - 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(" (incl. " + m.Stats.FromGoogleSheet + " from NTT sheet)"); - 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
"); - - // Stats - s.Append("
"); - foreach (var p in m.Config.Priorities) { int c; m.Stats.ByPriority.TryGetValue(p.Id, out c); s.Append("
" + c + "
" + H(p.Name) + "
"); } - s.Append("
" + m.Stats.SlaBreached + "
Breached
"); - s.Append("
" + m.Stats.AvgAgeDays.ToString("0.0") + "
Avg Age
"); - - // Locations - s.Append("
"); - foreach (var l in m.Config.Locations) { int c; m.Stats.ByLocation.TryGetValue(l.Id, out c); if (c > 0) s.Append("" + l.Icon + " " + H(l.Name) + " " + c + ""); } - s.Append("
"); - - // Sort - s.Append("
Sort:"); - foreach (var o in new[] { "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("✖ Clear"); - s.Append("
"); - - // Main layout: tiles + ready sidebar - s.Append("
"); - - // Tile grid - s.Append("
"); - if (m.Tiles.Count == 0) s.Append("
No open jobs
"); - else foreach (var t in m.Tiles) s.Append(TileHtml(t, u, m.Config)); - s.Append("
"); - - // Ready for Return sidebar - s.Append("

📦 Ready for Return

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

None right now

"); - else foreach (var r in m.ReadyForReturn) - { - s.Append("
" + H(r.DisplayId) + "
"); - s.Append("
" + H(r.DeviceComputerName ?? r.DeviceSerialNumber) + "
"); - s.Append("
" + H(r.UserDisplayName) + "
"); - s.Append("
"); - s.Append(""); - s.Append("
"); - } - s.Append("
"); - - // Workload - if (m.Stats.ByTech.Count > 0) - { - s.Append("

Tech Workload

"); - foreach (var kv in m.Stats.ByTech.OrderByDescending(x => x.Value)) s.Append("
" + H(kv.Key) + "" + kv.Value + "
"); - s.Append("
"); - } - - // Refresh bar - var rs = m.Config.DashboardRefreshSeconds; - s.Append("
Refresh in " + rs + "s
"); - s.Append("
Service Tracker v" + ServiceTrackerService.PluginVersion + "
"); - s.Append(""); - s.Append(""); - return s.ToString(); - } - - private string TileHtml(DashboardTile t, string u, ServiceTrackerConfig cfg) - { - var s = new StringBuilder(); - var bc = t.IsSlaBreached ? " tb" : t.IsSlaWarning ? " tw" : ""; - s.Append("
"); - s.Append("
"); - s.Append("
" + H(t.DisplayId) + "
" + t.AgeBadge + "
"); - if (t.IsSlaBreached) s.Append("
⚠ SLA BREACHED
"); - else if (t.IsSlaWarning) s.Append("
⏰ SLA Warning
"); - s.Append("
" + H(t.DeviceComputerName ?? t.DeviceSerialNumber) + "
"); - if (t.DeviceModelDescription != null) s.Append("
" + H(t.DeviceModelDescription) + "
"); - s.Append("
"); - s.Append("
👤" + H(t.UserDisplayName ?? "\u2014") + "
"); - s.Append("
" + t.LocationIcon + " " + H(t.LocationName) + "
"); - s.Append("
📋" + 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 sum = t.Summary.Length > 60 ? t.Summary.Substring(0, 57) + "..." : t.Summary; s.Append("
" + H(sum) + "
"); } - 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 Sheet" : t.JobTypeDescription) + "
"); - return s.ToString(); - } - - // ===================== DETAIL PAGE ===================== - private string BuildDetailPage(Disco.Models.Repository.Job job, ServiceTicket ticket, ServiceTrackerConfig cfg, string src, int jobId) - { - var u = "/Plugin/Disco.Plugins.ServiceTracker"; - var s = new StringBuilder(); - var displayId = src == "ntt" ? "NTT#" + jobId : "DIC#" + jobId; - s.Append("" + displayId + ""); - - s.Append("
← Dashboard

" + H(displayId) + "

"); - if (src == "disco") s.Append("📂 Open in Disco"); - s.Append("
"); - - s.Append("
"); - - // Left - Info + Edit - s.Append("
"); - s.Append("

Details

"); - if (job != null) - { - var domId = job.Device != null ? job.Device.DeviceDomainId : null; - s.Append(""); - s.Append(""); - s.Append(""); - s.Append(""); - s.Append(""); - if (job.ExpectedClosedDate.HasValue) s.Append(""); - } - else - { - s.Append(""); - if (ticket != null) - { - s.Append(""); - s.Append(""); - } - } - s.Append("
Device" + H(job.DeviceSerialNumber) + (domId != null ? " (" + H(domId) + ")" : "") + "
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 Close" + job.ExpectedClosedDate.Value.ToString("dd MMM yyyy") + "
SourceNTT Google Sheet
Summary" + H(ticket.Summary) + "
Created" + ticket.CreatedDate.ToString("dd MMM yyyy HH:mm") + "
"); - - // Edit form - 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("
"); - - // Tech dropdown - s.Append("
"); - - s.Append("
"); - s.Append("
"); - s.Append("
"); - - // Right - Notes + Change Log - s.Append("
"); - s.Append("

Activity

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

No notes yet.

"); - s.Append("
"); - - // Change Log - 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 ?? "\u2014") + "" + H(ch.NewValue ?? "\u2014") + "
"); - } - - s.Append("
"); // dr, dg - - // Inactivity reload - var inact = cfg.DetailInactivitySeconds; - s.Append("
Service Tracker v" + ServiceTrackerService.PluginVersion + "
"); - s.Append(""); - s.Append(""); - return s.ToString(); - } - - // ===================== CONFIG PAGE ===================== - private string BuildConfigPage(ServiceTrackerConfig cfg, bool saved, string error) - { - 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("
"); - - // General - s.Append("

General

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

📄 Google Sheet (NTT)

"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - s.Append("
"); - 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("

Sheet must be published to web as CSV (File > Share > Publish to web).

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

🔧 Technicians

"); - foreach (var t in cfg.Technicians) s.Append(TechRow(t)); - s.Append("
"); - s.Append("
"); - - // Priorities - s.Append("

🎯 Priorities

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

📍 Locations

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

📋 Status Options

"); - s.Append("
"); - - s.Append("
"); - s.Append("
Cancel
"); - s.Append("
Service Tracker v" + ServiceTrackerService.PluginVersion + "
"); - - // Config JS - tech list management + JSON validation - s.Append(@""); - s.Append(""); - return s.ToString(); - } - - private string TechRow(TechEntry t) - { - var ids = t.DiscoUserIds != null ? string.Join(", ", t.DiscoUserIds) : ""; - return "
" - + "" - + "" - + "
"; - } - - // ===================== CSS ===================== - 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}.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;margin-right:3px} -.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}.tv{flex:1} -.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 20px;color:#888;font-size:15px} -.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} -.wln{font-size:12px}.wlc{font-weight:700;font-size:15px;color:#337AB7} -.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:120px}.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;vertical-align:top}.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}.tlt{font-size:10px;text-transform:capitalize}.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;line-height:1.3} -.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}"; - } - } -}