This repository has been archived on 2025-02-17. You can view files and clone it, but cannot push or open issues or pull requests.
BreCal/src/BreCalClient/MainWindow.xaml.cs

792 lines
31 KiB
C#

// 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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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<int, ShipcallControlModel> _allShipcallsDict = new();
private readonly ConcurrentDictionary<int, ShipcallControl> _allShipCallsControlDict = new();
private readonly List<ShipcallControlModel> _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<Error>((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<Shipcall>? 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<Times> 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<int> 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
}
}