Participants can be of multiple types (e.g. agent and terminal), therefore the participant type must be stored in the times data record in order to assign times correctly during display and to differentiate in calculation.
600 lines
22 KiB
C#
600 lines
22 KiB
C#
// 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 static BreCalClient.Extensions;
|
|
|
|
namespace BreCalClient
|
|
{
|
|
/// <summary>
|
|
/// Interaction logic for MainWindow.xaml
|
|
/// </summary>
|
|
public partial class MainWindow : Window
|
|
{
|
|
|
|
private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30;
|
|
|
|
#region Fields
|
|
|
|
|
|
|
|
private readonly Dictionary<int, ShipcallControlModel> _allShipcallsDict = new();
|
|
private readonly Dictionary<int, ShipcallControl> _allShipCallsControlDict = new();
|
|
|
|
private readonly List<ShipcallControlModel> _visibleControlModels = new();
|
|
|
|
|
|
private List<Ship> _ships = new();
|
|
private readonly ConcurrentDictionary<int, Ship> _shipLookupDict = new();
|
|
private List<Berth> _berths = new();
|
|
private readonly ConcurrentDictionary<int, Berth> _berthLookupDict = new();
|
|
private List<Participant> _participants = new();
|
|
private readonly Dictionary<int, Participant> _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 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}";
|
|
}
|
|
}
|
|
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 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<Participant> 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<Shipcall>? 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<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;
|
|
this.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 (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));
|
|
}
|
|
|
|
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 ?? true);
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
}
|