// Copyright (c) 2023 schick Informatik // Description: Bremen calling main window // using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Windows; using log4net; using BreCalClient.misc.Api; using BreCalClient.misc.Client; using BreCalClient.misc.Model; using static BreCalClient.Extensions; using System.Collections.Concurrent; using Newtonsoft.Json; using System.Security.Principal; namespace BreCalClient { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { private readonly ILog _log = LogManager.GetLogger(typeof(MainWindow)); private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30; #region Fields private static Int32 _uiUpdateRunning = 0; private Timer? _timer; private Credentials? _credentials; private readonly ConcurrentDictionary _allShipcallsDict = new(); private readonly ConcurrentDictionary _allShipCallsControlDict = new(); private readonly List _visibleControlModels = new(); private readonly DefaultApi _api; private CancellationTokenSource _tokenSource = new(); private LoginResult? _loginResult; private bool _refreshImmediately = false; private bool? _showCanceled = null; private SortOrder _sortOrder = SortOrder.ETA_ETD; private int searchPastDays = 0; // 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(Properties.Settings.Default.API_URL); _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)); this.comboBoxSortOrder.SelectedIndex = (int)_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) { if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith("{"))) { Error? anError = JsonConvert.DeserializeObject((string)ex.ErrorContent); this.labelLoginResult.Content = anError?.Message ?? ex.Message; } else { 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(); if (esc.ShowDialog() ?? false) { // create UI & save new dialog model if (esc.ShipcallModel.Shipcall != null) { this.UpdateUI(); esc.ShipcallModel.Shipcall?.Participants.Clear(); foreach (ParticipantAssignment pa in esc.ShipcallModel.AssignedParticipants.Values) esc.ShipcallModel.Shipcall?.Participants.Add(pa); try { this._api.ShipcallsPost(esc.ShipcallModel.Shipcall); // save new ship call this.AddShipcall(esc.ShipcallModel); } catch(Exception ex) { this.ShowErrorDialog(ex.ToString(), ex.Message); } _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.LoginResult = this._loginResult; ad.ChangePasswordRequested += async (oldPw, newPw) => { if (_loginResult != null) { UserDetails ud = new() { Id = _loginResult.Id, FirstName = _loginResult.FirstName, LastName = _loginResult.LastName, UserPhone = _loginResult.UserPhone, UserEmail = _loginResult.UserEmail, 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() { BreCalLists.InitializeBerths(await _api.BerthsGetAsync()); BreCalLists.InitializeShips(await _api.ShipsGetAsync()); BreCalLists.InitializeParticipants(await _api.ParticipantsGetAsync()); this.searchFilterControl.SetBerths(BreCalLists.Berths); foreach (Participant participant in BreCalLists.Participants) { if (_loginResult?.ParticipantId == participant.Id) { App.Participant = participant; EnableControlsForParticipant(); } } this.searchFilterControl.SetAgencies(BreCalLists.Participants_Agent); 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) { if (_refreshImmediately) { _refreshImmediately = false; _tokenSource = new CancellationTokenSource(); } List? shipcalls = null; try { if(this.searchPastDays != 0) shipcalls = await _api.ShipcallsGetAsync(this.searchPastDays); else 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 (ex.Message.Contains("access", StringComparison.OrdinalIgnoreCase)) { this.RefreshToken(null); } } 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; 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 (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId)) scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId]; if (shipcall.Type == 1) { if (BreCalLists.BerthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0)) scm.Berth = BreCalLists.BerthLookupDict[shipcall.ArrivalBerthId ?? 0].Name; } else { if (BreCalLists.BerthLookupDict.ContainsKey(shipcall.DepartureBerthId ?? 0)) scm.Berth = BreCalLists.BerthLookupDict[shipcall.DepartureBerthId ?? 0].Name; } scm.AssignParticipants(); this.Dispatcher.Invoke(() => { ShipcallControl sc = new() { Height = 120, ShipcallControlModel = scm }; sc.EditTimesRequested += Sc_EditTimesRequested; sc.EditRequested += Sc_EditRequested; sc.EditAgencyRequested += Sc_EditAgencyRequested; sc.RefreshData(); this._allShipCallsControlDict[scm.Shipcall.Id] = sc; }); } private static void UpdateShipcall(ShipcallControlModel scm) { if(scm.Shipcall == null) return; Shipcall shipcall = scm.Shipcall; if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId)) scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId]; if (shipcall.Type == 1) { if (BreCalLists.BerthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0)) scm.Berth = BreCalLists.BerthLookupDict[shipcall.ArrivalBerthId ?? 0].Name; } else { if (BreCalLists.BerthLookupDict.ContainsKey(shipcall.DepartureBerthId ?? 0)) scm.Berth = BreCalLists.BerthLookupDict[shipcall.DepartureBerthId ?? 0].Name; } scm.AssignParticipants(); } 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, out _); this._allShipcallsDict.Remove(shipcallId, out _); } private void FilterShipcalls() { SearchFilterModel sfm = this.searchFilterControl.SearchFilter; if( sfm.EtaFrom.HasValue && sfm.EtaFrom < DateTime.Now.AddDays(-2)) { int daysInThePast = (int)Math.Ceiling((DateTime.Now - sfm.EtaFrom.Value).TotalDays); if (this.searchPastDays != daysInThePast) { this.searchPastDays = daysInThePast; _refreshImmediately = true; // set flag to avoid timer loop termination _tokenSource.Cancel(); // force timer loop end } } else { searchPastDays = 0; } 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) => { Participant? agency = x.GetParticipantForType(ParticipantType.AGENCY); if(agency != null) { return !sfm.Agencies.Contains(agency.Id); } return true; }); } 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); } 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.Eta ?? DateTime.Now : x.Etd ?? DateTime.Now; DateTime yDate = (y.Shipcall.Type == (int) Extensions.TypeEnum.Incoming) ? y.Eta ?? DateTime.Now : y.Etd ?? DateTime.Now; return DateTime.Compare(xDate, yDate); }); break; default: break; } } #endregion #region UpdateUI func private void UpdateUI() { this.Dispatcher.Invoke(new Action(() => { if (Interlocked.CompareExchange(ref _uiUpdateRunning, 1, 0) == 1) return; try { 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]); } } } catch(Exception e) { _log.ErrorFormat("Exception running ui update: {0}", e.ToString()); } finally { _uiUpdateRunning = 0; } })); } #endregion #region control event handler private async void Sc_EditRequested(ShipcallControl obj) { if (obj.ShipcallControlModel != null) { EditShipcallControl esc = new() { ShipcallModel = obj.ShipcallControlModel }; if(esc.ShowDialog() ?? false) { try { obj.ShipcallControlModel.Shipcall?.Participants.Clear(); obj.ShipcallControlModel.UpdateTimesAssignments(this._api); foreach(ParticipantAssignment pa in obj.ShipcallControlModel.AssignedParticipants.Values) obj.ShipcallControlModel.Shipcall?.Participants.Add(pa); 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) { if( obj.ShipcallControlModel == null) { return; } if (!obj.ShipcallControlModel.AssignedParticipants.ContainsKey(participantType)) return; // no assigment means no dialog my friend // show a dialog that lets the user create / update times for the given shipcall IEditTimesControl etc = (participantType == ParticipantType.TERMINAL) ? new EditTimesTerminalControl() : new EditTimesControl(); etc.Title = obj.ShipcallControlModel.Title; if(obj.ShipcallControlModel.Shipcall != null) etc.CallType = (TypeEnum) obj.ShipcallControlModel.Shipcall.Type; bool wasEdit = false; if (times != null) { etc.Times = times; wasEdit = true; } else { if(obj.ShipcallControlModel.AssignedParticipants[participantType].ParticipantId == App.Participant.Id) { etc.Times.ParticipantId = App.Participant.Id; // this is my record, so the Participant Id is set that allows editing } } // 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; } Id apiResultId = await _api.TimesPostAsync(etc.Times); etc.Times.Id = apiResultId.VarId; obj.ShipcallControlModel?.Times.Add(etc.Times); } _refreshImmediately = true; _tokenSource.Cancel(); } catch (Exception ex) { ShowErrorDialog(ex.Message, "Error saving times"); } } } private async void Sc_EditAgencyRequested(ShipcallControl sc, Times? times) { IEditShipcallTimesControl? editControl = null; switch(sc.ShipcallControlModel?.Shipcall?.Type) { case (int)TypeEnum.Incoming: editControl = new EditTimesAgencyIncomingControl(); break; case (int)TypeEnum.Outgoing: editControl = new EditTimesAgencyOutgoingControl(); break; case (int)TypeEnum.Shifting: editControl = new EditTimesAgencyShiftingControl(); break; } if (editControl != null) { editControl.ShipcallModel = sc.ShipcallControlModel ?? new ShipcallControlModel(); bool wasEdit = false; if (times != null) { editControl.Times = times; wasEdit = true; } else { if(editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY)) editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId; } editControl.Times.ParticipantType = (int)ParticipantType.AGENCY; if(editControl.ShowDialog() ?? false) { try { sc.ShipcallControlModel?.UpdateTimesAssignments(_api); // if the agent changed the assignment of the participant to another // always try to be the agent, even if we are BSMD if (editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY)) { editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId; } else { editControl.Times.ParticipantId = App.Participant.Id; } if (wasEdit) { await _api.TimesPutAsync(editControl.Times); } else { if ((sc.ShipcallControlModel != null) && (sc.ShipcallControlModel.Shipcall != null)) { editControl.Times.ShipcallId = sc.ShipcallControlModel.Shipcall.Id; } Id resultAPI_Id = await _api.TimesPostAsync(editControl.Times); editControl.Times.Id = resultAPI_Id.VarId; sc.ShipcallControlModel?.Times.Add(editControl.Times); } editControl.ShipcallModel.Shipcall?.Participants.Clear(); foreach (ParticipantAssignment pa in editControl.ShipcallModel.AssignedParticipants.Values) editControl.ShipcallModel.Shipcall?.Participants.Add(pa); await _api.ShipcallsPutAsync(editControl.ShipcallModel.Shipcall); _refreshImmediately = true; _tokenSource.Cancel(); } catch(Exception ex) { ShowErrorDialog(ex.Message, "Error saving agency information"); } } } } #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 } }