feat: Google Sheet integration, tech resolution, DIC#/NTT# prefixes, Ready for Return, Disco write-back
This commit is contained in:
+332
-183
@@ -9,7 +9,7 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
{
|
{
|
||||||
public class ServiceTrackerService
|
public class ServiceTrackerService
|
||||||
{
|
{
|
||||||
public const string PluginVersion = "1.1.0";
|
public const string PluginVersion = "1.2.0";
|
||||||
|
|
||||||
private readonly DiscoDataContext _database;
|
private readonly DiscoDataContext _database;
|
||||||
private readonly ServiceTrackerDataStore _dataStore;
|
private readonly ServiceTrackerDataStore _dataStore;
|
||||||
@@ -22,15 +22,20 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
_config = dataStore.LoadConfig();
|
_config = dataStore.LoadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceTrackerConfig Config { get { return _config; } }
|
||||||
|
|
||||||
public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null,
|
public DashboardViewModel BuildDashboard(string filterPriority = null, string filterLocation = null,
|
||||||
string filterStatus = null, string filterTech = null, string sortBy = "due")
|
string filterStatus = null, string filterTech = null, string sortBy = "due")
|
||||||
{
|
{
|
||||||
|
var tiles = new List<DashboardTile>();
|
||||||
|
string sheetError = null;
|
||||||
|
|
||||||
|
// --- Disco Jobs ---
|
||||||
var openJobs = _database.Jobs
|
var openJobs = _database.Jobs
|
||||||
.Include("Device").Include("Device.DeviceModel")
|
.Include("Device").Include("Device.DeviceModel")
|
||||||
.Include("User").Include("OpenedTechUser")
|
.Include("User").Include("OpenedTechUser")
|
||||||
.Include("JobType").Include("JobSubTypes")
|
.Include("JobType").Include("JobSubTypes")
|
||||||
.Where(j => j.ClosedDate == null)
|
.Where(j => j.ClosedDate == null).ToList();
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var allTickets = _dataStore.LoadAllTickets();
|
var allTickets = _dataStore.LoadAllTickets();
|
||||||
var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t);
|
var ticketLookup = allTickets.ToDictionary(t => t.JobId, t => t);
|
||||||
@@ -48,55 +53,100 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tiles = new List<DashboardTile>();
|
|
||||||
foreach (var job in openJobs)
|
foreach (var job in openJobs)
|
||||||
{
|
{
|
||||||
ServiceTicket ticket;
|
ServiceTicket ticket;
|
||||||
ticketLookup.TryGetValue(job.Id, out ticket);
|
ticketLookup.TryGetValue(job.Id, out ticket);
|
||||||
tiles.Add(BuildTile(job, ticket));
|
tiles.Add(BuildDiscoTile(job, ticket));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filterPriority))
|
// --- Google Sheet ---
|
||||||
tiles = tiles.Where(t => t.PriorityId == filterPriority).ToList();
|
int sheetCount = 0;
|
||||||
if (!string.IsNullOrEmpty(filterLocation))
|
if (_config.GoogleSheet != null && _config.GoogleSheet.Enabled)
|
||||||
tiles = tiles.Where(t => t.LocationId == filterLocation).ToList();
|
{
|
||||||
if (!string.IsNullOrEmpty(filterStatus))
|
try
|
||||||
tiles = tiles.Where(t => t.StatusOverride == filterStatus).ToList();
|
{
|
||||||
if (!string.IsNullOrEmpty(filterTech))
|
var sheetSvc = new GoogleSheetService(_config.GoogleSheet, _dataStore.DataDirectory);
|
||||||
tiles = tiles.Where(t => t.AssignedTechId == filterTech).ToList();
|
var sheetResult = sheetSvc.FetchTickets();
|
||||||
|
if (sheetResult.Error != null)
|
||||||
|
{
|
||||||
|
sheetError = sheetResult.Error;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (sheetResult.Warning != null) sheetError = sheetResult.Warning;
|
||||||
|
var extTickets = _dataStore.LoadExternalTickets();
|
||||||
|
var extLookup = extTickets.ToDictionary(t => t.JobId, t => t);
|
||||||
|
|
||||||
|
foreach (var ext in sheetResult.Tickets)
|
||||||
|
{
|
||||||
|
ServiceTicket st;
|
||||||
|
if (!extLookup.TryGetValue(ext.InternalId, out st))
|
||||||
|
{
|
||||||
|
st = new ServiceTicket
|
||||||
|
{
|
||||||
|
JobId = ext.InternalId, Source = "ntt",
|
||||||
|
PriorityId = MapSheetPriority(ext.RawPriority),
|
||||||
|
LocationId = _config.DefaultLocationId,
|
||||||
|
AssignedTechId = ResolveSheetTech(ext.AssignedTo),
|
||||||
|
Summary = ext.IssueDescription,
|
||||||
|
CreatedDate = ext.Timestamp, LastModifiedDate = DateTime.Now
|
||||||
|
};
|
||||||
|
_dataStore.SaveExternalTicket(st);
|
||||||
|
extLookup[ext.InternalId] = st;
|
||||||
|
}
|
||||||
|
tiles.Add(BuildSheetTile(ext, st));
|
||||||
|
sheetCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sheetError = "Sheet error: " + ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ready for Return ---
|
||||||
|
var readyForReturn = tiles.Where(t =>
|
||||||
|
{
|
||||||
|
var status = (t.StatusOverride ?? t.DiscoStatus ?? "").ToLower();
|
||||||
|
return status.Contains("ready for return") || status.Contains("ready for pickup");
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// Remove from main tiles
|
||||||
|
var mainTiles = tiles.Where(t => !readyForReturn.Contains(t)).ToList();
|
||||||
|
|
||||||
|
// --- Filter ---
|
||||||
|
if (!string.IsNullOrEmpty(filterPriority)) mainTiles = mainTiles.Where(t => t.PriorityId == filterPriority).ToList();
|
||||||
|
if (!string.IsNullOrEmpty(filterLocation)) mainTiles = mainTiles.Where(t => t.LocationId == filterLocation).ToList();
|
||||||
|
if (!string.IsNullOrEmpty(filterStatus)) mainTiles = mainTiles.Where(t => t.StatusOverride == filterStatus).ToList();
|
||||||
|
if (!string.IsNullOrEmpty(filterTech)) mainTiles = mainTiles.Where(t => t.AssignedTechId == filterTech).ToList();
|
||||||
|
|
||||||
|
// --- Sort ---
|
||||||
switch (sortBy)
|
switch (sortBy)
|
||||||
{
|
{
|
||||||
case "priority":
|
case "priority": mainTiles = mainTiles.OrderBy(t => t.PrioritySortOrder).ThenBy(t => t.SortDate).ToList(); break;
|
||||||
tiles = tiles.OrderBy(t => t.PrioritySortOrder).ThenBy(t => t.SortDate).ToList();
|
case "age": mainTiles = mainTiles.OrderByDescending(t => t.AgeDays).ToList(); break;
|
||||||
break;
|
case "modified": mainTiles = mainTiles.OrderByDescending(t => t.LastModifiedDate).ToList(); break;
|
||||||
case "age":
|
case "sla": mainTiles = mainTiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2)
|
||||||
tiles = tiles.OrderByDescending(t => t.AgeDays).ToList();
|
.ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList(); break;
|
||||||
break;
|
default: mainTiles = mainTiles.OrderBy(t => t.SortDate).ToList(); break;
|
||||||
case "modified":
|
|
||||||
tiles = tiles.OrderByDescending(t => t.LastModifiedDate).ToList();
|
|
||||||
break;
|
|
||||||
case "sla":
|
|
||||||
tiles = tiles.OrderBy(t => t.IsSlaBreached ? 0 : t.IsSlaWarning ? 1 : 2)
|
|
||||||
.ThenBy(t => t.SlaDeadline.HasValue ? t.SlaDeadline.Value : DateTime.MaxValue).ToList();
|
|
||||||
break;
|
|
||||||
case "due":
|
|
||||||
default:
|
|
||||||
tiles = tiles.OrderBy(t => t.SortDate).ToList();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stats = BuildStats(mainTiles);
|
||||||
|
stats.FromGoogleSheet = sheetCount;
|
||||||
|
|
||||||
return new DashboardViewModel
|
return new DashboardViewModel
|
||||||
{
|
{
|
||||||
Tiles = tiles,
|
Tiles = mainTiles, ReadyForReturn = readyForReturn,
|
||||||
Stats = BuildStats(tiles),
|
Stats = stats, Config = _config,
|
||||||
Config = _config,
|
|
||||||
CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "",
|
CurrentFilter = filterPriority ?? filterLocation ?? filterStatus ?? "",
|
||||||
SortBy = sortBy
|
SortBy = sortBy, GoogleSheetError = sheetError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardTile BuildTile(Job job, ServiceTicket ticket)
|
// --- Build Disco Tile ---
|
||||||
|
private DashboardTile BuildDiscoTile(Job job, ServiceTicket ticket)
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
var priorityId = (ticket != null ? ticket.PriorityId : null) ?? _config.DefaultPriorityId;
|
var priorityId = (ticket != null ? ticket.PriorityId : null) ?? _config.DefaultPriorityId;
|
||||||
@@ -112,47 +162,29 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
&& now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25);
|
&& now > slaDeadline.Value.AddHours(-priority.SlaHours * 0.25);
|
||||||
|
|
||||||
int ageDays = (int)(now - job.OpenedDate).TotalDays;
|
int ageDays = (int)(now - job.OpenedDate).TotalDays;
|
||||||
string ageBadge;
|
|
||||||
if (ageDays == 0) ageBadge = "Today";
|
|
||||||
else if (ageDays == 1) ageBadge = "1 day";
|
|
||||||
else if (ageDays < 7) ageBadge = ageDays + " days";
|
|
||||||
else if (ageDays < 30) ageBadge = (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : "");
|
|
||||||
else ageBadge = (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : "");
|
|
||||||
|
|
||||||
var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate;
|
var eta = (ticket != null ? ticket.EstimatedCompletion : null) ?? job.ExpectedClosedDate;
|
||||||
string etaDisplay = "\u2014";
|
|
||||||
if (eta.HasValue)
|
|
||||||
{
|
|
||||||
var etaDays = (int)(eta.Value - now).TotalDays;
|
|
||||||
if (etaDays < 0) etaDisplay = Math.Abs(etaDays) + "d overdue";
|
|
||||||
else if (etaDays == 0) etaDisplay = "Today";
|
|
||||||
else if (etaDays == 1) etaDisplay = "Tomorrow";
|
|
||||||
else etaDisplay = eta.Value.ToString("dd MMM");
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime sortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value
|
|
||||||
: eta.HasValue ? eta.Value
|
|
||||||
: job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate;
|
|
||||||
|
|
||||||
string discoStatus = "Open";
|
string discoStatus = "Open";
|
||||||
if (job.WaitingForUserAction.HasValue) discoStatus = "Awaiting User Action";
|
if (job.WaitingForUserAction.HasValue) discoStatus = "Awaiting User Action";
|
||||||
else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) discoStatus = "Device Held";
|
else if (job.DeviceHeld.HasValue && !job.DeviceReadyForReturn.HasValue) discoStatus = "Device Held";
|
||||||
else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) discoStatus = "Ready for Return";
|
else if (job.DeviceReadyForReturn.HasValue && !job.DeviceReturnedDate.HasValue) discoStatus = "Ready for Return";
|
||||||
|
|
||||||
string latestNote = null;
|
string latestNote = null; int noteCount = 0;
|
||||||
int noteCount = 0;
|
|
||||||
if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0)
|
if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0)
|
||||||
{
|
{
|
||||||
noteCount = ticket.Notes.Count;
|
noteCount = ticket.Notes.Count;
|
||||||
var latest = ticket.Notes.OrderByDescending(n => n.Timestamp).First();
|
var latest = ticket.Notes.OrderByDescending(n => n.Timestamp).First();
|
||||||
latestNote = latest.Content;
|
latestNote = latest.Content;
|
||||||
if (latestNote != null && latestNote.Length > 80)
|
if (latestNote != null && latestNote.Length > 80) latestNote = latestNote.Substring(0, 77) + "...";
|
||||||
latestNote = latestNote.Substring(0, 77) + "...";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve tech display name
|
||||||
|
var techId = ticket != null ? ticket.AssignedTechId : job.OpenedTechUserId;
|
||||||
|
var techName = ResolveTechName(techId);
|
||||||
|
|
||||||
return new DashboardTile
|
return new DashboardTile
|
||||||
{
|
{
|
||||||
JobId = job.Id,
|
JobId = job.Id, Source = "disco", DisplayId = "DIC#" + job.Id,
|
||||||
JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId,
|
JobTypeDescription = job.JobType != null ? job.JobType.Description : job.JobTypeId,
|
||||||
DeviceSerialNumber = job.DeviceSerialNumber ?? "\u2014",
|
DeviceSerialNumber = job.DeviceSerialNumber ?? "\u2014",
|
||||||
DeviceModelDescription = job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null,
|
DeviceModelDescription = job.Device != null && job.Device.DeviceModel != null ? job.Device.DeviceModel.Description : null,
|
||||||
@@ -161,169 +193,286 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId,
|
UserDisplayName = job.User != null ? job.User.DisplayName : job.UserId,
|
||||||
OpenedByTechId = job.OpenedTechUserId,
|
OpenedByTechId = job.OpenedTechUserId,
|
||||||
OpenedByTechName = job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId,
|
OpenedByTechName = job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId,
|
||||||
OpenedDate = job.OpenedDate,
|
OpenedDate = job.OpenedDate, ExpectedClosedDate = job.ExpectedClosedDate, DiscoStatus = discoStatus,
|
||||||
ExpectedClosedDate = job.ExpectedClosedDate,
|
PriorityId = priorityId, PriorityName = priority != null ? priority.Name : "Unknown",
|
||||||
DiscoStatus = discoStatus,
|
PriorityColor = priority != null ? priority.Color : "#999", PrioritySortOrder = priority != null ? priority.SortOrder : 99,
|
||||||
PriorityId = priorityId,
|
LocationId = locationId, LocationName = location != null ? location.Name : "Unknown",
|
||||||
PriorityName = priority != null ? priority.Name : "Unknown",
|
LocationIcon = location != null ? location.Icon : "", LocationColor = location != null ? location.Color : "#999",
|
||||||
PriorityColor = priority != null ? priority.Color : "#999",
|
AssignedTechId = techId, AssignedTechName = techName,
|
||||||
PrioritySortOrder = priority != null ? priority.SortOrder : 99,
|
EstimatedCompletion = eta, SlaDeadline = slaDeadline,
|
||||||
LocationId = locationId,
|
|
||||||
LocationName = location != null ? location.Name : "Unknown",
|
|
||||||
LocationIcon = location != null ? location.Icon : "",
|
|
||||||
LocationColor = location != null ? location.Color : "#999",
|
|
||||||
AssignedTechId = ticket != null ? ticket.AssignedTechId : null,
|
|
||||||
AssignedTechName = ResolveUserName(ticket != null ? ticket.AssignedTechId : null),
|
|
||||||
EstimatedCompletion = eta,
|
|
||||||
SlaDeadline = slaDeadline,
|
|
||||||
StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : discoStatus,
|
StatusOverride = ticket != null && ticket.StatusOverride != null ? ticket.StatusOverride : discoStatus,
|
||||||
Summary = ticket != null ? ticket.Summary : null,
|
Summary = ticket != null ? ticket.Summary : null,
|
||||||
NoteCount = noteCount,
|
NoteCount = noteCount, LatestNote = latestNote,
|
||||||
LatestNote = latestNote,
|
|
||||||
LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate,
|
LastModifiedDate = ticket != null ? ticket.LastModifiedDate : job.OpenedDate,
|
||||||
IsSlaBreached = slaBreached,
|
IsSlaBreached = slaBreached, IsSlaWarning = slaWarning,
|
||||||
IsSlaWarning = slaWarning,
|
AgeBadge = FormatAge(ageDays), AgeDays = ageDays, EtaDisplay = FormatEta(eta),
|
||||||
AgeBadge = ageBadge,
|
SortDate = slaBreached && slaDeadline.HasValue ? slaDeadline.Value : eta.HasValue ? eta.Value : job.ExpectedClosedDate.HasValue ? job.ExpectedClosedDate.Value : job.OpenedDate
|
||||||
AgeDays = ageDays,
|
|
||||||
EtaDisplay = etaDisplay,
|
|
||||||
SortDate = sortDate
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Build Sheet Tile ---
|
||||||
|
private DashboardTile BuildSheetTile(ExternalTicket ext, ServiceTicket st)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var priorityId = st != null ? st.PriorityId : MapSheetPriority(ext.RawPriority);
|
||||||
|
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId) ?? _config.Priorities.FirstOrDefault();
|
||||||
|
var locationId = st != null ? st.LocationId : _config.DefaultLocationId;
|
||||||
|
var location = _config.Locations.FirstOrDefault(l => l.Id == locationId) ?? _config.Locations.FirstOrDefault();
|
||||||
|
int ageDays = (int)(now - ext.Timestamp).TotalDays;
|
||||||
|
var techId = st != null ? st.AssignedTechId : ResolveSheetTech(ext.AssignedTo);
|
||||||
|
|
||||||
|
string latestNote = null; int noteCount = 0;
|
||||||
|
if (st != null && st.Notes != null && st.Notes.Count > 0)
|
||||||
|
{
|
||||||
|
noteCount = st.Notes.Count;
|
||||||
|
latestNote = st.Notes.OrderByDescending(n => n.Timestamp).First().Content;
|
||||||
|
if (latestNote != null && latestNote.Length > 80) latestNote = latestNote.Substring(0, 77) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract requester name from email
|
||||||
|
var requesterName = ext.RequesterEmail ?? "Unknown";
|
||||||
|
if (requesterName.Contains("@"))
|
||||||
|
requesterName = requesterName.Split('@')[0].Replace(".", " ");
|
||||||
|
|
||||||
|
return new DashboardTile
|
||||||
|
{
|
||||||
|
JobId = ext.InternalId, Source = "ntt", DisplayId = "NTT#" + ext.ExternalId.Replace("NTT", ""),
|
||||||
|
JobTypeDescription = "Google Sheet",
|
||||||
|
DeviceSerialNumber = ext.DeviceName ?? "\u2014",
|
||||||
|
DeviceComputerName = ext.DeviceName,
|
||||||
|
UserId = ext.RequesterEmail, UserDisplayName = requesterName,
|
||||||
|
OpenedDate = ext.Timestamp, DiscoStatus = ext.RawStatus ?? "Open",
|
||||||
|
PriorityId = priorityId, PriorityName = priority != null ? priority.Name : "Unknown",
|
||||||
|
PriorityColor = priority != null ? priority.Color : "#999", PrioritySortOrder = priority != null ? priority.SortOrder : 99,
|
||||||
|
LocationId = locationId, LocationName = location != null ? location.Name : "Unknown",
|
||||||
|
LocationIcon = location != null ? location.Icon : "", LocationColor = location != null ? location.Color : "#999",
|
||||||
|
AssignedTechId = techId, AssignedTechName = ResolveTechName(techId),
|
||||||
|
StatusOverride = st != null && st.StatusOverride != null ? st.StatusOverride : (ext.RawStatus ?? "Open"),
|
||||||
|
Summary = ext.IssueDescription, NoteCount = noteCount, LatestNote = latestNote,
|
||||||
|
LastModifiedDate = st != null ? st.LastModifiedDate : ext.Timestamp,
|
||||||
|
AgeBadge = FormatAge(ageDays), AgeDays = ageDays, EtaDisplay = FormatEta(st != null ? st.EstimatedCompletion : null),
|
||||||
|
SortDate = st != null && st.EstimatedCompletion.HasValue ? st.EstimatedCompletion.Value : ext.Timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tech Resolution ---
|
||||||
|
public string ResolveTechName(string techIdOrEmail)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(techIdOrEmail)) return null;
|
||||||
|
foreach (var tech in _config.Technicians)
|
||||||
|
{
|
||||||
|
if (tech.Id == techIdOrEmail || tech.DisplayName == techIdOrEmail) return tech.DisplayName;
|
||||||
|
if (!string.IsNullOrEmpty(tech.Email) && tech.Email.Equals(techIdOrEmail, StringComparison.OrdinalIgnoreCase)) return tech.DisplayName;
|
||||||
|
if (tech.DiscoUserIds != null)
|
||||||
|
{
|
||||||
|
foreach (var did in tech.DiscoUserIds)
|
||||||
|
{
|
||||||
|
if (did.Equals(techIdOrEmail, StringComparison.OrdinalIgnoreCase)) return tech.DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to Disco user lookup
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = _database.Users.FirstOrDefault(u => u.UserId == techIdOrEmail);
|
||||||
|
if (user != null) return user.DisplayName;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return techIdOrEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveSheetTech(string assignedTo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(assignedTo)) return null;
|
||||||
|
foreach (var tech in _config.Technicians)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tech.Email) && tech.Email.Equals(assignedTo, StringComparison.OrdinalIgnoreCase)) return tech.Id;
|
||||||
|
if (tech.DisplayName.Equals(assignedTo, StringComparison.OrdinalIgnoreCase)) return tech.Id;
|
||||||
|
}
|
||||||
|
return assignedTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapSheetPriority(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(raw)) return _config.DefaultPriorityId;
|
||||||
|
raw = raw.ToLower().Trim();
|
||||||
|
foreach (var p in _config.Priorities)
|
||||||
|
{
|
||||||
|
if (p.Name.ToLower() == raw || p.Id == raw) return p.Id;
|
||||||
|
}
|
||||||
|
if (raw.Contains("critical") || raw.Contains("urgent")) return "critical";
|
||||||
|
if (raw.Contains("high")) return "high";
|
||||||
|
if (raw.Contains("low")) return "low";
|
||||||
|
return _config.DefaultPriorityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CRUD ---
|
||||||
|
public void UpdateTicket(int jobId, string source, string priorityId, string locationId,
|
||||||
|
string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy)
|
||||||
|
{
|
||||||
|
ServiceTicket ticket;
|
||||||
|
if (source == "ntt")
|
||||||
|
ticket = _dataStore.GetExternalTicket(jobId);
|
||||||
|
else
|
||||||
|
ticket = _dataStore.GetTicket(jobId);
|
||||||
|
|
||||||
|
if (ticket == null) ticket = new ServiceTicket { JobId = jobId, Source = source ?? "disco" };
|
||||||
|
if (ticket.ChangeLog == null) ticket.ChangeLog = new List<ChangeEntry>();
|
||||||
|
|
||||||
|
Action<string, string, string> trackChange = (field, oldVal, newVal) =>
|
||||||
|
{
|
||||||
|
if (oldVal != newVal) ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = field, OldValue = oldVal, NewValue = newVal });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (priorityId != null && priorityId != ticket.PriorityId) { trackChange("Priority", ticket.PriorityId, priorityId); ticket.PriorityId = priorityId; }
|
||||||
|
if (locationId != null && locationId != ticket.LocationId) { trackChange("Location", ticket.LocationId, locationId); ticket.LocationId = locationId; }
|
||||||
|
if (assignedTechId != null && assignedTechId != ticket.AssignedTechId) { trackChange("Assigned Tech", ticket.AssignedTechId, assignedTechId); ticket.AssignedTechId = assignedTechId; }
|
||||||
|
if (status != null && status != ticket.StatusOverride) { trackChange("Status", ticket.StatusOverride ?? "(default)", string.IsNullOrEmpty(status) ? "(default)" : status); ticket.StatusOverride = status; }
|
||||||
|
if (summary != null && summary != ticket.Summary) { trackChange("Summary", null, "(updated)"); ticket.Summary = summary; }
|
||||||
|
if (eta.HasValue)
|
||||||
|
{
|
||||||
|
var oldEta = ticket.EstimatedCompletion.HasValue ? ticket.EstimatedCompletion.Value.ToString("dd MMM yyyy") : "none";
|
||||||
|
var newEta = eta.Value.ToString("dd MMM yyyy");
|
||||||
|
if (oldEta != newEta) { trackChange("ETA", oldEta, newEta); ticket.EstimatedCompletion = eta; }
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket.LastModifiedBy = modifiedBy;
|
||||||
|
if (priorityId != null)
|
||||||
|
{
|
||||||
|
var p = _config.Priorities.FirstOrDefault(x => x.Id == priorityId);
|
||||||
|
ticket.SlaDeadline = (p != null && p.SlaHours > 0) ? ticket.CreatedDate.AddHours(p.SlaHours) : (DateTime?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source == "ntt") _dataStore.SaveExternalTicket(ticket);
|
||||||
|
else _dataStore.SaveTicket(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddNote(int jobId, string source, string authorId, string authorName, string content, string noteType)
|
||||||
|
{
|
||||||
|
var note = new TicketNote { AuthorId = authorId, AuthorName = authorName, Content = content, NoteType = noteType ?? "general" };
|
||||||
|
|
||||||
|
if (source == "ntt")
|
||||||
|
_dataStore.AddExternalNote(jobId, note);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dataStore.AddNote(jobId, note);
|
||||||
|
// Write back to Disco
|
||||||
|
WriteNoteToDisco(jobId, authorId, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteNoteToDisco(int jobId, string techUserId, string content)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var job = _database.Jobs.FirstOrDefault(j => j.Id == jobId);
|
||||||
|
if (job == null) return;
|
||||||
|
var log = new JobLog
|
||||||
|
{
|
||||||
|
JobId = jobId,
|
||||||
|
TechUserId = techUserId,
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
Comments = "[Service Tracker] " + content
|
||||||
|
};
|
||||||
|
_database.JobLogs.Add(log);
|
||||||
|
_database.SaveChanges();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkCollected(int jobId, string source, string userId)
|
||||||
|
{
|
||||||
|
UpdateTicket(jobId, source, null, null, null, null, "Resolved", null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceTicket GetTicketDetail(int jobId, string source)
|
||||||
|
{
|
||||||
|
if (source == "ntt") return _dataStore.GetExternalTicket(jobId);
|
||||||
|
return _dataStore.GetTicket(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
private string FormatAge(int ageDays)
|
||||||
|
{
|
||||||
|
if (ageDays == 0) return "Today";
|
||||||
|
if (ageDays == 1) return "1 day";
|
||||||
|
if (ageDays < 7) return ageDays + " days";
|
||||||
|
if (ageDays < 30) return (ageDays / 7) + " wk" + (ageDays / 7 > 1 ? "s" : "");
|
||||||
|
return (ageDays / 30) + " mo" + (ageDays / 30 > 1 ? "s" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatEta(DateTime? eta)
|
||||||
|
{
|
||||||
|
if (!eta.HasValue) return "\u2014";
|
||||||
|
var days = (int)(eta.Value - DateTime.Now).TotalDays;
|
||||||
|
if (days < 0) return Math.Abs(days) + "d overdue";
|
||||||
|
if (days == 0) return "Today";
|
||||||
|
if (days == 1) return "Tomorrow";
|
||||||
|
return eta.Value.ToString("dd MMM");
|
||||||
|
}
|
||||||
|
|
||||||
private DashboardStats BuildStats(List<DashboardTile> tiles)
|
private DashboardStats BuildStats(List<DashboardTile> tiles)
|
||||||
{
|
{
|
||||||
var stats = new DashboardStats
|
var s = new DashboardStats
|
||||||
{
|
{
|
||||||
TotalOpen = tiles.Count,
|
TotalOpen = tiles.Count, SlaBreached = tiles.Count(t => t.IsSlaBreached),
|
||||||
SlaBreached = tiles.Count(t => t.IsSlaBreached),
|
|
||||||
SlaWarning = tiles.Count(t => t.IsSlaWarning),
|
SlaWarning = tiles.Count(t => t.IsSlaWarning),
|
||||||
AvgAgeDays = tiles.Count > 0 ? Math.Round(tiles.Average(t => (double)t.AgeDays), 1) : 0,
|
AvgAgeDays = tiles.Count > 0 ? Math.Round(tiles.Average(t => (double)t.AgeDays), 1) : 0,
|
||||||
OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0
|
OldestJobDays = tiles.Count > 0 ? tiles.Max(t => t.AgeDays) : 0
|
||||||
};
|
};
|
||||||
foreach (var p in _config.Priorities) stats.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id);
|
foreach (var p in _config.Priorities) s.ByPriority[p.Id] = tiles.Count(t => t.PriorityId == p.Id);
|
||||||
stats.Critical = tiles.Count(t => t.PriorityId == "critical");
|
s.Critical = tiles.Count(t => t.PriorityId == "critical"); s.High = tiles.Count(t => t.PriorityId == "high");
|
||||||
stats.High = tiles.Count(t => t.PriorityId == "high");
|
s.Medium = tiles.Count(t => t.PriorityId == "medium"); s.Low = tiles.Count(t => t.PriorityId == "low");
|
||||||
stats.Medium = tiles.Count(t => t.PriorityId == "medium");
|
s.Scheduled = tiles.Count(t => t.PriorityId == "scheduled");
|
||||||
stats.Low = tiles.Count(t => t.PriorityId == "low");
|
foreach (var l in _config.Locations) s.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id);
|
||||||
stats.Scheduled = tiles.Count(t => t.PriorityId == "scheduled");
|
s.InItOffice = tiles.Count(t => t.LocationId == "it-office"); s.WithUser = tiles.Count(t => t.LocationId == "with-user");
|
||||||
foreach (var l in _config.Locations) stats.ByLocation[l.Id] = tiles.Count(t => t.LocationId == l.Id);
|
s.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer");
|
||||||
stats.InItOffice = tiles.Count(t => t.LocationId == "it-office");
|
|
||||||
stats.WithUser = tiles.Count(t => t.LocationId == "with-user");
|
|
||||||
stats.AtRepairer = tiles.Count(t => t.LocationId == "at-repairer");
|
|
||||||
foreach (var tile in tiles)
|
foreach (var tile in tiles)
|
||||||
{
|
{
|
||||||
var status = tile.StatusOverride ?? "Open";
|
var st = tile.StatusOverride ?? "Open";
|
||||||
if (!stats.ByStatus.ContainsKey(status)) stats.ByStatus[status] = 0;
|
if (!s.ByStatus.ContainsKey(st)) s.ByStatus[st] = 0; s.ByStatus[st]++;
|
||||||
stats.ByStatus[status]++;
|
|
||||||
}
|
}
|
||||||
foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId)))
|
foreach (var tile in tiles.Where(t => !string.IsNullOrEmpty(t.AssignedTechId)))
|
||||||
{
|
{
|
||||||
var tech = tile.AssignedTechName ?? tile.AssignedTechId;
|
var tn = tile.AssignedTechName ?? tile.AssignedTechId;
|
||||||
if (!stats.ByTech.ContainsKey(tech)) stats.ByTech[tech] = 0;
|
if (!s.ByTech.ContainsKey(tn)) s.ByTech[tn] = 0; s.ByTech[tn]++;
|
||||||
stats.ByTech[tech]++;
|
|
||||||
}
|
}
|
||||||
return stats;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceTicket CreateDefaultTicket(Job job)
|
private ServiceTicket CreateDefaultTicket(Job job)
|
||||||
{
|
{
|
||||||
var priority = _config.Priorities.FirstOrDefault(p => p.Id == _config.DefaultPriorityId);
|
var priority = _config.Priorities.FirstOrDefault(p => p.Id == _config.DefaultPriorityId);
|
||||||
DateTime? sla = null;
|
DateTime? sla = (priority != null && priority.SlaHours > 0) ? job.OpenedDate.AddHours(priority.SlaHours) : (DateTime?)null;
|
||||||
if (priority != null && priority.SlaHours > 0)
|
string locId = _config.DefaultLocationId;
|
||||||
sla = job.OpenedDate.AddHours(priority.SlaHours);
|
|
||||||
string locationId = _config.DefaultLocationId;
|
|
||||||
if (job.DeviceHeld.HasValue && !string.IsNullOrEmpty(job.DeviceHeldLocation))
|
if (job.DeviceHeld.HasValue && !string.IsNullOrEmpty(job.DeviceHeldLocation))
|
||||||
{
|
{
|
||||||
var matchedLoc = _config.Locations.FirstOrDefault(
|
var ml = _config.Locations.FirstOrDefault(l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase));
|
||||||
l => l.Name.Equals(job.DeviceHeldLocation, StringComparison.OrdinalIgnoreCase));
|
if (ml != null) locId = ml.Id;
|
||||||
if (matchedLoc != null) locationId = matchedLoc.Id;
|
|
||||||
}
|
}
|
||||||
|
var techId = ResolveDiscoTechToId(job.OpenedTechUserId);
|
||||||
return new ServiceTicket
|
return new ServiceTicket
|
||||||
{
|
{
|
||||||
JobId = job.Id, PriorityId = _config.DefaultPriorityId, LocationId = locationId,
|
JobId = job.Id, Source = "disco", PriorityId = _config.DefaultPriorityId, LocationId = locId,
|
||||||
AssignedTechId = job.OpenedTechUserId, EstimatedCompletion = job.ExpectedClosedDate,
|
AssignedTechId = techId, EstimatedCompletion = job.ExpectedClosedDate,
|
||||||
SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now
|
SlaDeadline = sla, CreatedDate = DateTime.Now, LastModifiedDate = DateTime.Now
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveUserName(string userId)
|
private string ResolveDiscoTechToId(string discoUserId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(userId)) return null;
|
if (string.IsNullOrEmpty(discoUserId)) return null;
|
||||||
try
|
foreach (var tech in _config.Technicians)
|
||||||
{
|
{
|
||||||
var user = _database.Users.FirstOrDefault(u => u.UserId == userId);
|
if (tech.DiscoUserIds != null)
|
||||||
return user != null ? user.DisplayName : userId;
|
|
||||||
}
|
|
||||||
catch { return userId; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateTicket(int jobId, string priorityId, string locationId,
|
|
||||||
string assignedTechId, DateTime? eta, string status, string summary, string modifiedBy)
|
|
||||||
{
|
{
|
||||||
var ticket = _dataStore.GetTicket(jobId);
|
foreach (var did in tech.DiscoUserIds)
|
||||||
if (ticket == null) ticket = new ServiceTicket { JobId = jobId };
|
|
||||||
if (ticket.ChangeLog == null) ticket.ChangeLog = new List<ChangeEntry>();
|
|
||||||
|
|
||||||
// Track changes
|
|
||||||
if (priorityId != null && priorityId != ticket.PriorityId)
|
|
||||||
{
|
{
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Priority", OldValue = ticket.PriorityId, NewValue = priorityId });
|
if (did.Equals(discoUserId, StringComparison.OrdinalIgnoreCase)) return tech.Id;
|
||||||
ticket.PriorityId = priorityId;
|
|
||||||
}
|
|
||||||
if (locationId != null && locationId != ticket.LocationId)
|
|
||||||
{
|
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Location", OldValue = ticket.LocationId, NewValue = locationId });
|
|
||||||
ticket.LocationId = locationId;
|
|
||||||
}
|
|
||||||
if (assignedTechId != null && assignedTechId != ticket.AssignedTechId)
|
|
||||||
{
|
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Assigned Tech", OldValue = ticket.AssignedTechId, NewValue = assignedTechId });
|
|
||||||
ticket.AssignedTechId = assignedTechId;
|
|
||||||
}
|
|
||||||
if (eta.HasValue)
|
|
||||||
{
|
|
||||||
var oldEta = ticket.EstimatedCompletion.HasValue ? ticket.EstimatedCompletion.Value.ToString("dd MMM yyyy") : "none";
|
|
||||||
var newEta = eta.Value.ToString("dd MMM yyyy");
|
|
||||||
if (oldEta != newEta)
|
|
||||||
{
|
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "ETA", OldValue = oldEta, NewValue = newEta });
|
|
||||||
ticket.EstimatedCompletion = eta;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status != null && status != ticket.StatusOverride)
|
|
||||||
{
|
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Status", OldValue = ticket.StatusOverride ?? "(Disco default)", NewValue = string.IsNullOrEmpty(status) ? "(Disco default)" : status });
|
|
||||||
ticket.StatusOverride = status;
|
|
||||||
}
|
}
|
||||||
if (summary != null && summary != ticket.Summary)
|
return discoUserId;
|
||||||
{
|
|
||||||
ticket.ChangeLog.Add(new ChangeEntry { UserId = modifiedBy, Field = "Summary", OldValue = null, NewValue = "(updated)" });
|
|
||||||
ticket.Summary = summary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket.LastModifiedBy = modifiedBy;
|
|
||||||
|
|
||||||
// Recalculate SLA if priority changed
|
|
||||||
if (priorityId != null)
|
|
||||||
{
|
|
||||||
var priority = _config.Priorities.FirstOrDefault(p => p.Id == priorityId);
|
|
||||||
if (priority != null && priority.SlaHours > 0)
|
|
||||||
ticket.SlaDeadline = ticket.CreatedDate.AddHours(priority.SlaHours);
|
|
||||||
else
|
|
||||||
ticket.SlaDeadline = null;
|
|
||||||
}
|
|
||||||
_dataStore.SaveTicket(ticket);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddNote(int jobId, string authorId, string authorName, string content, string noteType)
|
|
||||||
{
|
|
||||||
_dataStore.AddNote(jobId, new TicketNote
|
|
||||||
{
|
|
||||||
AuthorId = authorId, AuthorName = authorName,
|
|
||||||
Content = content, NoteType = noteType ?? "general"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceTicket GetTicketDetail(int jobId) { return _dataStore.GetTicket(jobId); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user