// 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 Polly; using System.Net.Http; using System.Net; using System.Windows.Input; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; using System.Linq; namespace BreCalClient { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { private readonly ILog _log = LogManager.GetLogger(typeof(MainWindow)); private readonly ToastViewModel _vm; private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30; private const int SHIPS_UPDATE_INTERVAL_SECONDS = 120; private const int CHECK_NOTIFICATIONS_INTERVAL_SECONDS = 5; private const int PROGRESS_STEPS = 50; #region Fields //private static int _uiUpdateRunning = 0; private static readonly SemaphoreSlim uiLock = new(1); private Credentials? _credentials; private readonly ConcurrentDictionary _allShipcallsDict = new(); private readonly ConcurrentDictionary _allShipCallsControlDict = new(); private readonly List _visibleControlModels = new(); private readonly ShipcallApi _shipcallApi; private readonly UserApi _userApi; private readonly TimesApi _timesApi; private readonly StaticApi _staticApi; private readonly ShipApi _shipApi; 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; private HistoryDialog? _historyDialog; #endregion #region Enums private enum ConnectionStatus { UNDEFINED, SUCCESSFUL, FAILED } #endregion #region Construction public MainWindow() { InitializeComponent(); _userApi = new UserApi(Properties.Settings.Default.API_URL); _userApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; _shipcallApi = new ShipcallApi(Properties.Settings.Default.API_URL); _shipcallApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; _timesApi = new TimesApi(Properties.Settings.Default.API_URL); _timesApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; _staticApi = new StaticApi(Properties.Settings.Default.API_URL); _staticApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; _shipApi = new ShipApi(Properties.Settings.Default.API_URL); _shipApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer"; const int maxDelayInMilliseconds = 32 * 1000; var jitterer = new Random(); var retryPolicy = // Policy.Handle() Policy.HandleResult(resp => resp.StatusCode == HttpStatusCode.Unauthorized) //.OrResult .WaitAndRetryAsync(1, retryAttempt => { var calculatedDelayInMilliseconds = Math.Pow(2, retryAttempt) * 1000; var jitterInMilliseconds = jitterer.Next(0, 1000); var actualDelay = Math.Min(calculatedDelayInMilliseconds + jitterInMilliseconds, maxDelayInMilliseconds); return TimeSpan.FromMilliseconds(actualDelay); }, onRetry: (resp, timespan, context) => { this.RefreshToken(); Trace.WriteLine("token refreshed"); }); RetryConfiguration.AsyncRetryPolicy = retryPolicy; this.generalProgressStatus.Maximum = PROGRESS_STEPS; _vm = new ToastViewModel(); this.Unloaded += MainWindow_Unloaded; } private void MainWindow_Unloaded(object sender, RoutedEventArgs e) { _vm.OnUnloaded(); } #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.FilterCriteriaMap = SearchFilterModel.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 = BreCalClient.Resources.Resources.textUserNamePasswordEmpty; return; } _credentials = new(username: textUsername.Text.Trim(), password: textPassword.Password.Trim()); try { _loginResult = await _userApi.LoginAsync(_credentials); if (_loginResult != null) { if (_loginResult.Id > 0) { Mouse.OverrideCursor = Cursors.Wait; this.busyIndicator.IsBusy = false; this._userApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._shipcallApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._timesApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._staticApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._shipApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this.LoadStaticLists(); this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}"; } } labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}"; } catch (ApiException ex) { if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith("{"))) { Error? anError = JsonConvert.DeserializeObject((string)ex.ErrorContent); if ((anError != null) && anError.ErrorField.Equals("invalid credentials")) this.labelLoginResult.Content = BreCalClient.Resources.Resources.textWrongCredentials; else this.labelLoginResult.Content = anError?.ErrorField ?? 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 bool RefreshToken() { bool result = false; try { _loginResult = _userApi.Login(_credentials); if (_loginResult != null) { if (_loginResult.Id > 0) { this._userApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._timesApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._shipcallApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._staticApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; this._shipApi.Configuration.ApiKey["Authorization"] = _loginResult.Token; result = true; } } else { _log.Error("Token refresh: Renewed login returned empty login result"); } } catch (Exception ex) { _log.ErrorFormat("Error refreshing token: {0}", ex.Message); } return result; } private void buttonExit_Click(object sender, RoutedEventArgs e) { this.Close(); } private void buttonNew_Click(object sender, RoutedEventArgs e) { NewWithModel(null); } private async void NewWithModel(ShipcallControlModel? model) { EditShipcallControl esc = new() { ShipEditingEnabled = App.Participant.IsTypeFlagSet(Extensions.ParticipantType.BSMD), ShipApi = _shipApi, IsCreate = true }; if (model != null) esc.ShipcallModel = model; if (esc.ShowDialog() ?? false) { // create UI & save new dialog model if (esc.ShipcallModel.Shipcall != null) { await uiLock.WaitAsync(); this.UpdateUI(); uiLock.Release(); esc.ShipcallModel.Shipcall?.Participants.Clear(); foreach (ParticipantAssignment pa in esc.ShipcallModel.AssignedParticipants.Values) esc.ShipcallModel.Shipcall?.Participants.Add(pa); try { this._shipcallApi.ShipcallCreate(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 // if this was an arrival, create the matching departure call and open it if (esc.ShipcallModel.Shipcall?.Type == ShipcallType.Arrival) { ShipcallControlModel scmOut = new() { Shipcall = new() { Type = ShipcallType.Departure } }; scmOut.Shipcall.ShipId = esc.ShipcallModel.Shipcall.ShipId; scmOut.Shipcall.PortId = esc.ShipcallModel.Shipcall.PortId; scmOut.Ship = esc.ShipcallModel.Ship; scmOut.AllowPortChange = false; DateTime eta = esc.ShipcallModel.Shipcall?.Eta ?? DateTime.Now; scmOut.Shipcall.Etd = eta.AddDays(2); scmOut.Shipcall.DepartureBerthId = esc.ShipcallModel.Shipcall?.ArrivalBerthId; if (esc.ShipcallModel.Shipcall != null) { scmOut.Shipcall.Participants = new(); scmOut.Shipcall.Participants.AddRange(esc.ShipcallModel.Shipcall.Participants); foreach(ParticipantType pType in esc.ShipcallModel.AssignedParticipants.Keys) scmOut.AssignedParticipants[pType] = esc.ShipcallModel.AssignedParticipants[pType]; } this.Dispatcher.Invoke(() => { NewWithModel(scmOut); }); } } } } private void buttonInfo_Click(object sender, RoutedEventArgs e) { AboutDialog ad = new() { LoginResult = this._loginResult }; ad.ChangePasswordRequested += async (oldPw, newPw) => { if (_loginResult != null) { UserDetails ud = new() { Id = _loginResult.Id, OldPassword = oldPw, NewPassword = newPw }; try { await _userApi.UserUpdateAsync(ud); MessageBox.Show(BreCalClient.Resources.Resources.textPasswordChanged, BreCalClient.Resources.Resources.textConfirmation, MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { this.Dispatcher.Invoke(new Action(() => { ShowErrorDialog(ex.Message, "Error saving user information"); })); } } }; ad.ChangeUserSettingsRequested += async () => { if (_loginResult != null) { UserDetails ud = new() { Id = _loginResult.Id, FirstName = _loginResult.FirstName, LastName = _loginResult.LastName, UserPhone = _loginResult.UserPhone, UserEmail = _loginResult.UserEmail, NotifyEmail = _loginResult.NotifyEmail, NotifyPopup = _loginResult.NotifyPopup, NotifySignal = _loginResult.NotifySignal, NotifyWhatsapp = _loginResult.NotifyWhatsapp }; try { await _userApi.UserUpdateAsync(ud); MessageBox.Show(BreCalClient.Resources.Resources.textInformationUpdated, BreCalClient.Resources.Resources.textConfirmation, MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { this.Dispatcher.Invoke(new Action(() => { ShowErrorDialog(ex.Message, "Error saving user information"); })); } } }; ad.ShowDialog(); } private void buttonClearFilter_Click(object sender, RoutedEventArgs e) { this.searchFilterControl.ClearFilters(); this.checkboxShowCancelledCalls.IsChecked = false; this.comboBoxPorts.UnSelectAll(); this.FilterShipcalls(); } private async void SearchFilterControl_SearchFilterChanged() { this.FilterShipcalls(); await uiLock.WaitAsync(); this.UpdateUI(); uiLock.Release(); } private void checkboxShowCancelledCalls_Checked(object sender, RoutedEventArgs e) { this._showCanceled = this.checkboxShowCancelledCalls.IsChecked; this.SearchFilterControl_SearchFilterChanged(); } private void comboBoxPorts_ItemSelectionChanged(object sender, Xceed.Wpf.Toolkit.Primitives.ItemSelectionChangedEventArgs e) { this.searchFilterControl.SearchFilter.Ports.Clear(); List berths = new(); foreach (Port port in comboBoxPorts.SelectedItems) { this.searchFilterControl.SearchFilter.Ports.Add(port.Id); berths.AddRange(BreCalLists.GetBerthsByPort(port.Id)); } // create list of berths from selected port(s) or return all berths if (berths.Count == 0) berths = BreCalLists.AllBerths; this.searchFilterControl.SetBerths(berths); this.SearchFilterControl_SearchFilterChanged(); } private async void comboBoxSortOrder_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { _sortOrder = (Extensions.SortOrder) this.comboBoxSortOrder.SelectedIndex; this.FilterShipcalls(); await uiLock.WaitAsync(); this.UpdateUI(); uiLock.Release(); } private void buttonHistory_Click(object sender, RoutedEventArgs e) { if(_historyDialog == null) { _historyDialog = new HistoryDialog(_allShipcallsDict, _staticApi); _historyDialog.Closed += (sender, e) => { this._historyDialog = null; }; _historyDialog.HistoryItemSelected += (x) => { if(_allShipCallsControlDict.ContainsKey(x)) _allShipCallsControlDict[x].BringIntoView(); }; _historyDialog.Show(); } else { _historyDialog.Activate(); } } private void buttonManualRefresh_Click(object sender, RoutedEventArgs e) { _refreshImmediately = true; // set flag to avoid timer loop termination _tokenSource.Cancel(); // force timer loop end Mouse.OverrideCursor = Cursors.Wait; } #endregion #region network operations private async void LoadStaticLists() { if (_loginResult == null) return; BreCalLists.InitializePorts(await _staticApi.GetPortsAsync()); BreCalLists.InitializeBerths(await _staticApi.BerthsGetAsync()); BreCalLists.InitializeShips(await _shipApi.ShipsGetAsync()); BreCalLists.InitializeParticipants(await _staticApi.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.FilterCriteriaMap)) { SearchFilterModel.Deserialize(Properties.Settings.Default.FilterCriteriaMap); SearchFilterModel? currentFilter = null; if (SearchFilterModel.filterMap != null) { if((_loginResult != null) && SearchFilterModel.filterMap.ContainsKey(_loginResult.Id)) { currentFilter = SearchFilterModel.filterMap[_loginResult.Id]; } } else { SearchFilterModel.filterMap = new(); } if (currentFilter == null) { currentFilter = new(); if(_loginResult != null) SearchFilterModel.filterMap[_loginResult.Id] = currentFilter; } this.searchFilterControl.SetFilterFromModel(currentFilter); if (currentFilter.Ports != null) { foreach (Port p in this.comboBoxPorts.ItemsSource) { if (currentFilter.Ports.Contains(p.Id)) this.comboBoxPorts.SelectedItems.Add(p); } } } _ = Task.Run(() => RefreshShipcalls()); _ = Task.Run(() => RefreshShips()); _ = Task.Run(() => CheckNotifications()); } public async Task RefreshShips() { while (true) { Thread.Sleep(SHIPS_UPDATE_INTERVAL_SECONDS * 1000); BreCalLists.InitializeShips(await _shipApi.ShipsGetAsync()); } } 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 _shipcallApi.ShipcallsGetAsync(this.searchPastDays); else shipcalls = await _shipcallApi.ShipcallsGetAsync(); this.Dispatcher.Invoke(new Action(() => { labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}"; labelLatestUpdate.Text = $"Last update: {DateTime.Now.ToLongTimeString()}"; labelStatusBar.Text = ""; generalProgressStatus.Value = 0; })); } 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(); } } try { if (shipcalls != null) { foreach (Shipcall shipcall in shipcalls) { // load times for each shipcall List currentTimes = await _timesApi.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(); await uiLock.WaitAsync(); this.UpdateUI(); } } catch(Exception ex) { _log.Error(ex); } finally { uiLock.Release(); } try { double interval = (double) SHIPCALL_UPDATE_INTERVAL_SECONDS / PROGRESS_STEPS; for (int i = 0; i < PROGRESS_STEPS; i++) { await Task.Delay(TimeSpan.FromSeconds(interval), _tokenSource.Token); this.Dispatcher.Invoke(new Action(() => { this.generalProgressStatus.Value = i; })); } } catch(TaskCanceledException) { } } } public async Task CheckNotifications() { while (true) { Thread.Sleep(CHECK_NOTIFICATIONS_INTERVAL_SECONDS * 1000); List notifications = await _staticApi.NotificationsGetAsync(); AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm); } } #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].Ship; if (shipcall.Type == ShipcallType.Arrival) { 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 = 145, 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].Ship; if (shipcall.Type == ShipcallType.Arrival) { 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; if (sfm.EtaFrom == null) sfm.EtaFrom = DateTime.Now.AddDays(-2); } 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) && (x.Shipcall?.Type == ShipcallType.Arrival)) || (!sfm.Berths.Contains((x.Shipcall?.DepartureBerthId) ?? -1) && (x.Shipcall?.Type != ShipcallType.Arrival))); } 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 => { if (x.Shipcall == null) return false; else return !sfm.Categories.Contains(x.Shipcall.Type); }); } if(sfm.Ports.Count > 0 ) { _ = this._visibleControlModels.RemoveAll(x => { if(x.Shipcall == null) return false; else return !sfm.Ports.Contains(x.Shipcall.PortId); }); } 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 => { Times? t = x.GetTimesForParticipantType(ParticipantType.AGENCY); switch (x.Shipcall?.Type) { case ShipcallType.Arrival: { if ((t != null) && t.EtaBerth.HasValue) return t.EtaBerth.Value < sfm.EtaFrom; return x.Shipcall?.Eta < sfm.EtaFrom; } default: // Shifting / Departing { if ((t != null) && t.EtdBerth.HasValue) return t.EtdBerth.Value < sfm.EtaFrom; return x.Shipcall?.Etd < sfm.EtaFrom; } } }); } if(sfm.EtaTo != null) { _ = this._visibleControlModels.RemoveAll(x => { Times? t = x.GetTimesForParticipantType(ParticipantType.AGENCY); DateTime refValue = sfm.EtaTo.Value.AddMinutes(1440); // including 23:59 switch (x.Shipcall?.Type) { case ShipcallType.Arrival: { if ((t != null) && t.EtaBerth.HasValue) return t.EtaBerth.Value > refValue; return x.Shipcall?.Eta > refValue; } default: // Shifting / Departing { if ((t != null) && t.EtdBerth.HasValue) return t.EtdBerth.Value > refValue; return x.Shipcall?.Etd > refValue; } } }); } if(sfm.MineOnly ?? false) { _ = _visibleControlModels.RemoveAll(x => { bool contained = false; foreach(ParticipantAssignment p in x.AssignedParticipants.Values) { if(p.ParticipantId.Equals(App.Participant.Id)) { contained = true; break; } } return !contained; }); } if(!_showCanceled ?? true) // canceled calls are filtered by default { _ = this._visibleControlModels.RemoveAll(x => x.Shipcall?.Canceled ?? false); } // special Basti case: Wenn das ATA / ATD eingetragen ist und schon 2 Stunden in der Vergangenheit liegt if (searchPastDays <= 3) { _ = this._visibleControlModels.RemoveAll(x => { Times? mooringTimes = x.GetTimesForParticipantType(ParticipantType.MOORING); if (mooringTimes != null) { switch (x.Shipcall?.Type) { case ShipcallType.Arrival: if (mooringTimes.Ata.HasValue && ((DateTime.Now - mooringTimes.Ata.Value).TotalHours > 2)) return true; break; default: if (mooringTimes.Atd.HasValue && ((DateTime.Now - mooringTimes.Atd.Value).TotalHours > 2)) return true; break; } } return 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 now = DateTime.Now; DateTime xDate = (x.Shipcall.Type == ShipcallType.Arrival) ? (x.Eta ?? now) : (x.Etd ?? now); Times? xTimes = x.GetTimesForParticipantType(ParticipantType.AGENCY); if(xTimes != null) xDate = (x.Shipcall.Type == ShipcallType.Arrival) ? (xTimes.EtaBerth ?? now) : (xTimes.EtdBerth ?? now); DateTime yDate = (y.Shipcall.Type == ShipcallType.Arrival) ? (y.Eta ?? now) : (y.Etd ?? now); Times? yTimes = y.GetTimesForParticipantType(ParticipantType.AGENCY); if (yTimes != null) yDate = (y.Shipcall.Type == ShipcallType.Arrival) ? (yTimes.EtaBerth ?? now) : (yTimes.EtdBerth ?? 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; } Mouse.OverrideCursor = null; })); } #endregion #region control event handler private async void Sc_EditRequested(ShipcallControl obj) { if (obj.ShipcallControlModel != null) { EditShipcallControl esc = new() { ShipcallModel = obj.ShipcallControlModel, ShipApi = _shipApi, ShipEditingEnabled = App.Participant.IsTypeFlagSet(Extensions.ParticipantType.BSMD) }; if(esc.ShowDialog() ?? false) { try { obj.ShipcallControlModel.Shipcall?.Participants.Clear(); if(! await obj.ShipcallControlModel.UpdateTimesAssignments(this._timesApi)) { ShowErrorDialog(obj.ShipcallControlModel.LastErrorMessage, "Update assignments error"); } foreach(ParticipantAssignment pa in obj.ShipcallControlModel.AssignedParticipants.Values) obj.ShipcallControlModel.Shipcall?.Participants.Add(pa); await _shipcallApi.ShipcallUpdateAsync(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, ParticipantType participantType) { if( obj.ShipcallControlModel == null) { return; } if (!obj.ShipcallControlModel.AssignedParticipants.ContainsKey(participantType)) return; // no assigment means no dialog my friend Times? agencyTimes = obj.ShipcallControlModel.GetTimesForParticipantType(ParticipantType.AGENCY); // 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; etc.ShipcallModel = obj.ShipcallControlModel; if (etc is EditTimesControl control) control.AgencyTimes = agencyTimes; 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 _timesApi.TimesUpdateAsync(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 _timesApi.TimesCreateAsync(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) { IEditTimesControl? editControl = null; switch(sc.ShipcallControlModel?.Shipcall?.Type) { case ShipcallType.Arrival: editControl = new EditTimesAgencyIncomingControl(); break; case ShipcallType.Departure: editControl = new EditTimesAgencyOutgoingControl(); break; case ShipcallType.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 { if (sc.ShipcallControlModel != null) { if (!await sc.ShipcallControlModel.UpdateTimesAssignments(_timesApi)) // if the agent changed the assignment of the participant to another { ShowErrorDialog(sc.ShipcallControlModel.LastErrorMessage, "Error updating times assignment"); } } // 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 _timesApi.TimesUpdateAsync(editControl.Times); } else { if ((sc.ShipcallControlModel != null) && (sc.ShipcallControlModel.Shipcall != null)) { editControl.Times.ShipcallId = sc.ShipcallControlModel.Shipcall.Id; } Id resultAPI_Id = await _timesApi.TimesCreateAsync(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 _shipcallApi.ShipcallUpdateAsync(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) { // Example: // Error calling ShipcallUpdate: {\"message\": \"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users // (if the special-flag is enabled). Assigned Agency: ShipcallParticipantMap(id=628, shipcall_id=115, participant_id=10, // type=8, created=datetime.datetime(2024, 8, 28, 15, 13, 14), modified=None) with Flags: 42\"} Match m = Regex.Match(message, "\\{(.*)\\}"); if ((m != null) && m.Success) { try { dynamic? msg = JsonConvert.DeserializeObject(m.Value); if (msg != null) { if (msg.error_field != null) { caption = $"{caption}: {msg.error_field}"; } if(msg.error_description != null) { message = msg.error_description; } } } catch (Exception) { } } _log.ErrorFormat("{0} - {1}", caption, message); 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; this.comboBoxPorts.ItemsSource = BreCalLists.AllPorts.Where(x => App.Participant.Ports.Contains(x.Id)); } private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) { Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true }); e.Handled = true; } #endregion } }