using C4IT.FASD.Base; using C4IT.FASD.Cockpit.Communication; using C4IT.Logging; using C4IT.MultiLanguage; using FasdDesktopUi.Basics; using FasdDesktopUi.Basics.Enums; using FasdDesktopUi.Basics.Helper; using FasdDesktopUi.Basics.Models; using FasdDesktopUi.Basics.Services; using FasdDesktopUi.Basics.Services.Models; using FasdDesktopUi.Basics.Services.RelationService; using FasdDesktopUi.Basics.Services.SupportCaseSearchService; using FasdDesktopUi.Basics.UiActions; using FasdDesktopUi.Basics.UserControls; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Threading; using static C4IT.Logging.cLogManager; using static FasdDesktopUi.Basics.UserControls.TicketOverview; namespace FasdDesktopUi.Pages.SearchPage { public partial class SearchPageView : Window, ISearchUiProvider { private static SearchPageView _instance = null; private const int WM_NCHITTEST = 0x0084; private const int HTTRANSPARENT = -1; public static SearchPageView Instance { get { return _instance ?? (_instance = new SearchPageView()); } } private cF4sdPipeServer _pipeServer; private cHotKeyManager _hotKeyManager; private CancellationTokenSource _searchCancellationTokenSource = new CancellationTokenSource(); private bool _isActivating = false; private cF4sdApiSearchResultRelation preSelectedRelation = null; private bool _renderTicketOverviewUserNames = false; private readonly HashSet _ticketOverviewHistoryEntries = new HashSet(); private const string DemoTicketHasDetailsInfoKey = "Demo.HasTicketDetails"; // Event zum auslösen wenn Toggle geändert wird public event EventHandler FilterToggleCheckedChanged; // Aktueller Zustand der Checkbox public bool IsFilterChecked => FilterCheckbox.IsChecked == true; public SupportCaseSearchService SearchService { get; } = new SupportCaseSearchService(new RelationService()); private SearchPageView() { try { InitializeComponent(); Visibility = Visibility.Visible; _instance = this; // FilterToggleCheckBox-Events registrieren FilterCheckbox.Checked += (s, e) => FilterToggleCheckedChanged?.Invoke(this, true); FilterCheckbox.Unchecked += (s, e) => FilterToggleCheckedChanged?.Invoke(this, false); Loaded += (s, e) => { Hide(); SearchBarUc.ActivateManualSearch(); }; AddCustomEventHandlers(); UiSettingsChanged(null, null); if (TicketOverviewUpdateService.Instance != null) { TicketOverviewUpdateService.Instance.Start(); TicketOverviewUpdateService.Instance.OverviewCountsChanged += TicketOverviewUpdateService_OverviewCountsChanged; } } catch (Exception E) { LogException(E); } } private void SetSearchResultVisibility(bool isVisible) { SearchResultBorder.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed; BodyStack_SearchResults.Visibility = (isVisible || SearchResultBorder.IsVisible) ? Visibility.Visible : Visibility.Collapsed; } public void SetSearchHistoryVisibility(bool isVisible) { SearchHistoryBorder.Visibility = isVisible && !SearchHistory.IsEmpty() ? Visibility.Visible : Visibility.Collapsed; BodyStack_SearchResults.Visibility = (isVisible || SearchResultBorder.IsVisible) ? Visibility.Visible : Visibility.Collapsed; } private bool CheckTicketOverviewAvailability() { if (cFasdCockpitCommunicationBase.Instance.IsDemo()) return true; return false; } private void SetTicketOverviewVisibility(bool isVisible) { var b = isVisible; if (!CheckTicketOverviewAvailability()) b = false; BodyStack_TicketOverview.Visibility = b ? Visibility.Visible : Visibility.Collapsed; TicketOverviewBorder.Visibility = b ? Visibility.Visible : Visibility.Collapsed; FilterCheckbox.Visibility = b ? Visibility.Visible : Visibility.Collapsed; RoleLabel.Visibility = b ? Visibility.Visible : Visibility.Collapsed; OwnTicketsLabel.Visibility = b ? Visibility.Visible : Visibility.Collapsed; TicketOverviewLabel.Visibility = b ? Visibility.Visible : Visibility.Collapsed; } public void ShowLoadingTextItem(string itemText) { SetSearchHistoryVisibility(false); ResultMenu.ShowLoadingTextItem(itemText); } public void ShowTicketOverviewPane() { Dispatcher.Invoke(() => { bool overviewAlreadyVisible = TicketOverviewBorder.Visibility == Visibility.Visible; SetTicketOverviewVisibility(true); if (!overviewAlreadyVisible) { SetSearchResultVisibility(false); SetSearchHistoryVisibility(false); TicketOverviewUc?.ResetSelection(); } TicketOverviewUc?.RefreshHighlightState(IsFilterChecked); var app = Application.Current as FasdDesktopUi.App; app?.ClearTicketOverviewTrayNotification(); }); } internal void CloseTicketOverviewResults() { _renderTicketOverviewUserNames = false; ResultMenu.ShowSearchResults(new cFilteredResults(), null, this); ResultMenu.SetHeaderText(string.Empty); SetSearchResultVisibility(false); SetTicketOverviewVisibility(true); } public void ShowSearchRelations(cSearchHistorySearchResultEntry searchHistoryEntry, IRelationService relationService, ISearchUiProvider searchUiProvider) { try { _renderTicketOverviewUserNames = searchHistoryEntry != null && _ticketOverviewHistoryEntries.Contains(searchHistoryEntry); Dispatcher.Invoke(() => ResultMenu.SetHeaderText(searchHistoryEntry.HeaderText, hideDetailsCheckbox: _renderTicketOverviewUserNames)); cSearchManager.ResolveRelations(searchHistoryEntry.Relations); ILookup relationsLookup = searchHistoryEntry.Relations .OrderBy(r => r.UsingLevel) .ThenBy(r => r.LastUsed) .ToLookup(GetInformationClass, r => GetMenuData(r, relationService)); Dispatcher.Invoke(() => { ResultMenu.UpdateSearchRelations(relationsLookup); ResultMenu.SetHeaderText(searchHistoryEntry.HeaderText, hideDetailsCheckbox: _renderTicketOverviewUserNames); }); } catch (Exception ex) { LogException(ex); } enumFasdInformationClass GetInformationClass(cF4sdApiSearchResultRelation relation) => cF4sdIdentityEntry.GetFromSearchResult(relation.Type); cMenuDataBase GetMenuData(cF4sdApiSearchResultRelation relation, IRelationService trelationService) { try { var requiredInformationClasses = new List() { cF4sdIdentityEntry.GetFromSearchResult(relation.Type) }; bool isEnabled = cHealthCardDataHelper.HasAvailableHealthCard(requiredInformationClasses); string disabledReason = null; if (!isEnabled) { disabledReason = cMultiLanguageSupport.GetItem("Searchbar.NoValidHealthcard") ?? string.Empty; } else if (ShouldDisableTicketRelationForDemo(relation)) { isEnabled = false; disabledReason = cMultiLanguageSupport.GetItem("Searchbar.Demo.NoTicketDetails") ?? string.Empty; } string trailingText = null; if (_renderTicketOverviewUserNames && relation.Infos != null && relation.Infos.TryGetValue("UserDisplayName", out var relationUserDisplayName)) { trailingText = relationUserDisplayName; } string summaryText = null; if (_renderTicketOverviewUserNames && relation.Infos != null && relation.Infos.TryGetValue("Summary", out var relationSummary)) { summaryText = relationSummary; } return new cMenuDataSearchRelation(relation) { MenuText = relation.DisplayName, SubMenuText = summaryText, TrailingText = trailingText, UiAction = new cUiProcessSearchRelationAction(searchHistoryEntry, relation, relationService, searchUiProvider) { DisplayType = isEnabled ? enumActionDisplayType.enabled : enumActionDisplayType.disabled, Description = isEnabled ? string.Empty : disabledReason, AlternativeDescription = isEnabled ? string.Empty : disabledReason } }; } catch (Exception ex) { LogException(ex); // >>> Fallback NICHT null: einfacher, aktivierter Eintrag <<< string fallbackTrailingText = null; if (_renderTicketOverviewUserNames && relation.Infos != null && relation.Infos.TryGetValue("UserDisplayName", out var relationUserDisplayNameFallback)) { fallbackTrailingText = relationUserDisplayNameFallback; } string fallbackSummaryText = null; if (_renderTicketOverviewUserNames && relation.Infos != null && relation.Infos.TryGetValue("Summary", out var relationSummaryFallback)) { fallbackSummaryText = relationSummaryFallback; } string fallbackDisabledReason = null; bool fallbackIsEnabled = true; try { var required = new List { cF4sdIdentityEntry.GetFromSearchResult(relation.Type) }; fallbackIsEnabled = cHealthCardDataHelper.HasAvailableHealthCard(required); if (!fallbackIsEnabled) { fallbackDisabledReason = cMultiLanguageSupport.GetItem("Searchbar.NoValidHealthcard") ?? string.Empty; } else if (ShouldDisableTicketRelationForDemo(relation)) { fallbackIsEnabled = false; fallbackDisabledReason = cMultiLanguageSupport.GetItem("Searchbar.Demo.NoTicketDetails") ?? string.Empty; } } catch { fallbackIsEnabled = true; fallbackDisabledReason = string.Empty; } return new cMenuDataSearchRelation(relation) { MenuText = relation.DisplayName, SubMenuText = fallbackSummaryText, TrailingText = fallbackTrailingText, UiAction = new cUiProcessSearchRelationAction(searchHistoryEntry, relation, relationService, searchUiProvider) { DisplayType = fallbackIsEnabled ? enumActionDisplayType.enabled : enumActionDisplayType.disabled, Description = fallbackIsEnabled ? string.Empty : fallbackDisabledReason, AlternativeDescription = fallbackIsEnabled ? string.Empty : fallbackDisabledReason } }; } } } private void TriggeredSearch(string searchValue) { try { var visibleWindows = Application.Current.Windows.Cast().Where(window => window.IsVisible && (window is SearchPageView) == false).ToList(); visibleWindows.ForEach(window => window.Visibility = Visibility.Hidden); SearchBarUc.SetSearchText(searchValue); Show(); Activate(); } catch (Exception E) { LogException(E); } } private void ShowExternalSearchInfo(string strInfo, cFasdApiSearchResultCollection resultEntry, enumF4sdSearchResultClass Class) { var filteredResults = new cFilteredResults(resultEntry) { AutoContinue = true }; var _t = ShowExternalSearchInfoAsync(strInfo, filteredResults, Class); } private async Task ShowExternalSearchInfoAsync(string strInfo, cFilteredResults result, enumF4sdSearchResultClass Class) { try { await this.Dispatcher.InvokeAsync(async () => { try { await SearchBarUc.SetFixedSearchResultAsync(Class, strInfo, result); if (result.AutoContinue && result.Results?.Count == 1) { ResultMenu.IndexOfSelectedResultItem = 0; ResultMenu.SelectCurrentResultItem(); } Show(); } catch (Exception E) { LogException(E); } }); } catch (Exception E) { LogException(E); } } private void ActivateManualSearch() { CancledSearchAction(); SearchBarUc.ActivateManualSearch(); Show(); Activate(); if (cSearchManager.Instance.HistoryList.Count > 0) { SearchHistory.ShowSearchHistory(); SetSearchHistoryVisibility(true); } else SetSearchHistoryVisibility(false); } public void PhoneCallSearch(cPhoneSearchParameters searchInfo) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { var _h = Task.Run(async () => { try { var _result = await cFasdCockpitCommunicationBase.Instance.GetPhoneSearchResults(searchInfo); if (_result != null && _result.Count > 0 || cFasdCockpitConfig.Instance?.PhoneSupport?.ShowUnresolvedPhoneNumbers is true) { string strTxt; if (string.IsNullOrEmpty(searchInfo.name)) strTxt = searchInfo.phone; else strTxt = $"{searchInfo.name} ({searchInfo.phone})"; var strInfo = string.Format(cMultiLanguageSupport.GetItem("Searchbar.Phonecall.Info"), strTxt); ShowExternalSearchInfo(strInfo, _result, enumF4sdSearchResultClass.Phone); } } catch (Exception E) { LogException(E); } }); } catch (Exception E) { LogException(E); } finally { LogMethodEnd(CM); } } private void ComputerDomainSearch(cComputerDomainSearchParameters searchInfo) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { var _h = Task.Run(async () => { try { var _result = await cFasdCockpitCommunicationBase.Instance.GetComputerSearchResults(searchInfo.name, searchInfo.domain); var strInfo = string.Format(cMultiLanguageSupport.GetItem("Searchbar.ComputerSearch.Info"), searchInfo.name); ShowExternalSearchInfo(strInfo, _result, enumF4sdSearchResultClass.Computer); } catch (Exception E) { LogException(E); } }); } catch (Exception E) { LogException(E); } finally { LogMethodEnd(CM); } } private void UserSidSearch(cUserSidSearchParameters searchInfo) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { var _h = Task.Run(async () => { try { var lstSids = searchInfo.sids.Split(',').Select((v) => v.Trim()).ToList(); var _result = await cFasdCockpitCommunicationBase.Instance.GetUserSearchResults(searchInfo.name, lstSids); var strInfo = string.Format(cMultiLanguageSupport.GetItem("Searchbar.UserSearch.Info"), searchInfo.name); ShowExternalSearchInfo(strInfo, _result, enumF4sdSearchResultClass.User); } catch (Exception E) { LogException(E); } }); } catch (Exception E) { LogException(E); } finally { LogMethodEnd(CM); } } private void TicketSearch(cTicketSearchParameters searchInfo) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { if (!Guid.TryParse(searchInfo.ticketId, out var _ticketId)) return; var _h = Task.Run(async () => { try { var lstSids = searchInfo.sids?.Split(',').Select((v) => v.Trim()).ToList(); var _result = await cFasdCockpitCommunicationBase.Instance.GetUserSearchResults(searchInfo.userName, lstSids); if (_result is null || _result.Count == 0 || _result.First().Value.Count == 0) { LogEntry($"No corresponding user could be found for ticket '{searchInfo.ticketName}'", LogLevels.Warning); return; } var UserId = _result.Values.First().First().id; var _ticketRelation = new cF4sdApiSearchResultRelation() { Type = enumF4sdSearchResultClass.Ticket, DisplayName = searchInfo.ticketName, id = _ticketId, Status = enumF4sdSearchResultStatus.Active, Identities = new cF4sdIdentityList { { new cF4sdIdentityEntry() { Class = enumFasdInformationClass.User, Id = UserId } }, { new cF4sdIdentityEntry() { Class = enumFasdInformationClass.Ticket, Id = _ticketId } }, } }; var filteredResults = new cFilteredResults(_result) { AutoContinue = true, PreSelectedRelation = _ticketRelation }; var strInfo = string.Format(cMultiLanguageSupport.GetItem("Searchbar.TicketSearch.Info"), searchInfo.ticketName); var _t = ShowExternalSearchInfoAsync(strInfo, filteredResults, enumF4sdSearchResultClass.Ticket); } catch (Exception E) { LogException(E); } }); } catch (Exception E) { LogException(E); } finally { LogMethodEnd(CM); } } private void PipeServer_PipeMessage(string reply) { try { var type = reply; if (type.Contains(':')) type = reply.Substring(0, Math.Max(reply.IndexOf(':'), 0)); type = type.ToLowerInvariant(); if (string.IsNullOrWhiteSpace(type)) { LogEntry("No valid pipe reply", LogLevels.Warning); return; } if (type == "hello") return; if (DefaultLogger.IsDebug) { var _msg = new List() { "pipe message received:", reply }; DefaultLogger.LogList(LogLevels.Debug, _msg); } if (cConnectionStatusHelper.Instance?.ApiConnectionStatus is cConnectionStatusHelper.enumOnlineStatus.offline) return; var value = reply.Remove(0, type.Length).TrimStart(':'); switch (type) { case "search": try { Dispatcher.Invoke(() => TriggeredSearch(value)); } catch (Exception E) { LogException(E); } break; case "phonesearch": try { var searchInfo = JsonConvert.DeserializeObject(value); PhoneCallSearch(searchInfo); } catch (Exception E) { LogException(E); } break; case "computerdomainsearch": try { var searchInfo = JsonConvert.DeserializeObject(value); ComputerDomainSearch(searchInfo); } catch (Exception E) { LogException(E); } break; case "usersidsearch": try { var searchInfo = JsonConvert.DeserializeObject(value); UserSidSearch(searchInfo); } catch (Exception E) { LogException(E); } break; case "ticketsearch": try { var searchInfo = JsonConvert.DeserializeObject(value); TicketSearch(searchInfo); } catch (Exception E) { LogException(E); } break; default: break; } } catch (Exception E) { LogException(E); } } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { _pipeServer = new cF4sdPipeServer(); _pipeServer.PipeMessage += PipeServer_PipeMessage; _pipeServer.Listen(App.PipeName, MaxSize: 4096, LowerIntegrity: true); LogEntry($"Start listening on named pipe '{App.PipeName}'...", LogLevels.Debug); IntPtr handle = new WindowInteropHelper(this).Handle; var hwndSource = HwndSource.FromHwnd(handle); hwndSource.AddHook(SearchViewWindowProc); hwndSource.AddHook(new HwndSourceHook(cUtility.WindowProc)); var hotKeyDefinitions = new List { new cHotKeyManager.cHotKeyDefinition { Id = 1, Modifier = new List { ModifierKeys.Control }, Key = Key.F3, HotKeyAction = HotKeyManager_ActivateSearch }, new cHotKeyManager.cHotKeyDefinition { Id = 2, Modifier = new List { ModifierKeys.Control, ModifierKeys.Alt }, Key = Key.F3, HotKeyAction = HotKeyManager_CopyAndSearch } }; #if isDemo if (cFasdCockpitCommunicationBase.Instance?.IsDemo() == true) { hotKeyDefinitions.Add(new cHotKeyManager.cHotKeyDefinition { Id = 3, Modifier = new List { ModifierKeys.Control, ModifierKeys.Alt }, Key = Key.T, HotKeyAction = () => TicketOverviewUpdateService.Instance.SimulateDemoTicket() }); } #endif _hotKeyManager = new cHotKeyManager(handle, hotKeyDefinitions); } catch (Exception E) { LogException(E); } finally { LogMethodEnd(CM); } } public void ActivateSearchView() { if (_isActivating) return; _isActivating = true; var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); try { bool doShow = true; if (doShow) { ActivateManualSearch(); } } catch (Exception E) { LogException(E); } finally { _isActivating = false; LogMethodEnd(CM); } } private void HotKeyManager_ActivateSearch() { if (!IsVisible) ActivateSearchView(); } public void BringToFrontPreserveState() { Dispatcher.Invoke(() => { if (!IsVisible) Show(); if (WindowState == WindowState.Minimized) WindowState = WindowState.Normal; Activate(); Topmost = true; Topmost = false; Focus(); }); } private async void HotKeyManager_CopyAndSearch() { try { bool isHotKeyDown = true; while (isHotKeyDown) { isHotKeyDown = _hotKeyManager.IsKeyDown(cHotKeyManager.VirtualKeyStates.VK_F3) || _hotKeyManager.IsKeyDown(cHotKeyManager.VirtualKeyStates.VK_CONTROL) || _hotKeyManager.IsKeyDown(cHotKeyManager.VirtualKeyStates.VK_MENU); await Task.Delay(50); } cUtility.SendCtrlCToCurrentWindow(); await Task.Delay(150); var copiedText = Clipboard.GetText(); copiedText = cUtility.CleanPhoneString(copiedText); copiedText = copiedText.Trim().Substring(0, Math.Min(copiedText.Trim().Length, 75)); ActivateSearchView(); if (copiedText != null) { await Dispatcher.BeginInvoke((Action)delegate { Activate(); SearchBarUc.SetSearchText(copiedText); }, DispatcherPriority.Render); } } catch (Exception E) { LogException(E); } } protected override void OnClosed(EventArgs e) { try { if (TicketOverviewUpdateService.Instance != null) TicketOverviewUpdateService.Instance.OverviewCountsChanged -= TicketOverviewUpdateService_OverviewCountsChanged; _pipeServer?.Dispose(); } catch { } _pipeServer = null; _hotKeyManager?.UnRegisterHotKeys(); RemoveHandler(TicketOverview.SelectionRequestedEvent, new TicketOverviewSelectionRequestedEventHandler(TicketOverview_SelectionRequested)); base.OnClosed(e); } private void AddCustomEventHandlers() { AddHandler(cUiActionBase.UiActionClickedEvent, new cUiActionBase.UiActionEventHandlerDelegate(UiActionWasTriggered)); SearchBarUc.CancledSearchAction = CancledSearchAction; AddHandler(TicketOverview.SelectionRequestedEvent, new TicketOverviewSelectionRequestedEventHandler(TicketOverview_SelectionRequested), true); cFasdCockpitConfig.Instance.UiSettingsChanged += UiSettingsChanged; SearchBarUc.SearchValueChanged += HandleSearchValueChanged; SearchService.RelationsFound += HandleRelationsFound; } private async void HandleSearchValueChanged(object sender, string searchValue) { const int minSearchLength = 2; try { _searchCancellationTokenSource?.Cancel(); _searchCancellationTokenSource = new CancellationTokenSource(); if (string.IsNullOrWhiteSpace(searchValue)) { SearchBarUc.SetSpinnerVisibility(Visibility.Hidden); UpdateSearchResults(new cFilteredResults()); return; } if (searchValue.Length < minSearchLength) return; SearchBarUc.SetSpinnerVisibility(Visibility.Visible); cFasdApiSearchResultCollection searchResults = await SupportCaseSearchService.GetSearchResultsAsync(searchValue, _searchCancellationTokenSource.Token); SearchBarUc.SetSpinnerVisibility(Visibility.Hidden); UpdateSearchResults(new cFilteredResults(searchResults)); } catch (Exception ex) { LogException(ex); } } private void HandleRelationsFound(object sender, StagedSearchResultRelationsEventArgs e) { try { Dispatcher.Invoke(() => { var first = e.RelatedTo.FirstOrDefault(); var relationSearchResult = new cSearchHistorySearchResultEntry(first.DisplayName, first.DisplayName, e.RelatedTo.ToList(), e.StagedResultRelations.Relations.ToList(), this); ShowSearchRelations(relationSearchResult, e.RelationService, this); UpdatePendingInformationClasses(e.StagedResultRelations.PendingInformationClasses); }); } catch (Exception ex) { LogException(ex); } } private void TicketOverviewUpdateService_OverviewCountsChanged(object sender, TicketOverviewCountsChangedEventArgs e) { try { ApplyLatestCounts(); var positiveChanges = e.Changes?.Where(change => change.Delta > 0).ToList(); TicketOverviewUc?.SetHighlights(positiveChanges, IsFilterChecked); var app = Application.Current as FasdDesktopUi.App; if (positiveChanges == null || positiveChanges.Count == 0) { app?.ClearTicketOverviewTrayNotification(); return; } var message = BuildNotificationMessage(positiveChanges); if (string.IsNullOrWhiteSpace(message)) { app?.ClearTicketOverviewTrayNotification(); return; } app?.ShowTicketOverviewTrayNotification(message); ShowTicketOverviewNotification(message); } catch (Exception ex) { LogException(ex); } } private void ApplyLatestCounts() { var service = TicketOverviewUpdateService.Instance; if (service == null) return; var counts = service.GetCountsForScope(IsFilterChecked); if (counts == null || counts.Count == 0) return; TicketOverviewUc?.UpdateCounts(counts, IsFilterChecked); TicketOverviewUc?.RefreshHighlightState(IsFilterChecked); } private string BuildNotificationMessage(IReadOnlyList changes) { if (changes == null || changes.Count == 0) return string.Empty; var aggregates = new List<(string RowKey, string ColumnKey, TileScope Scope, int Delta)>(); var indexMap = new Dictionary<(string RowKey, string ColumnKey, TileScope Scope), int>(); foreach (var change in changes) { if (!TryResolveTicketOverviewLabels(change.Key, out var rowKey, out var columnKey)) continue; var delta = change.NewCount - change.OldCount; if (delta == 0) continue; var aggregateKey = (rowKey, columnKey, change.Scope); if (indexMap.TryGetValue(aggregateKey, out var aggregateIndex)) { var existing = aggregates[aggregateIndex]; aggregates[aggregateIndex] = (existing.RowKey, existing.ColumnKey, existing.Scope, existing.Delta + delta); } else { indexMap[aggregateKey] = aggregates.Count; aggregates.Add((rowKey, columnKey, change.Scope, delta)); } } if (aggregates.Count == 0) return string.Empty; var pieces = new List(aggregates.Count); foreach (var aggregate in aggregates) { if (aggregate.Delta == 0) continue; var row = cMultiLanguageSupport.GetItem(aggregate.RowKey) ?? aggregate.RowKey; var column = cMultiLanguageSupport.GetItem(aggregate.ColumnKey) ?? aggregate.ColumnKey; var scopeRowLabel = GetScopeRowLabel(aggregate.Scope, aggregate.RowKey, row); var deltaText = aggregate.Delta >= 0 ? $"+{aggregate.Delta}" : aggregate.Delta.ToString(); pieces.Add($"{scopeRowLabel} – {column} ({deltaText})"); } return string.Join(Environment.NewLine, pieces); } private static bool TryResolveTicketOverviewLabels(string key, out string rowKey, out string columnKey) { rowKey = null; columnKey = null; if (string.IsNullOrWhiteSpace(key)) return false; var normalized = key.ToLowerInvariant(); if (normalized.StartsWith("tickets")) rowKey = "TicketOverview.Row.Heading.Tickets"; else if (normalized.StartsWith("incident")) rowKey = "TicketOverview.Row.Heading.Incidents"; else if (normalized.StartsWith("unassigned")) rowKey = "TicketOverview.Row.Heading.UnassignedTickets"; else return false; columnKey = ResolveColumnTranslationKey(normalized); return columnKey != null; } private static string ResolveColumnTranslationKey(string normalizedKey) { if (normalizedKey.EndsWith("newinfo")) return "TicketOverview.Column.Heading.NewInfo"; if (normalizedKey.EndsWith("critical")) return "TicketOverview.Column.Heading.Critical"; if (normalizedKey.EndsWith("active")) return "TicketOverview.Column.Heading.Active"; return "TicketOverview.Column.Heading.New"; } private static string GetScopeRowLabel(TileScope scope, string rowKey, string rowLabel) { string suffix = null; if (string.Equals(rowKey, "TicketOverview.Row.Heading.Tickets", StringComparison.OrdinalIgnoreCase)) suffix = "Tickets"; else if (string.Equals(rowKey, "TicketOverview.Row.Heading.Incidents", StringComparison.OrdinalIgnoreCase)) suffix = "Incidents"; else if (string.Equals(rowKey, "TicketOverview.Row.Heading.UnassignedTickets", StringComparison.OrdinalIgnoreCase)) suffix = "UnassignedTickets"; var translationKey = scope == TileScope.Role ? $"TicketOverview.ScopeRow.Role.{suffix}" : $"TicketOverview.ScopeRow.Personal.{suffix}"; return cMultiLanguageSupport.GetItem(translationKey) ?? rowLabel; } private IntPtr SearchViewWindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_NCHITTEST) { if (!IsPointWithinInteractiveBounds(lParam)) { handled = true; return new IntPtr(HTTRANSPARENT); } } return IntPtr.Zero; } private bool IsPointWithinInteractiveBounds(IntPtr lParam) { if (!IsLoaded || MainBorder == null || !MainBorder.IsLoaded) return true; if (MainBorder.ActualWidth <= 0 || MainBorder.ActualHeight <= 0) return true; var screenPoint = ExtractScreenPoint(lParam); var windowPoint = PointFromScreen(screenPoint); if (!(InputHitTest(windowPoint) is DependencyObject hitElement)) return false; return IsDescendantOf(hitElement, MainBorder); } private static Point ExtractScreenPoint(IntPtr lParam) { int value = unchecked((int)(long)lParam); short x = (short)(value & 0xFFFF); short y = (short)((value >> 16) & 0xFFFF); return new Point(x, y); } private static bool IsDescendantOf(DependencyObject element, DependencyObject ancestor) { while (element != null) { if (element == ancestor) return true; element = GetParent(element); } return false; } private static DependencyObject GetParent(DependencyObject current) { if (current == null) return null; if (current is Visual || current is System.Windows.Media.Media3D.Visual3D) return VisualTreeHelper.GetParent(current); if (current is System.Windows.ContentElement contentElement) return System.Windows.ContentOperations.GetParent(contentElement); return LogicalTreeHelper.GetParent(current); } private void ShowTicketOverviewNotification(string message) { if (string.IsNullOrWhiteSpace(message)) return; try { var notificationTitle = cMultiLanguageSupport.GetItem("TicketOverview.Notification.Title") ?? "Ticketübersicht"; var app = Application.Current as App; app?.notifyIcon?.ShowBalloonTip(3000, notificationTitle, message, System.Windows.Forms.ToolTipIcon.Info); } catch (Exception ex) { LogException(ex); } } private async void TicketOverview_SelectionRequested(object sender, TicketOverviewSelectionRequestedEventArgs e) { try { var app = Application.Current as FasdDesktopUi.App; app?.ClearTicketOverviewTrayNotification(); _renderTicketOverviewUserNames = true; SetSearchHistoryVisibility(false); SetTicketOverviewVisibility(true); SetSearchResultVisibility(true); var header = BuildHeaderText(e.Key, e.UseRoleScope, e.Count); ResultMenu.SetHeaderText(header, hideDetailsCheckbox: true); ShowLoadingTextItem(cMultiLanguageSupport.GetItem("Searchbar.Loading.CaseData")); SetPendingInformationClasses(new HashSet { enumFasdInformationClass.Ticket }); var relations = await LoadRelationsForTileAsync(e.Key, e.UseRoleScope, Math.Max(0, e.Count)); Debug.WriteLine($"[TicketOverview] Relations loaded: {relations?.Count ?? 0}"); var firstRelation = relations.FirstOrDefault(); string displayText = header; if (firstRelation != null) { string firstSummary = null; if (firstRelation.Infos != null && firstRelation.Infos.TryGetValue("Summary", out var summaryValue)) { firstSummary = summaryValue; } displayText = string.IsNullOrWhiteSpace(firstSummary) ? $"{header} → {firstRelation.DisplayName}" : $"{header} → {firstRelation.DisplayName} – {firstSummary}"; } var entry = new cSearchHistorySearchResultEntry( displayText, header, new List(), relations, this ) { isSeen = true }; _ticketOverviewHistoryEntries.Add(entry); var lookup = relations.ToLookup( r => cF4sdIdentityEntry.GetFromSearchResult(r.Type), r => { var required = new List { cF4sdIdentityEntry.GetFromSearchResult(r.Type) }; bool isEnabled = cHealthCardDataHelper.HasAvailableHealthCard(required); string disabledReason = null; if (!isEnabled) { disabledReason = cMultiLanguageSupport.GetItem("Searchbar.NoValidHealthcard") ?? string.Empty; } else if (ShouldDisableTicketRelationForDemo(r)) { isEnabled = false; disabledReason = cMultiLanguageSupport.GetItem("Searchbar.Demo.NoTicketDetails") ?? string.Empty; } string trailingUser = null; if (_renderTicketOverviewUserNames && r.Infos != null && r.Infos.TryGetValue("UserDisplayName", out var userDisplayName)) { trailingUser = userDisplayName; } return (cMenuDataBase)new cMenuDataSearchRelation(r) { MenuText = r.DisplayName, TrailingText = trailingUser, UiAction = new cUiProcessSearchRelationAction(entry, r, null, this) { DisplayType = isEnabled ? enumActionDisplayType.enabled : enumActionDisplayType.disabled, Description = isEnabled ? string.Empty : disabledReason, AlternativeDescription = isEnabled ? string.Empty : disabledReason } }; } ); ResultMenu.UpdateSearchRelations(lookup); ResultMenu.SetHeaderText(header, hideDetailsCheckbox: true); UpdatePendingInformationClasses(new HashSet()); } catch (Exception ex) { LogException(ex); CancledSearchAction(); } System.Diagnostics.Debug.WriteLine( $"[TicketOverview] Key={e.Key}, UseRoleScope={e.UseRoleScope}, Count={e.Count}"); } private string BuildHeaderText(string key, bool useRoleScope, int? count = null) { string scopeKey = useRoleScope ? "Searchbar.Header.Scope.Role" : "Searchbar.Header.Scope.Personal"; string entityKey = DetermineFallbackEntityKey(key); string filterKey = DetermineFallbackFilterKey(key); string scope = cMultiLanguageSupport.GetItem(scopeKey) ?? scopeKey; string entity = cMultiLanguageSupport.GetItem(entityKey) ?? entityKey; string filter = cMultiLanguageSupport.GetItem(filterKey) ?? filterKey; string template = cMultiLanguageSupport.GetItem("Searchbar.Header.Template") ?? "{0} - {1} ({2})"; bool expectsCount = template.IndexOf("{3}", StringComparison.Ordinal) >= 0; try { if (expectsCount) { int countValue = count ?? 0; return string.Format(template, entity, filter, scope, countValue); } return string.Format(template, entity, filter, scope); } catch (FormatException) { return string.Format("{0} - {1} ({2})", entity, filter, scope); } } private string DetermineFallbackEntityKey(string key) { if (string.IsNullOrWhiteSpace(key)) return "Searchbar.Header.Entity.Tickets"; var keyLower = key.ToLowerInvariant(); return keyLower.Contains("incident") ? "Searchbar.Header.Entity.Incidents" : "Searchbar.Header.Entity.Tickets"; } private string DetermineFallbackFilterKey(string key) { if (string.IsNullOrWhiteSpace(key)) return "Searchbar.Header.Filter.Overview"; var keyLower = key.ToLowerInvariant(); if (keyLower.Contains("newinfo")) return "Searchbar.Header.Filter.NewInfo"; if (keyLower.Contains("new")) return "Searchbar.Header.Filter.New"; if (keyLower.Contains("active")) return "Searchbar.Header.Filter.Active"; if (keyLower.Contains("critical")) return "Searchbar.Header.Filter.Critical"; if (keyLower.Contains("unassigned")) return "Searchbar.Header.Filter.Unassigned"; if (keyLower.Contains("hold") || keyLower.Contains("onhold") || keyLower.Contains("wait")) return "Searchbar.Header.Filter.OnHold"; if (keyLower.Contains("closed")) return "Searchbar.Header.Filter.Closed"; return "Searchbar.Header.Filter.Overview"; } private bool ShouldDisableTicketRelationForDemo(cF4sdApiSearchResultRelation relation) { if (relation == null) return false; var communication = cFasdCockpitCommunicationBase.Instance; if (communication?.IsDemo() != true) return false; if (relation.Type != enumF4sdSearchResultClass.Ticket) return false; if (relation.Infos == null) return false; if (!relation.Infos.TryGetValue(DemoTicketHasDetailsInfoKey, out var hasDetailsValue)) return false; if (bool.TryParse(hasDetailsValue, out var hasDetails)) return !hasDetails; return true; } private async Task> LoadRelationsForTileAsync(string key, bool useRoleScope, int count) { var communication = cFasdCockpitCommunicationBase.Instance; if (communication == null) return new List(); try { var relations = await communication.GetTicketOverviewRelations(key, useRoleScope, Math.Max(0, count)).ConfigureAwait(false); var list = relations?.ToList() ?? new List(); if (TicketOverviewUpdateService.Instance != null) foreach (var demoRelation in TicketOverviewUpdateService.Instance.GetDemoRelations(key, useRoleScope)) { if (!list.Any(r => r.id == demoRelation.id)) list.Add(demoRelation); } return list; } catch (Exception ex) { LogException(ex); return new List(); } } private void UpdateSearchResults(cFilteredResults filteredResults) { this.Dispatcher.Invoke(new Action(() => { try { _renderTicketOverviewUserNames = false; if (filteredResults?.Results is null) { SetSearchResultVisibility(false); SetTicketOverviewVisibility(true); return; } string menuHeaderText = filteredResults?.PreSelectedRelation?.DisplayName; ResultMenu.ShowSearchResults(filteredResults, menuHeaderText, this); preSelectedRelation = filteredResults.PreSelectedRelation; SetSearchResultVisibility(true); SetTicketOverviewVisibility(false); } catch (Exception E) { LogException(E); } })); } private void CancledSearchAction() { _renderTicketOverviewUserNames = false; SearchBarUc.Clear(); ResultMenu.ShowSearchResults(new cFilteredResults(), null, this); SetSearchResultVisibility(false); SetSearchHistoryVisibility(false); SetTicketOverviewVisibility(true); Visibility = Visibility.Hidden; } private void SlimPageWindowStateBar_ClickedClose(object sender, RoutedEventArgs e) { CancledSearchAction(); } private async void UiActionWasTriggered(object sender, UiActionEventArgs e) { try { if (e.UiAction == null) return; Mouse.OverrideCursor = Cursors.Wait; switch (e.UiAction) { case cUiProcessSearchResultAction searchResultAction: searchResultAction.PreSelectedSearchRelation = preSelectedRelation; break; case cUiProcessSearchRelationAction _: case cUiProcessSearchHistoryEntry _: break; default: var _t = e.UiAction?.GetType().Name; Debug.Assert(true, $"The UI action '{_t}' is not supported in detailed page"); CancledSearchAction(); return; } SetSearchResultVisibility(true); if (!await e.UiAction.RunUiActionAsync(sender, null, false, null)) CancledSearchAction(); } catch (Exception E) { LogException(E); } finally { Mouse.OverrideCursor = null; } } private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Up: if (ResultMenu.IndexOfSelectedResultItem == int.MinValue) ResultMenu.IndexOfSelectedResultItem = ResultMenu.GetSearchResultCount() - 1; else ResultMenu.IndexOfSelectedResultItem--; break; case Key.Down: if (ResultMenu.IndexOfSelectedResultItem == int.MinValue) ResultMenu.IndexOfSelectedResultItem = 0; else ResultMenu.IndexOfSelectedResultItem++; break; case Key.Enter: ResultMenu.SelectCurrentResultItem(); e.Handled = true; break; case Key.Escape: CancledSearchAction(); break; } } private void UiSettingsChanged(object sender, EventArgs e) { try { var positionAlignement = cFasdCockpitConfig.Instance.Global.SmallViewAlignment; switch (positionAlignement) { case enumF4sdHorizontalAlignment.Center: Dispatcher.Invoke(() => MainBorder.HorizontalAlignment = HorizontalAlignment.Center); break; case enumF4sdHorizontalAlignment.Right: Dispatcher.Invoke(() => MainBorder.HorizontalAlignment = HorizontalAlignment.Right); break; default: Dispatcher.Invoke(() => MainBorder.HorizontalAlignment = HorizontalAlignment.Left); break; } } catch (Exception E) { LogException(E); } } public void SetPendingInformationClasses(HashSet informationClasses) => Dispatcher.Invoke(() => ResultMenu.SetPendingInformationClasses(informationClasses)); public void UpdatePendingInformationClasses(HashSet informationClasses) => Dispatcher.Invoke(() => ResultMenu.UpdatePendingInformationClasses(informationClasses)); } }