Files
disco-service-tracker-plugin/Services/ServiceTrackerService.cs
T

377 lines
15 KiB
C#

using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Plugins.ServiceTracker.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Disco.Plugins.ServiceTracker.Services
{
/// <summary>
/// Core service that combines Disco Job data with ServiceTracker metadata
/// to produce the dashboard view model.
/// </summary>
public class ServiceTrackerService
{
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 DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null,
string filterStatus = null, string filterTech = null, string sortBy = "due")
{
// Get all open jobs from Disco
var openJobs = _database.Jobs
.Include("Device")
.Include("Device.DeviceModel")
.Include("User")
.Include("OpenedTechUser")
.Include("JobType")
.Include("JobSubTypes")
.Where(j => j.ClosedDate == null)
.ToList();
// Load all service tracker tickets
var allTickets = _dataStore.LoadAllTickets();
var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t);
// Auto-create tickets for jobs that don't have one (if enabled)
if (_config.AutoCreateTicketsForNewJobs)
{
foreach (var job in openJobs)
{
if (!ticketLookup.ContainsKey(job.Id))
{
var newTicket = CreateDefaultTicket(job);
_dataStore.SaveTicket(newTicket);
ticketLookup[job.Id] = newTicket;
}
}
}
// Build tiles
var tiles = new List<DashboardTile>();
foreach (var job in openJobs)
{
ServiceTicket ticket;
ticketLookup.TryGetValue(job.Id, out ticket);
tiles.Add(BuildTile(job, ticket));
}
// Apply filters
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();
// 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 ?? DateTime.MaxValue).ToList();
break;
case "due":
default:
tiles = tiles.OrderBy(t => t.SortDate).ToList();
break;
}
// Build stats
var stats = BuildStats(tiles);
return new DashboardViewModel
{
Tiles = tiles,
Stats = stats,
Config = _config,
CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "",
SortBy = sortBy
};
}
private DashboardTile BuildTile(Job job, ServiceTicket ticket)
{
var now = DateTime.Now;
// Resolve priority
var priorityId = ticket?.PriorityId ?? _config.DefaultPriorityId;
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId)
?? _config.Priorities.FirstOrDefault();
// Resolve location
var locationId = ticket?.LocationId ?? _config.DefaultLocationId;
var location = _config.Locations.FirstOrDefault(l => l.Id == locationId)
?? _config.Locations.FirstOrDefault();
// Compute SLA
var slaDeadline = ticket?.SlaDeadline;
if (!slaDeadline.HasValue && priority != null && priority.SlaHours > 0)
{
slaDeadline = job.OpenedDate.AddHours(priority.SlaHours);
}
bool slaBreached = slaDeadline.HasValue && now > slaDeadline.Value;
bool slaWarning = !slaBreached && slaDeadline.HasValue &&
priority != null && priority.SlaHours > 0 &&
now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25);
// Compute age
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" : "");
// ETA display
var eta = ticket?.EstimatedCompletion ?? job.ExpectedClosedDate;
string etaDisplay = "—";
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");
}
// Sort date: use SLA deadline if breached, then ETA, then expected close, then opened
DateTime sortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value
: eta ?? job.ExpectedClosedDate ?? job.OpenedDate;
// Determine Disco status string
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";
// Latest note
string latestNote = null;
int noteCount = 0;
if (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) + "...";
}
return new DashboardTile
{
JobId = job.Id,
JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId,
DeviceSerialNumber = job.DeviceSerialNumber ?? "—",
DeviceModelDescription = job.Device?.DeviceModel != null ? job.Device.DeviceModel.Description : null,
DeviceComputerName = job.Device?.DeviceDomainId,
UserId = job.UserId,
UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId,
OpenedByTechId = job.OpenedTechUserId,
OpenedByTechName = job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId,
OpenedDate = job.OpenedDate,
ExpectedClosedDate = job.ExpectedClosedDate,
DiscoStatus = discoStatus,
PriorityId = priorityId,
PriorityName = priority?.Name ?? "Unknown",
PriorityColor = priority?.Color ?? "#999",
PrioritySortOrder = priority?.SortOrder ?? 99,
LocationId = locationId,
LocationName = location?.Name ?? "Unknown",
LocationIcon = location?.Icon ?? "",
LocationColor = location?.Color ?? "#999",
AssignedTechId = ticket?.AssignedTechId,
AssignedTechName = ResolveUserName(ticket?.AssignedTechId),
EstimatedCompletion = eta,
SlaDeadline = slaDeadline,
StatusOverride = ticket?.StatusOverride ?? discoStatus,
Summary = ticket?.Summary,
NoteCount = noteCount,
LatestNote = latestNote,
LastModifiedDate = ticket?.LastModifiedDate ?? job.OpenedDate,
IsSlaBreached = slaBreached,
IsSlaWarning = slaWarning,
AgeBadge = ageBadge,
AgeDays = ageDays,
EtaDisplay = etaDisplay,
SortDate = sortDate
};
}
private DashboardStats BuildStats(List<DashboardTile> tiles)
{
var stats = 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 => t.AgeDays), 1) : 0,
OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0
};
// By priority
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");
// By location
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");
// By status
foreach (var tile in tiles)
{
var status = tile.StatusOverride ?? "Open";
if (!stats.ByStatus.ContainsKey(status))
stats.ByStatus[status] = 0;
stats.ByStatus[status]++;
}
// By tech
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]++;
}
return stats;
}
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);
// Determine default location based on DeviceHeld
string locationId = _config.DefaultLocationId;
if (job.DeviceHeld.HasValue)
{
if (!string.IsNullOrEmpty(job.DeviceHeldLocation))
{
// Try to match the Disco location to a configured location
var matchedLoc = _config.Locations.FirstOrDefault(
l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase));
if (matchedLoc != null)
locationId = matchedLoc.Id;
}
}
return new ServiceTicket
{
JobId = job.Id,
PriorityId = _config.DefaultPriorityId,
LocationId = locationId,
AssignedTechId = job.OpenedTechUserId,
EstimatedCompletion = job.ExpectedClosedDate,
SlaDeadline = sla,
StatusOverride = null,
Summary = null,
CreatedDate = DateTime.Now,
LastModifiedDate = DateTime.Now
};
}
private string ResolveUserName(string userId)
{
if (string.IsNullOrEmpty(userId)) return null;
try
{
var user = _database.Users.FirstOrDefault(u => u.UserId == userId);
return user != null ? user.DisplayName : userId;
}
catch
{
return userId;
}
}
// --- CRUD operations for tickets ---
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 (priorityId != null) ticket.PriorityId = priorityId;
if (locationId != null) ticket.LocationId = locationId;
if (assignedTechId != null) ticket.AssignedTechId = assignedTechId;
if (eta.HasValue) ticket.EstimatedCompletion = eta;
if (status != null) ticket.StatusOverride = status;
if (summary != null) 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);
}
public void AddNote(int jobId, string authorId, string authorName, string content, string noteType)
{
var note = new TicketNote
{
AuthorId = authorId,
AuthorName = authorName,
Content = content,
NoteType = noteType ?? "general"
};
_dataStore.AddNote(jobId, note);
}
public ServiceTicket GetTicketDetail(int jobId)
{
return _dataStore.GetTicket(jobId);
}
}
}