Files
C4IT-F4SD-Client/FasdDesktopUi/Basics/Services/TicketOverviewUpdateService.cs
2025-11-11 11:03:42 +01:00

600 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using C4IT.FASD.Base;
using C4IT.FASD.Cockpit.Communication;
using FasdDesktopUi.Basics.Models;
using FasdDesktopUi.Basics.Services.Models;
#if isDemo
using System.Net;
using FasdCockpitCommunicationDemo;
using System.Text.RegularExpressions;
#endif
namespace FasdDesktopUi.Basics.Services
{
public sealed class TicketOverviewUpdateService
{
private static readonly TimeSpan RefreshInterval = TimeSpan.FromMinutes(5);
private static readonly string[] OverviewKeys = new[]
{
"TicketsNew",
"TicketsActive",
"TicketsCritical",
"TicketsNewInfo",
"IncidentNew",
"IncidentActive",
"IncidentCritical",
"IncidentNewInfo",
"UnassignedTickets",
"UnassignedTicketsCritical"
};
private const string DemoTicketDetailsKey = "Demo.HasTicketDetails";
private readonly Dispatcher _dispatcher;
private readonly Dictionary<string, TileCounts> _currentCounts = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<(string Key, bool UseRoleScope), List<cF4sdApiSearchResultRelation>> _demoRelations = new Dictionary<(string, bool), List<cF4sdApiSearchResultRelation>>();
private DispatcherTimer _timer;
private bool _isFetching;
private bool _fetchRetryPending;
private bool _isDemo;
private bool _initialized;
private readonly Random _random = new Random();
#if isDemo
private readonly List<DemoTicketRecord> _persistedDemoTickets = new List<DemoTicketRecord>();
private readonly List<DemoTicketTemplate> _demoTemplates = new List<DemoTicketTemplate>();
private readonly HashSet<string> _usedSummaries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private const int SimulationHotkeyDelayMs = 400;
private int _pendingSimulations;
private DispatcherTimer _simulationFlushTimer;
#endif
private TicketOverviewUpdateService()
{
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
foreach (var key in OverviewKeys)
{
_currentCounts[key] = TileCounts.Empty;
}
#if isDemo
_simulationFlushTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), DispatcherPriority.Background, SimulationFlushTimer_Tick, _dispatcher)
{
IsEnabled = false
};
#endif
}
static TicketOverviewUpdateService()
{
#if isDemo
Instance = new TicketOverviewUpdateService();
#endif
}
public static TicketOverviewUpdateService Instance { get; } = null;
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
public IReadOnlyDictionary<string, TileCounts> CurrentCounts => _currentCounts;
public void Start()
{
if (_initialized)
return;
_initialized = true;
#if isDemo
_isDemo = true;
LoadPersistedDemoTickets();
#else
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
#endif
if (!_isDemo)
{
_timer = new DispatcherTimer(RefreshInterval, DispatcherPriority.Background, async (s, e) => await FetchAsync().ConfigureAwait(false), _dispatcher);
_timer.Start();
_ = FetchAsync();
}
else
{
_ = FetchAsync();
}
}
public async Task FetchAsync()
{
if (_isFetching)
return;
var communication = cFasdCockpitCommunicationBase.Instance;
if (communication == null)
{
ScheduleFetchRetry();
return;
}
_isFetching = true;
try
{
_isDemo = communication?.IsDemo() == true;
if (_isDemo && _timer != null)
{
_timer.Stop();
_timer = null;
}
var counts = await Task.Run(() => RetrieveCountsAsync()).ConfigureAwait(false);
if (counts != null)
{
await _dispatcher.InvokeAsync(() => ProcessCounts(counts));
}
}
finally
{
_isFetching = false;
}
}
private Dictionary<string, TileCounts> RetrieveCountsAsync()
{
var communication = cFasdCockpitCommunicationBase.Instance;
if (communication == null)
return null;
var result = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase);
foreach (var key in OverviewKeys)
{
var personalTask = communication.GetTicketOverviewRelations(key, useRoleScope: false, count: 0);
var roleTask = communication.GetTicketOverviewRelations(key, useRoleScope: true, count: 0);
Task.WaitAll(personalTask, roleTask);
int personalCount = personalTask.Result?.Count ?? 0;
int roleCount = roleTask.Result?.Count ?? 0;
if (_isDemo)
{
personalCount += GetDemoRelationCount(key, useRoleScope: false);
roleCount += GetDemoRelationCount(key, useRoleScope: true);
}
result[key] = new TileCounts(personalCount, roleCount);
}
return result;
}
private void ProcessCounts(Dictionary<string, TileCounts> newCounts)
{
var changes = new List<TileCountChange>();
bool hasPrevious = _currentCounts.Values.Any(tc => tc.Personal > 0 || tc.Role > 0);
foreach (var key in OverviewKeys)
{
var previous = _currentCounts[key];
var current = newCounts.TryGetValue(key, out var value) ? value : TileCounts.Empty;
if (previous.Personal != current.Personal)
{
changes.Add(new TileCountChange(key, TileScope.Personal, previous.Personal, current.Personal));
}
if (previous.Role != current.Role)
{
changes.Add(new TileCountChange(key, TileScope.Role, previous.Role, current.Role));
}
_currentCounts[key] = current;
}
if (!hasPrevious)
return;
if (changes.Count == 0)
return;
var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary<string, TileCounts>(_currentCounts));
OverviewCountsChanged?.Invoke(this, args);
}
public void SimulateDemoTicket()
{
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
if (!_isDemo)
return;
#if isDemo
if (_demoTemplates.Count == 0)
{
LoadDemoTemplates();
if (_demoTemplates.Count == 0)
return;
}
_pendingSimulations++;
if (_simulationFlushTimer != null)
{
_simulationFlushTimer.Stop();
_simulationFlushTimer.Interval = TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs);
_simulationFlushTimer.Start();
}
else
{
ProcessDemoSimulations(_pendingSimulations);
_pendingSimulations = 0;
}
#endif
}
public IEnumerable<cF4sdApiSearchResultRelation> GetDemoRelations(string key, bool useRoleScope)
{
if (!_isDemo)
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
lock (_demoRelations)
{
if (_demoRelations.TryGetValue((key, useRoleScope), out var list))
return list.ToList();
}
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
}
#if isDemo
private void LoadPersistedDemoTickets()
{
var data = TicketOverviewDataStore.LoadData();
_demoTemplates.Clear();
if (data.Templates != null)
_demoTemplates.AddRange(data.Templates);
_persistedDemoTickets.Clear();
_usedSummaries.Clear();
if (data.Tickets == null)
return;
foreach (var record in data.Tickets)
{
if (!string.IsNullOrWhiteSpace(record.Summary))
_usedSummaries.Add(record.Summary);
_persistedDemoTickets.Add(record);
AddRelationForRecord(record);
}
}
private void LoadDemoTemplates()
{
var templates = TicketOverviewDataStore.LoadTemplates();
if (templates == null || templates.Count == 0)
return;
_demoTemplates.Clear();
_demoTemplates.AddRange(templates);
}
private void SimulationFlushTimer_Tick(object sender, EventArgs e)
{
_simulationFlushTimer.Stop();
var count = _pendingSimulations;
_pendingSimulations = 0;
ProcessDemoSimulations(count);
}
private void ProcessDemoSimulations(int count)
{
if (count <= 0)
return;
if (_demoTemplates.Count == 0)
{
LoadDemoTemplates();
if (_demoTemplates.Count == 0)
return;
}
var appliedChanges = new List<TileCountChange>();
for (int i = 0; i < count; i++)
{
var template = _demoTemplates[_random.Next(_demoTemplates.Count)];
var record = CreateDemoTicketRecord(template);
if (record == null)
continue;
if (!TicketOverviewDataStore.AppendTicket(record))
continue;
var change = RegisterDemoTicket(record);
if (change.HasValue)
appliedChanges.Add(change.Value);
}
if (appliedChanges.Count == 0)
return;
var args = new TicketOverviewCountsChangedEventArgs(appliedChanges, new Dictionary<string, TileCounts>(_currentCounts));
OverviewCountsChanged?.Invoke(this, args);
}
private void AddRelationForRecord(DemoTicketRecord record)
{
if (record == null)
return;
var relation = CreateRelationFromRecord(record);
var scopeKey = (record.TileKey, record.UseRoleScope);
lock (_demoRelations)
{
if (!_demoRelations.TryGetValue(scopeKey, out var list))
{
list = new List<cF4sdApiSearchResultRelation>();
_demoRelations[scopeKey] = list;
}
if (list.Any(existing => existing.id == relation.id))
return;
list.Add(relation);
}
}
private cF4sdApiSearchResultRelation CreateRelationFromRecord(DemoTicketRecord record)
{
var relation = new cF4sdApiSearchResultRelation
{
Type = enumF4sdSearchResultClass.Ticket,
DisplayName = record.DisplayName,
Name = record.DisplayName,
id = record.TicketId,
Status = enumF4sdSearchResultStatus.Active,
Infos = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Summary"] = record.Summary ?? string.Empty,
["StatusId"] = record.StatusId ?? string.Empty,
["UserDisplayName"] = record.UserDisplayName ?? string.Empty,
["UserAccount"] = record.UserAccount ?? string.Empty,
["UserDomain"] = record.UserDomain ?? string.Empty,
[DemoTicketDetailsKey] = bool.TrueString
},
Identities = new cF4sdIdentityList
{
new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = record.TicketId },
new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = record.UserId }
}
};
return relation;
}
private DemoTicketDetail CloneDetail(DemoTicketDetail source)
{
if (source == null)
return new DemoTicketDetail();
return new DemoTicketDetail
{
AffectedUser = source.AffectedUser,
Asset = source.Asset,
Category = source.Category,
Classification = source.Classification,
Description = source.Description,
DescriptionHtml = source.DescriptionHtml,
Priority = source.Priority,
Solution = source.Solution,
SolutionHtml = source.SolutionHtml,
Journal = source.Journal?.Select(entry => new DemoTicketJournalEntry
{
Header = entry?.Header,
Description = entry?.Description,
DescriptionHtml = entry?.DescriptionHtml,
IsVisibleForUser = entry?.IsVisibleForUser ?? true,
CreationDate = entry?.CreationDate ?? default
}).ToList() ?? new List<DemoTicketJournalEntry>()
};
}
private TileCountChange? RegisterDemoTicket(DemoTicketRecord record)
{
if (record == null)
return null;
_persistedDemoTickets.Add(record);
AddRelationForRecord(record);
if (!string.IsNullOrWhiteSpace(record.Summary))
_usedSummaries.Add(record.Summary);
if (cFasdCockpitCommunicationBase.Instance is cFasdCockpitCommunicationDemo demoCommunication)
{
demoCommunication.RegisterGeneratedTicket(record);
}
if (!_currentCounts.TryGetValue(record.TileKey, out var previousCounts))
previousCounts = TileCounts.Empty;
TileCounts updatedCounts;
int oldValue;
int newValue;
TileScope scope;
if (record.UseRoleScope)
{
updatedCounts = new TileCounts(previousCounts.Personal, previousCounts.Role + 1);
oldValue = previousCounts.Role;
newValue = updatedCounts.Role;
scope = TileScope.Role;
}
else
{
updatedCounts = new TileCounts(previousCounts.Personal + 1, previousCounts.Role);
oldValue = previousCounts.Personal;
newValue = updatedCounts.Personal;
scope = TileScope.Personal;
}
_currentCounts[record.TileKey] = updatedCounts;
return new TileCountChange(record.TileKey, scope, oldValue, newValue);
}
private string EnsureUniqueSummary(string preferredSummary)
{
if (string.IsNullOrWhiteSpace(preferredSummary))
preferredSummary = "Demo Ticket";
if (!_usedSummaries.Contains(preferredSummary))
{
_usedSummaries.Add(preferredSummary);
return preferredSummary;
}
var nextFreeSummary = _demoTemplates
.Select(t => t?.Summary)
.Where(s => !string.IsNullOrWhiteSpace(s))
.FirstOrDefault(s => !_usedSummaries.Contains(s));
if (!string.IsNullOrWhiteSpace(nextFreeSummary))
{
_usedSummaries.Add(nextFreeSummary);
return nextFreeSummary;
}
var baseSummary = preferredSummary;
var suffix = 2;
var candidate = baseSummary;
while (_usedSummaries.Contains(candidate))
{
candidate = $"{baseSummary} #{suffix}";
suffix++;
}
_usedSummaries.Add(candidate);
return candidate;
}
private DemoTicketRecord CreateDemoTicketRecord(DemoTicketTemplate template)
{
if (template == null)
return null;
var relationId = Guid.NewGuid();
var createdAt = DateTime.UtcNow;
var prefix = string.IsNullOrWhiteSpace(template.DisplayNamePrefix) ? "TCK" : template.DisplayNamePrefix.Trim();
prefix = prefix.ToUpperInvariant();
var displayName = TicketOverviewDataStore.GetNextDisplayName(prefix);
var summary = EnsureUniqueSummary(template.Summary ?? string.Empty);
var detail = CloneDetail(template.Detail);
if (string.IsNullOrWhiteSpace(detail.AffectedUser))
detail.AffectedUser = template.UserDisplayName ?? "Ticket, Timo";
if (string.IsNullOrWhiteSpace(detail.Description) && !string.IsNullOrWhiteSpace(detail.DescriptionHtml))
{
detail.Description = Regex.Replace(detail.DescriptionHtml, "<.*?>", string.Empty);
}
if (string.IsNullOrWhiteSpace(detail.Description))
detail.Description = summary;
if (string.IsNullOrWhiteSpace(detail.DescriptionHtml))
{
detail.DescriptionHtml = $"<p>{WebUtility.HtmlEncode(detail.Description)}</p>";
}
if (string.IsNullOrWhiteSpace(detail.Solution) && !string.IsNullOrWhiteSpace(detail.SolutionHtml))
{
detail.Solution = Regex.Replace(detail.SolutionHtml, "<.*?>", string.Empty);
}
if (string.IsNullOrWhiteSpace(detail.Solution))
detail.Solution = string.Empty;
if (string.IsNullOrWhiteSpace(detail.SolutionHtml) && !string.IsNullOrWhiteSpace(detail.Solution))
{
detail.SolutionHtml = $"<p>{WebUtility.HtmlEncode(detail.Solution)}</p>";
}
if (detail.Journal == null || detail.Journal.Count == 0)
{
detail.Journal = new List<DemoTicketJournalEntry>
{
new DemoTicketJournalEntry
{
Header = "Ticket erstellt",
Description = detail.Description ?? "Automatisch generiertes Demoticket.",
DescriptionHtml = detail.DescriptionHtml ?? "<p>Automatisch generiertes Demoticket.</p>",
IsVisibleForUser = true,
CreationDate = createdAt
}
};
}
foreach (var entry in detail.Journal)
{
if (entry.CreationDate == default)
entry.CreationDate = createdAt;
}
return new DemoTicketRecord
{
TicketId = relationId,
TileKey = string.IsNullOrWhiteSpace(template.TileKey) ? "TicketsNew" : template.TileKey,
UseRoleScope = template.UseRoleScope,
DisplayName = displayName,
Summary = summary,
StatusId = string.IsNullOrWhiteSpace(template.StatusId) ? "New" : template.StatusId,
UserDisplayName = template.UserDisplayName ?? detail.AffectedUser ?? "Ticket, Timo",
UserAccount = template.UserAccount ?? "TT007",
UserDomain = template.UserDomain ?? "CONTOSO",
UserId = template.UserId ?? Guid.Parse("42c760d6-90e8-469f-b2fe-ac7d4cc6cb0a"),
CreatedAt = createdAt,
Detail = detail
};
}
#endif
public Dictionary<string, int> GetCountsForScope(bool useRoleScope)
{
return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase);
}
private int GetDemoRelationCount(string key, bool useRoleScope)
{
lock (_demoRelations)
{
if (_demoRelations.TryGetValue((key, useRoleScope), out var list))
return list.Count;
}
return 0;
}
private void ScheduleFetchRetry()
{
if (_fetchRetryPending)
return;
_fetchRetryPending = true;
_ = _dispatcher.InvokeAsync(async () =>
{
try
{
await Task.Delay(250).ConfigureAwait(false);
await FetchAsync().ConfigureAwait(false);
}
finally
{
_fetchRetryPending = false;
}
}, DispatcherPriority.Background);
}
}
}