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
puls200 110ff5ccce EXTENDED TIMES TO DIFFERENTIATE BETWEEN PARTICIPANT TYPES
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.
2023-09-21 08:23:19 +02:00

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
}
}