821 lines
28 KiB
C#
821 lines
28 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;
|
|
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 string[] OverviewKeys = new[]
|
|
{
|
|
"TicketsNew",
|
|
"TicketsActive",
|
|
"TicketsCritical",
|
|
"TicketsNewInfo",
|
|
"IncidentNew",
|
|
"IncidentActive",
|
|
"IncidentCritical",
|
|
"IncidentNewInfo",
|
|
"UnassignedTickets",
|
|
"UnassignedTicketsCritical"
|
|
};
|
|
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 readonly HashSet<TileScope> _pendingScopes = new HashSet<TileScope>();
|
|
private readonly HashSet<TileScope> _initializedScopes = new HashSet<TileScope>();
|
|
private readonly object _fetchLock = new object();
|
|
private readonly HashSet<TileScope> _retryScopes = new HashSet<TileScope>();
|
|
private DispatcherTimer _personalTimer;
|
|
private DispatcherTimer _roleTimer;
|
|
private Task _fetchWorker;
|
|
private bool _retryScheduled;
|
|
private bool _isDemo;
|
|
private bool _initialized;
|
|
private bool _isEnabled;
|
|
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()
|
|
{
|
|
Instance = new TicketOverviewUpdateService();
|
|
}
|
|
|
|
public static TicketOverviewUpdateService Instance { get; }
|
|
|
|
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
|
|
|
|
public IReadOnlyDictionary<string, TileCounts> CurrentCounts => _currentCounts;
|
|
|
|
public bool IsScopeInitialized(TileScope scope)
|
|
{
|
|
lock (_fetchLock)
|
|
{
|
|
return _initializedScopes.Contains(scope);
|
|
}
|
|
}
|
|
|
|
public bool AreAllScopesInitialized
|
|
{
|
|
get
|
|
{
|
|
lock (_fetchLock)
|
|
{
|
|
return _initializedScopes.Contains(TileScope.Personal)
|
|
&& _initializedScopes.Contains(TileScope.Role);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
UpdateAvailability(true);
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
UpdateAvailability(false);
|
|
}
|
|
|
|
public void UpdateAvailability(bool isEnabled)
|
|
{
|
|
if (isEnabled)
|
|
{
|
|
if (!_isEnabled)
|
|
{
|
|
_isEnabled = true;
|
|
StartInternal();
|
|
}
|
|
else
|
|
{
|
|
RefreshTimerIntervals();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_isEnabled)
|
|
StopInternal();
|
|
|
|
_isEnabled = false;
|
|
}
|
|
}
|
|
|
|
private void StartInternal()
|
|
{
|
|
if (_initialized)
|
|
return;
|
|
|
|
_initialized = true;
|
|
#if isDemo
|
|
_isDemo = true;
|
|
LoadPersistedDemoTickets();
|
|
#else
|
|
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
|
|
#endif
|
|
|
|
InitializeTimers();
|
|
_ = FetchAsync();
|
|
}
|
|
|
|
private void StopInternal()
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
_initialized = false;
|
|
lock (_fetchLock)
|
|
{
|
|
_pendingScopes.Clear();
|
|
_initializedScopes.Clear();
|
|
}
|
|
|
|
lock (_retryScopes)
|
|
{
|
|
_retryScheduled = false;
|
|
_retryScopes.Clear();
|
|
}
|
|
|
|
_dispatcher.InvokeAsync(() =>
|
|
{
|
|
_personalTimer?.Stop();
|
|
_roleTimer?.Stop();
|
|
_personalTimer = null;
|
|
_roleTimer = null;
|
|
|
|
foreach (var key in OverviewKeys)
|
|
{
|
|
_currentCounts[key] = TileCounts.Empty;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void InitializeTimers()
|
|
{
|
|
_personalTimer = CreateScopeTimer(TileScope.Personal);
|
|
_roleTimer = CreateScopeTimer(TileScope.Role);
|
|
|
|
_personalTimer?.Start();
|
|
_roleTimer?.Start();
|
|
}
|
|
|
|
private DispatcherTimer CreateScopeTimer(TileScope scope)
|
|
{
|
|
var interval = GetPollingInterval(scope);
|
|
var timer = new DispatcherTimer(interval, DispatcherPriority.Background, async (s, e) => await FetchAsync(scope).ConfigureAwait(false), _dispatcher)
|
|
{
|
|
IsEnabled = false
|
|
};
|
|
return timer;
|
|
}
|
|
|
|
private TimeSpan GetPollingInterval(TileScope scope)
|
|
{
|
|
var ticketConfig = cFasdCockpitConfig.Instance?.Global?.TicketConfiguration;
|
|
int minutes = scope == TileScope.Role
|
|
? cF4sdTicketConfig.DefaultOverviewPollingRole
|
|
: cF4sdTicketConfig.DefaultOverviewPollingPersonal;
|
|
|
|
if (ticketConfig != null)
|
|
{
|
|
minutes = scope == TileScope.Role
|
|
? ticketConfig.OverviewPollingRole
|
|
: ticketConfig.OverviewPollingPersonal;
|
|
}
|
|
|
|
if (minutes < 1)
|
|
minutes = 1;
|
|
|
|
return TimeSpan.FromMinutes(minutes);
|
|
}
|
|
|
|
public Task FetchAsync()
|
|
{
|
|
if (!_isEnabled)
|
|
return Task.CompletedTask;
|
|
|
|
return QueueFetchAsync(new[] { TileScope.Personal, TileScope.Role });
|
|
}
|
|
|
|
public Task FetchAsync(TileScope scope)
|
|
{
|
|
if (!_isEnabled)
|
|
return Task.CompletedTask;
|
|
|
|
return QueueFetchAsync(new[] { scope });
|
|
}
|
|
|
|
private Task QueueFetchAsync(IEnumerable<TileScope> scopes)
|
|
{
|
|
if (!_isEnabled)
|
|
return Task.CompletedTask;
|
|
|
|
if (scopes == null)
|
|
return Task.CompletedTask;
|
|
|
|
lock (_fetchLock)
|
|
{
|
|
foreach (var scope in scopes)
|
|
{
|
|
_pendingScopes.Add(scope);
|
|
}
|
|
|
|
if (_fetchWorker == null || _fetchWorker.IsCompleted)
|
|
{
|
|
_fetchWorker = Task.Run(ProcessFetchQueueAsync);
|
|
}
|
|
|
|
return _fetchWorker;
|
|
}
|
|
}
|
|
|
|
private async Task ProcessFetchQueueAsync()
|
|
{
|
|
while (true)
|
|
{
|
|
TileScope scope;
|
|
|
|
lock (_fetchLock)
|
|
{
|
|
if (_pendingScopes.Count == 0)
|
|
{
|
|
_fetchWorker = null;
|
|
return;
|
|
}
|
|
|
|
scope = _pendingScopes.First();
|
|
_pendingScopes.Remove(scope);
|
|
}
|
|
|
|
await FetchScopeAsync(scope).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task FetchScopeAsync(TileScope scope)
|
|
{
|
|
if (!_isEnabled)
|
|
return;
|
|
|
|
var communication = cFasdCockpitCommunicationBase.Instance;
|
|
if (communication == null)
|
|
{
|
|
ScheduleFetchRetry(scope);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_isDemo = communication.IsDemo();
|
|
|
|
var rawCounts = await communication.GetTicketOverviewCounts(OverviewKeys, scope == TileScope.Role).ConfigureAwait(false);
|
|
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (rawCounts != null)
|
|
{
|
|
foreach (var kvp in rawCounts)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(kvp.Key))
|
|
continue;
|
|
|
|
counts[kvp.Key] = Math.Max(0, kvp.Value);
|
|
}
|
|
}
|
|
|
|
if (!_isEnabled)
|
|
return;
|
|
|
|
await _dispatcher.InvokeAsync(() => ProcessScopeCounts(scope, counts));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Fetch {scope} failed: {ex}");
|
|
ScheduleFetchRetry(scope);
|
|
}
|
|
}
|
|
|
|
private void RefreshTimerIntervals()
|
|
{
|
|
_dispatcher.InvokeAsync(() =>
|
|
{
|
|
if (_personalTimer != null)
|
|
_personalTimer.Interval = GetPollingInterval(TileScope.Personal);
|
|
|
|
if (_roleTimer != null)
|
|
_roleTimer.Interval = GetPollingInterval(TileScope.Role);
|
|
});
|
|
}
|
|
|
|
private void ProcessScopeCounts(TileScope scope, IDictionary<string, int> newCounts)
|
|
{
|
|
if (newCounts == null)
|
|
return;
|
|
|
|
var hasInitializedScope = _initializedScopes.Contains(scope);
|
|
var changes = new List<TileCountChange>();
|
|
|
|
foreach (var key in OverviewKeys)
|
|
{
|
|
var previous = _currentCounts.TryGetValue(key, out var counts) ? counts : TileCounts.Empty;
|
|
var incoming = newCounts.TryGetValue(key, out var value) ? value : 0;
|
|
|
|
TileCounts updated;
|
|
int oldValue;
|
|
|
|
if (scope == TileScope.Role)
|
|
{
|
|
updated = new TileCounts(previous.Personal, incoming);
|
|
oldValue = previous.Role;
|
|
}
|
|
else
|
|
{
|
|
updated = new TileCounts(incoming, previous.Role);
|
|
oldValue = previous.Personal;
|
|
}
|
|
|
|
_currentCounts[key] = updated;
|
|
|
|
if (hasInitializedScope && oldValue != incoming)
|
|
{
|
|
changes.Add(new TileCountChange(key, scope, oldValue, incoming));
|
|
}
|
|
}
|
|
|
|
if (!hasInitializedScope)
|
|
{
|
|
_initializedScopes.Add(scope);
|
|
var initArgs = new TicketOverviewCountsChangedEventArgs(
|
|
Array.Empty<TileCountChange>(),
|
|
new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase),
|
|
scope);
|
|
OverviewCountsChanged?.Invoke(this, initArgs);
|
|
return;
|
|
}
|
|
|
|
if (changes.Count == 0)
|
|
return;
|
|
|
|
var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase));
|
|
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 activityType = ResolveDemoActivityType(record?.ActivityType);
|
|
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,
|
|
["ActivityType"] = activityType
|
|
},
|
|
Identities = new cF4sdIdentityList
|
|
{
|
|
new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = record.TicketId },
|
|
new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = record.UserId }
|
|
}
|
|
};
|
|
|
|
return relation;
|
|
}
|
|
|
|
private static string ResolveDemoActivityType(string configuredActivityType)
|
|
{
|
|
return string.IsNullOrWhiteSpace(configuredActivityType)
|
|
? null
|
|
: configuredActivityType.Trim();
|
|
}
|
|
|
|
private DemoTicketDetail CloneDetail(DemoTicketDetail source)
|
|
{
|
|
if (source == null)
|
|
return new DemoTicketDetail();
|
|
|
|
return new DemoTicketDetail
|
|
{
|
|
AffectedUser = source.AffectedUser,
|
|
Asset = source.Asset,
|
|
Category = source.Category,
|
|
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,
|
|
ActivityType = ResolveDemoActivityType(template.ActivityType),
|
|
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(TileScope scope)
|
|
{
|
|
if (!_isEnabled)
|
|
return;
|
|
|
|
lock (_retryScopes)
|
|
{
|
|
_retryScopes.Add(scope);
|
|
if (_retryScheduled)
|
|
return;
|
|
|
|
_retryScheduled = true;
|
|
}
|
|
|
|
_ = _dispatcher.InvokeAsync(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
|
TileScope[] scopes;
|
|
lock (_retryScopes)
|
|
{
|
|
scopes = _retryScopes.ToArray();
|
|
_retryScopes.Clear();
|
|
_retryScheduled = false;
|
|
}
|
|
|
|
foreach (var pendingScope in scopes)
|
|
{
|
|
await FetchAsync(pendingScope).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}");
|
|
}
|
|
}, DispatcherPriority.Background);
|
|
}
|
|
}
|
|
}
|