587 lines
22 KiB
C#
587 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)
|
|
{
|
|
_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);
|
|
|
|
_ = 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
|
|
};
|
|
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)
|
|
{
|
|
// show a dialog that lets the user create / update times for the given shipcall
|
|
IEditTimesControl etc = new EditTimesControl();
|
|
bool wasEdit = false;
|
|
if (times != null)
|
|
{
|
|
etc.Times = times;
|
|
wasEdit = true;
|
|
|
|
// wie bekommen wir hier raus ob terminal oder agent gemeint ist?
|
|
|
|
// if(_participantLookupDict[times.ParticipantId].Type ==
|
|
|
|
}
|
|
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
|
|
|
|
}
|
|
}
|