// Copyright (c) 2023 schick Informatik // Description: Bremen calling main window // using BreCalClient.misc.Api; using BreCalClient.misc.Client; using BreCalClient.misc.Model; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using log4net; using static BreCalClient.Extensions; namespace BreCalClient { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { private ILog _log = LogManager.GetLogger(typeof(MainWindow)); private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30; #region Fields private Timer _timer; Credentials _credentials; private readonly Dictionary _allShipcallsDict = new(); private readonly Dictionary _allShipCallsControlDict = new(); private readonly List _visibleControlModels = new(); private List _ships = new(); private readonly ConcurrentDictionary _shipLookupDict = new(); private List _berths = new(); private readonly ConcurrentDictionary _berthLookupDict = new(); private List _participants = new(); private readonly Dictionary _participantLookupDict = new(); private readonly DefaultApi _api; private readonly CancellationTokenSource _tokenSource = new(); private LoginResult? _loginResult; private bool _refreshImmediately = false; private bool? _showCanceled = null; private Extensions.SortOrder? _sortOrder; // private bool _filterChanged = false; // private bool _sequenceChanged = false; #endregion #region Enums private enum ConnectionStatus { UNDEFINED, SUCCESSFUL, FAILED } #endregion #region Construction public MainWindow() { InitializeComponent(); _api = new DefaultApi(); _api.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; } #endregion #region event handler private void Window_Loaded(object sender, RoutedEventArgs e) { labelGeneralStatus.Text = $"Connection {ConnectionStatus.UNDEFINED}"; labelVersion.Text = "V. " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; if (!string.IsNullOrEmpty(Properties.Settings.Default.APP_TITLE)) this.Title = Properties.Settings.Default.APP_TITLE; searchFilterControl.SearchFilterChanged += SearchFilterControl_SearchFilterChanged; searchFilterControl.LogoImageClicked += () => { Process.Start("explorer", Properties.Settings.Default.LOGO_IMAGE_URL); }; this.comboBoxSortOrder.ItemsSource = Enum.GetValues(typeof(Extensions.SortOrder)); } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { // serialize filter settings Properties.Settings.Default.FilterCriteria = this.searchFilterControl.SearchFilter.Serialize(); Properties.Settings.Default.Save(); _tokenSource.Cancel(); } private async void buttonLogin_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(this.textPassword.Password) || string.IsNullOrEmpty(this.textUsername.Text)) { this.labelLoginResult.Content = Application.Current.FindResource("textUserNamePasswordEmpty").ToString(); return; } _credentials = new(username: textUsername.Text.Trim(), password: textPassword.Password.Trim()); try { _loginResult = await _api.LoginPostAsync(_credentials); if (_loginResult != null) { if (_loginResult.Id > 0) { this.busyIndicator.IsBusy = false; this._api.Configuration.ApiKey["Authorization"] = _loginResult.Token; this.LoadStaticLists(); this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}"; _timer = new Timer(RefreshToken, null, 4000000, Timeout.Infinite); } } labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}"; } catch (ApiException ex) { this.labelLoginResult.Content = ex.Message; labelGeneralStatus.Text = $"Connection {ConnectionStatus.FAILED}"; } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); labelGeneralStatus.Text = $"Connection {ConnectionStatus.FAILED}"; } } private void RefreshToken(object? state) { try { _loginResult = _api.LoginPost(_credentials); if (_loginResult != null) { if (_loginResult.Id > 0) { this._api.Configuration.ApiKey["Authorization"] = _loginResult.Token; } } else { _log.Error("Token refresh: Renewed login returned empty login result"); } } catch (Exception ex) { _log.ErrorFormat("Error refreshing token: {0}", ex.Message); } } private void buttonExit_Click(object sender, RoutedEventArgs e) { this.Close(); } private void buttonNew_Click(object sender, RoutedEventArgs e) { EditShipcallControl esc = new() { Participants = this._participants, Ships = this._ships, Berths = this._berths }; if (esc.ShowDialog() ?? false) { // create UI & save new dialog model if (esc.ShipcallModel.Shipcall != null) { this.UpdateUI(); this._api.ShipcallsPost(esc.ShipcallModel.Shipcall); // save new ship call this.AddShipcall(esc.ShipcallModel); _refreshImmediately = true; // set flag to avoid timer loop termination _tokenSource.Cancel(); // force timer loop end } } } private void buttonInfo_Click(object sender, RoutedEventArgs e) { AboutDialog ad = new(); ad.ChangePasswordRequested += async (oldPw, newPw) => { if (_loginResult != null) { UserDetails ud = new() { Id = _loginResult.Id, FirstName = _loginResult.FirstName, LastName = _loginResult.LastName, UserPhone = _loginResult.UserPhone, OldPassword = oldPw, NewPassword = newPw }; try { await _api.UserPutAsync(ud); MessageBox.Show(BreCalClient.Resources.Resources.textPasswordChanged, BreCalClient.Resources.Resources.textConfirmation, MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { this.Dispatcher.Invoke(new Action(() => { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); })); } } }; ad.ShowDialog(); } private void buttonClearFilter_Click(object sender, RoutedEventArgs e) { this.searchFilterControl.ClearFilters(); this.checkboxShowCancelledCalls.IsChecked = false; this.FilterShipcalls(); } private void SearchFilterControl_SearchFilterChanged() { this.FilterShipcalls(); this.UpdateUI(); } private void checkboxShowCancelledCalls_Checked(object sender, RoutedEventArgs e) { this._showCanceled = this.checkboxShowCancelledCalls.IsChecked; this.SearchFilterControl_SearchFilterChanged(); } private void comboBoxSortOrder_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { _sortOrder = (Extensions.SortOrder) this.comboBoxSortOrder.SelectedIndex; this.FilterShipcalls(); this.UpdateUI(); } #endregion #region network operations private async void LoadStaticLists() { this._berths = await _api.BerthsGetAsync(); foreach(var berth in this._berths) _berthLookupDict[berth.Id] = berth; this.searchFilterControl.SetBerths(this._berths); this._ships = await _api.ShipsGetAsync(); foreach(var ship in this._ships) _shipLookupDict[ship.Id] = ship; this._participants = await _api.ParticipantsGetAsync(); List agencies = new(); foreach (Participant participant in this._participants) { this._participantLookupDict[participant.Id] = participant; if (_loginResult?.ParticipantId == participant.Id) { App.Participant = participant; EnableControlsForParticipant(); } if(participant.IsTypeFlagSet(Extensions.ParticipantType.AGENCY)) agencies.Add(participant); } this.searchFilterControl.SetAgencies(agencies); if (!string.IsNullOrEmpty(Properties.Settings.Default.FilterCriteria)) { SearchFilterModel? sfm = SearchFilterModel.Deserialize(Properties.Settings.Default.FilterCriteria); if (sfm != null) this.searchFilterControl.SetFilterFromModel(sfm); } _ = Task.Run(() => RefreshShipcalls()); } public async Task RefreshShipcalls() { while (!_tokenSource.Token.IsCancellationRequested || _refreshImmediately) { _refreshImmediately = false; List? shipcalls = null; try { shipcalls = await _api.ShipcallsGetAsync(); this.Dispatcher.Invoke(new Action(() => { labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}"; labelGeneralStatus.Text = $"Ok"; })); } catch (Exception ex) { this.Dispatcher.Invoke(new Action(() => { labelGeneralStatus.Text = $"Connection {ConnectionStatus.FAILED}"; labelStatusBar.Text = ex.Message; })); } if (shipcalls != null) { foreach (Shipcall shipcall in shipcalls) { // load times for each shipcall List currentTimes = await _api.TimesGetAsync(shipcall.Id); if(!_allShipcallsDict.ContainsKey(shipcall.Id)) { // add entry ShipcallControlModel scm = new() { Shipcall = shipcall, Times = currentTimes }; this.AddShipcall(scm); } else { // update entry _allShipcallsDict[shipcall.Id].Shipcall = shipcall; _allShipcallsDict[shipcall.Id].Times = currentTimes; this.UpdateShipcall(_allShipcallsDict[shipcall.Id]); } } List existingIds = new(this._allShipcallsDict.Keys); foreach (int existingId in existingIds) { if (shipcalls.Find(s => s.Id == existingId) == null) // the model is no longer in the search result { this.RemoveShipcall(existingId); } } this.FilterShipcalls(); this.UpdateUI(); } try { await Task.Delay(TimeSpan.FromSeconds(SHIPCALL_UPDATE_INTERVAL_SECONDS), _tokenSource.Token); } catch(TaskCanceledException) { } } } #endregion #region basic operations private void AddShipcall(ShipcallControlModel scm) { if (scm.Shipcall == null) return; _allShipcallsDict[scm.Shipcall.Id] = scm; Shipcall shipcall = scm.Shipcall; if (this._shipLookupDict.ContainsKey(shipcall.ShipId)) scm.Ship = this._shipLookupDict[shipcall.ShipId]; if (this._berthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0)) scm.Berth = this._berthLookupDict[shipcall.ArrivalBerthId ?? 0].Name; scm.AssignParticipants(this._participants); this.Dispatcher.Invoke(() => { ShipcallControl sc = new() { Height = 120, ShipcallControlModel = scm, ParticipantDict = _participantLookupDict, Berths = _berths }; sc.EditTimesRequested += Sc_EditTimesRequested; sc.EditRequested += Sc_EditRequested; sc.RefreshData(); this._allShipCallsControlDict[scm.Shipcall.Id] = sc; }); } private void UpdateShipcall(ShipcallControlModel scm) { if(scm.Shipcall == null) return; Shipcall shipcall = scm.Shipcall; if (this._shipLookupDict.ContainsKey(shipcall.ShipId)) scm.Ship = this._shipLookupDict[shipcall.ShipId]; if (this._berthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0)) scm.Berth = this._berthLookupDict[shipcall.ArrivalBerthId ?? 0].Name; scm.AssignParticipants(this._participants); } private void RemoveShipcall(int shipcallId) { this.Dispatcher.Invoke(() => { this.stackPanel.Children.Remove(this._allShipCallsControlDict[shipcallId]); }); ShipcallControlModel removeModel = this._allShipcallsDict[shipcallId]; _visibleControlModels.Remove(removeModel); this._allShipCallsControlDict.Remove(shipcallId); this._allShipcallsDict.Remove(shipcallId); } private void FilterShipcalls() { SearchFilterModel sfm = this.searchFilterControl.SearchFilter; this._visibleControlModels.Clear(); // first add everything this._visibleControlModels.AddRange(_allShipcallsDict.Values); // now remove elements whose filter criteria are met if(sfm.Berths.Count > 0 ) { this._visibleControlModels.RemoveAll(x => !sfm.Berths.Contains((x.Shipcall?.ArrivalBerthId) ?? -1)); } if(sfm.Agencies.Count > 0 ) { this._visibleControlModels.RemoveAll(x => !sfm.Agencies.Contains((x.GetParticipantIdForType(Extensions.ParticipantType.AGENCY)) ?? -1)); } if(sfm.Categories.Count > 0 ) { this._visibleControlModels.RemoveAll(x => !sfm.Categories.Contains((x.Shipcall?.Type) ?? -1)); } if(!string.IsNullOrEmpty(sfm.SearchString)) { this._visibleControlModels.RemoveAll(x => !(x.ContainsRemarkText(sfm.SearchString) || (x.Ship?.Name.Contains(sfm.SearchString, StringComparison.InvariantCultureIgnoreCase) ?? false))); } if(sfm.ShipLengthTo != null) { this._visibleControlModels.RemoveAll(x => x.Ship?.Length > sfm.ShipLengthTo); } if(sfm.ShipLengthFrom != null) { this._visibleControlModels.RemoveAll(x => x.Ship?.Length < sfm.ShipLengthFrom); } if(sfm.EtaFrom != null) { this._visibleControlModels.RemoveAll(x => x.Shipcall?.Eta < sfm.EtaFrom); } if(sfm.EtaTo != null) { this._visibleControlModels.RemoveAll(x => x.Shipcall?.Eta > sfm.EtaTo); } if(!_showCanceled ?? true) // canceled calls are filtered by default { this._visibleControlModels.RemoveAll(x => x.Shipcall?.Canceled ?? false); } if (this._sortOrder != null) { switch(this._sortOrder) { case Extensions.SortOrder.SHIP_NAME: this._visibleControlModels.Sort((x, y) => { if (x.Ship == null) return 0; if (y.Ship == null) return 0; return x.Ship.Name.CompareTo(y.Ship.Name); }); break; case Extensions.SortOrder.MODIFIED: this._visibleControlModels.Sort((x, y) => { if (x.Shipcall == null) return 0; if (y.Shipcall == null) return 0; return DateTime.Compare(x.Shipcall.Modified ?? x.Shipcall.Created, y.Shipcall.Modified ?? x.Shipcall.Created); }); break; case Extensions.SortOrder.ETA_ETD: this._visibleControlModels.Sort((x, y) => { if (x.Shipcall == null) return 0; if (y.Shipcall == null) return 0; DateTime xDate = (x.Shipcall.Type == (int) Extensions.TypeEnum.Incoming) ? x.Shipcall.Eta : x.Shipcall.Etd ?? x.Shipcall.Eta; DateTime yDate = (y.Shipcall.Type == (int) Extensions.TypeEnum.Incoming) ? y.Shipcall.Eta : y.Shipcall.Etd ?? y.Shipcall.Eta; return DateTime.Compare(xDate, yDate); }); break; default: break; } } } #endregion private void UpdateUI() { this.Dispatcher.Invoke(new Action(() => { this.stackPanel.Children.Clear(); foreach(ShipcallControlModel visibleModel in this._visibleControlModels) { if (visibleModel.Shipcall == null) continue; // should not happen if(this._allShipCallsControlDict.ContainsKey(visibleModel.Shipcall.Id)) { this._allShipCallsControlDict[visibleModel.Shipcall.Id].RefreshData(); this.stackPanel.Children.Add(this._allShipCallsControlDict[visibleModel.Shipcall.Id]); } } })); } #region control event handler private async void Sc_EditRequested(ShipcallControl obj) { if (obj.ShipcallControlModel != null) { EditShipcallControl esc = new() { ShipcallModel = obj.ShipcallControlModel, Ships = _ships, Participants = _participants, Berths = _berths }; if(esc.ShowDialog() ?? false) { try { await _api.ShipcallsPutAsync(obj.ShipcallControlModel.Shipcall); obj.RefreshData(); _refreshImmediately = true; _tokenSource.Cancel(); } catch (Exception ex) { ShowErrorDialog(ex.Message, "Error saving edited shipcall"); } } } } private async void Sc_EditTimesRequested(ShipcallControl obj, Times? times, Extensions.ParticipantType participantType) { // show a dialog that lets the user create / update times for the given shipcall IEditTimesControl etc = (participantType == ParticipantType.TERMINAL) ? new EditTimesTerminalControl() : new EditTimesControl(); if (etc is EditTimesTerminalControl ettc) ettc.Berths = this._berths; bool wasEdit = false; if (times != null) { etc.Times = times; wasEdit = true; } // actually we should only do this on create but we have existing data etc.Times.ParticipantType = (int) participantType; if(etc.ShowDialog() ?? false) { try { if (wasEdit) { await _api.TimesPutAsync(etc.Times); } else { etc.Times.ParticipantId = App.Participant.Id; if ((obj.ShipcallControlModel != null) && (obj.ShipcallControlModel.Shipcall != null)) { etc.Times.ShipcallId = obj.ShipcallControlModel.Shipcall.Id; } await _api.TimesPostAsync(etc.Times); obj.ShipcallControlModel?.Times.Add(etc.Times); } _refreshImmediately = true; _tokenSource.Cancel(); } catch (Exception ex) { ShowErrorDialog(ex.Message, "Error saving times"); } } } #endregion #region helper private void ShowErrorDialog(string message, string caption) { Dispatcher.Invoke(new Action(() => { MessageBox.Show(message, caption, MessageBoxButton.OK, MessageBoxImage.Error); })); } private void EnableControlsForParticipant() { if (App.Participant.IsTypeFlagSet(Extensions.ParticipantType.BSMD)) this.buttonNew.Visibility = Visibility.Visible; } #endregion } }