// Copyright (c) 2017 schick Informatik // Description: Control für die Auftragsbearbeitung (Rahmen) // using System; using System.Collections.Generic; using System.Diagnostics; using System.Windows.Controls; using System.Windows; using System.Windows.Data; using bsmd.database; using ENI2.DetailViewControls; using ENI2.Util; using ENI2.EditControls; using System.Windows.Input; using Microsoft.Office.Interop.Excel; using System.Linq; namespace ENI2 { /// /// Interaction logic for DetailRootControl.xaml /// public partial class DetailRootControl : UserControl { #region Fields private readonly List _listBoxList = new List(); private List _messages; private readonly Dictionary controlCache = new Dictionary(); // private readonly Dictionary messageClassControlDict = new Dictionary(); private readonly object messageListLock = new object(); private readonly HighlightService highlightService = new HighlightService(); // Referenzen für Fehler/Violation Dialoge (können, müssen aber nicht offen bleiben) protected ErrorListDialog _errorListDialog = null; protected ViolationListDialog _violationListDialog = null; #endregion #region Properties public MessageCore Core { get; private set; } public bool LockedByOtherUser { get; set; } public ReportingParty LockedBy { get; set; } internal event DatabaseEntityWatchdog.DatabaseEntityChangedHandler HighlightReset; internal event Action OpenNewCoreRequested; internal event Action ReloadCoreRequested; public bool HasUnsavedChanges { get { return this.buttonSave.Visibility == Visibility.Visible; } // schwach aber es wird's tun } public List HasUnsentMessages { get { // Bedingung: // wenn in einer Meldeklasse zwar Daten vorhanden sind, eingespielt durch Excel import oder // Handeingabe, diese aber NICHT gesendet wurden. // TODO: Hier wird noch ein Flag benötigt, dass die erfolgte Anzeige des Warndialogs speichert List result = new List(); foreach (Message aMessage in _messages) { if (((aMessage.InternalStatus == Message.BSMDStatus.UPDATED) || (aMessage.InternalStatus == Message.BSMDStatus.SAVED)) && // || // (aMessage.InternalStatus == Message.BSMDStatus.EXCEL)) && !aMessage.UnsentMessageWarningShown) { aMessage.UnsentMessageWarningShown = true; DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); result.Add(aMessage.MessageNotificationClassDisplay); } } return result; } } public List HasUnConfirmedMessages { get { List result = new List(); foreach(Message aMessage in _messages) { if (((aMessage.InternalStatus == Message.BSMDStatus.SENT) || (aMessage.InternalStatus == Message.BSMDStatus.TOSEND)) && !aMessage.UnconfirmedMessageWarningShown) { aMessage.UnconfirmedMessageWarningShown = true; DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); result.Add(aMessage.MessageNotificationClassDisplay); } } return result; } } public bool HasCriticalInfoMissing(out string messageText) { messageText = ""; // Hier haben wir Logik für Spezialfälle, z.B. dass für BRE und BRV bestimmte Meldeklassen gesendet werden *müssen* if(this.Core.PoC.Equals("DEBRV") || this.Core.PoC.Equals("DEBRE")) { foreach(Message aMessage in _messages) { if((aMessage.MessageNotificationClass == Message.NotificationClass.NOA_NOD) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "NOA_NOD"; return true; } if ((aMessage.MessageNotificationClass == Message.NotificationClass.AGNT) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "AGNT"; return true; } if ((aMessage.MessageNotificationClass == Message.NotificationClass.INFO) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "INFO"; return true; } if ((aMessage.MessageNotificationClass == Message.NotificationClass.SEC) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "SEC"; return true; } if ((aMessage.MessageNotificationClass == Message.NotificationClass.TIEFA) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "TIEFA"; return true; } if ((aMessage.MessageNotificationClass == Message.NotificationClass.SERV) && (aMessage.InternalStatus != Message.BSMDStatus.CONFIRMED)) { messageText = "SERV"; return true; } } } return false; } #endregion #region Construction public DetailRootControl(MessageCore aCore) { Core = aCore; InitializeComponent(); shipNameLabel.Text = aCore.Shipname; shipEMailLabel.Text = aCore.HerbergEmailContactReportingVessel; displayIdLabel.Text = aCore.DisplayId; // Listbox befüllen this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textOverview, MessageGroupControlType = typeof(OverViewDetailControl), ImagePath = "Resources/documents.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textPortCall, MessageGroupControlType = typeof(PortCallDetailControl), ImagePath = "Resources/eye_blue.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textPortNotification, MessageGroupControlType = typeof(PortNotificationDetailControl), ImagePath = "Resources/anchor.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textWaste, MessageGroupControlType = typeof(WasteDetailControl), ImagePath = "Resources/garbage.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textArrivalNotification, MessageGroupControlType = typeof(ArrivalNotificationDetailControl), ImagePath = "Resources/arrow_down_right_red.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textSecurity, MessageGroupControlType = typeof(SecurityDetailControl), ImagePath = "Resources/shield_yellow.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textPSC72h, MessageGroupControlType = typeof(PSC72hDetailControl), ImagePath = "Resources/alarmclock.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textMDH, MessageGroupControlType = typeof(MaritimeHealthDeclarationDetailControl), ImagePath = "Resources/medical_bag.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textDepartureNotification, MessageGroupControlType = typeof(DepartureNotificationDetailControl), ImagePath = "Resources/arrow_up_right_green.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textShipData, MessageGroupControlType = typeof(ShipDataDetailControl), ImagePath = "Resources/containership.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textBorderPolice, MessageGroupControlType = typeof(BorderPoliceDetailControl), ImagePath = "Resources/policeman_german.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textDGArrival, MessageGroupControlType = typeof(DangerousGoodsDetailControl), ImagePath = "Resources/sign_warning_radiation.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textDGDeparture, MessageGroupControlType = typeof(DangerousGoodsDetailControl), ImagePath = "Resources/sign_warning_radiation.png" }); this._listBoxList.Add(new MessageGroup() { MessageGroupName = Properties.Resources.textTowage, MessageGroupControlType = typeof(TowageDetailControl), ImagePath = "Resources/ship2.png" }); this.listBoxMessages.ItemsSource = this._listBoxList; _messages = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessagesForCore(Core, DBManager.MessageLoad.ALL); List missingMessages = bsmd.database.Util.CreateMessagesForCore(Core, _messages, this.LockedBy); _messages.AddRange(missingMessages); BindingOperations.EnableCollectionSynchronization(_messages, this.messageListLock); Dispatcher.BeginInvoke((System.Action)(() => this.listBoxMessages.SelectedIndex = 0)); RoutedCommand saveCmd = new RoutedCommand(); saveCmd.InputGestures.Add(new KeyGesture(Key.S, ModifierKeys.Control)); CommandBindings.Add(new CommandBinding(saveCmd, this.buttonSave_Click)); RoutedCommand saveAllCmd = new RoutedCommand(); saveAllCmd.InputGestures.Add(new KeyGesture(Key.A, ModifierKeys.Control)); CommandBindings.Add(new CommandBinding(saveAllCmd, this.buttonSaveAll_Click)); RoutedCommand reloadCmd = new RoutedCommand(); reloadCmd.InputGestures.Add(new KeyGesture(Key.R, ModifierKeys.Control)); CommandBindings.Add(new CommandBinding(reloadCmd, this.ShortCutReload)); RoutedCommand validateCmd = new RoutedCommand(); validateCmd.InputGestures.Add(new KeyGesture(Key.F, ModifierKeys.Control)); CommandBindings.Add(new CommandBinding(validateCmd, this.ShortCutValidate)); } #endregion #region public methods public void CoreChanged(MessageCore newCore) { this.Core = newCore; System.Windows.Application.Current.Dispatcher.Invoke(delegate { if(controlCache.ContainsKey(Properties.Resources.textOverview)) { OverViewDetailControl ovdc = controlCache[Properties.Resources.textOverview] as OverViewDetailControl; ovdc.Core = newCore; ovdc.UpdateCore(); } }); } public void ReloadCore() { this.DetailControl_RequestReload(this.Core.Id.Value); } #endregion #region class MessageGroup /// /// Klasse um ein Element der Listbox darzustellen (notwendig für das Databinding) /// public class MessageGroup { public Type MessageGroupControlType { get; set; } public string MessageGroupName { get; set; } public string ImagePath { get; set; } } #endregion #region event handler private void listBoxMessages_SelectionChanged(object sender, SelectionChangedEventArgs e) { if(listBoxMessages.SelectedItem != null) { MessageGroup mg = this.listBoxMessages.SelectedItem as MessageGroup; if (!controlCache.ContainsKey(mg.MessageGroupName)) { // create control instance for display: DetailBaseControl detailControl = (DetailBaseControl)Activator.CreateInstance(mg.MessageGroupControlType); // Spezial-Balkon für die Wiederverwendung von HAZD / HAZA als ein Control (es tut mir leid :D) if (mg.MessageGroupName.Equals(Properties.Resources.textDGDeparture)) ((DangerousGoodsDetailControl)detailControl).IsDeparture = true; detailControl.Core = Core; detailControl.Messages = _messages; detailControl.LockedByOtherUser = this.LockedByOtherUser; detailControl.JumpToListElementRequest += (index) => { if ((index >= 0) && (index < _listBoxList.Count)) { this.listBoxMessages.SelectedIndex = index; } }; detailControl.RequestReload += DetailControl_RequestReload; detailControl.NotificationClassChanged += DetailControl_NotificationClassChanged; detailControl.ResetControlCache += DetailControl_ResetControlCache; detailControl.RequestValidate += DetailControl_RequestValidate; detailControl.RequestDisable += DetailControl_RequestDisable; detailControl.RequestCopy += DetailControl_RequestCopy; detailControl.RequestSendValidation += DetailControl_RequestSendValidation; detailControl.Initialize(); bool isEnabled = !this.LockedByOtherUser; detailControl.SetEnabled(isEnabled); if (!isEnabled && (detailControl is OverViewDetailControl control) && !(Core.Cancelled ?? false)) control.ShowLockedBy(this.LockedBy); controlCache.Add(mg.MessageGroupName, detailControl); this.buttonSave.Visibility = Visibility.Hidden; } else { // Control has been created before: Set visibility of "save" button: bool hasDirtyMessages = false; DetailBaseControl dbc = controlCache[mg.MessageGroupName]; foreach (Message message in dbc.ControlMessages) if (message.IsDirty) hasDirtyMessages = true; this.buttonSave.Visibility = hasDirtyMessages ? Visibility.Visible : Visibility.Hidden; } // plug it in ;-) detailView.Children.Clear(); // zuerst Vio dann Error controlCache[mg.MessageGroupName].HighlightViolationMessageContainer(); controlCache[mg.MessageGroupName].HighlightErrorMessageContainer(); detailView.Children.Add(controlCache[mg.MessageGroupName]); } } private void DetailControl_RequestCopy() { CopyDeclarationDialog cdd = new CopyDeclarationDialog(); MessageCore newCore = new MessageCore(); cdd.NewCore = newCore; cdd.OldCore = this.Core; cdd.Closed += (senderDialog, closeArgs) => { CopyDeclarationDialog closedDialog = senderDialog as CopyDeclarationDialog; if (closedDialog.IsOK) { MessageCore existingCore = null; if (!newCore.IsDK && newCore.VisitId.IsNullOrEmpty() && newCore.TransitId.IsNullOrEmpty()) { // deutsche Häfen fordern eine Visit-Id an, für DK erfolgt hier nur die Anlage eines Datensatzes newCore.BSMDStatusInternal = MessageCore.BSMDStatus.TOSEND; } if (newCore.PoC.Equals("ZZNOK")) newCore.IsTransit = true; newCore.Incoming = true; newCore.DefaultReportingPartyId = this.LockedBy.Id; if(!newCore.VisitId.IsNullOrEmpty() || !newCore.TransitId.IsNullOrEmpty()) { if (newCore.IsTransit) existingCore = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessageCoreByTransitId(newCore.TransitId); else existingCore = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessageCoreByVisitId(newCore.VisitId); } bool skipCopyTransit = false; bool skipCopyVisit = false; if(existingCore != null) { if(MessageBox.Show(Properties.Resources.textDeclarationAlreadyExists, Properties.Resources.textCaptionExists, MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.OK) { List existingMessages = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessagesForCore(existingCore, DBManager.MessageLoad.ALL); foreach(Message existingMessage in existingMessages) { if((existingMessage.InternalStatus == Message.BSMDStatus.SENT) && (existingMessage.MessageNotificationClass != Message.NotificationClass.VISIT) && (existingMessage.MessageNotificationClass != Message.NotificationClass.TRANSIT)) { MessageBox.Show(Properties.Resources.textMessagesAlreadySent, Properties.Resources.textCaptionError, MessageBoxButton.OK, MessageBoxImage.Stop); return; } } // delete all existing data of core existingCore.ETA = newCore.ETA; existingCore.ETAKielCanal = newCore.ETAKielCanal; newCore = existingCore; foreach (Message existingMessage in existingMessages) { // Bearbeitungsinformationen für bestehende ID-Beantragung beibehalten, falls bereits vorhanden if(existingCore.IsTransit && (existingMessage.MessageNotificationClass == Message.NotificationClass.TRANSIT) && (existingMessage.InternalStatus == Message.BSMDStatus.CONFIRMED)) { skipCopyTransit = true; continue; } if(!existingCore.IsTransit && (existingMessage.MessageNotificationClass == Message.NotificationClass.VISIT) && (existingMessage.InternalStatus == Message.BSMDStatus.CONFIRMED)) { skipCopyVisit = true; continue; } if (existingMessage is ISublistContainer sublistContainer) { (sublistContainer).DeleteElements(); } DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Delete(existingMessage); } } else { return; } } DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(newCore); // Meldeklassen für neuen Anlauf erzeugen List newMessages = new List(); foreach (Message oldMessage in this._messages) { if (skipCopyTransit && (oldMessage.MessageNotificationClass == Message.NotificationClass.TRANSIT)) continue; if (skipCopyVisit && (oldMessage.MessageNotificationClass == Message.NotificationClass.VISIT)) continue; // 10.9.19: bestimmte Meldeklassen nicht kopieren. Muss getestet werden, ob hier // stattdessen "Leer"-Messages erzeugt werden müssen if (oldMessage.MessageNotificationClass == Message.NotificationClass.ATA) continue; if (oldMessage.MessageNotificationClass == Message.NotificationClass.ATD) continue; if (oldMessage.MessageNotificationClass == Message.NotificationClass.NOA_NOD) continue; bool isAndienKlasse = (oldMessage.MessageNotificationClass == Message.NotificationClass.AGNT) || (oldMessage.MessageNotificationClass == Message.NotificationClass.STAT) || (oldMessage.MessageNotificationClass == Message.NotificationClass.INFO) || (oldMessage.MessageNotificationClass == Message.NotificationClass.HAZA) || (oldMessage.MessageNotificationClass == Message.NotificationClass.HAZD); if (!cdd.CopyAll && !isAndienKlasse) continue; Message newMessage = oldMessage.Clone() as Message; newMessage.MessageCore = newCore; newMessage.MessageCoreId = newCore.Id; DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(newMessage); newMessage.SaveElements(); } DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).LoadXtraData(newCore); // reload data from DB and set all rel properites this.OnOpenNewCoreRequested(newCore); } }; cdd.Show(); } private void DetailControl_RequestDisable() { this.LockedByOtherUser = true; // fake flag // clear existing controls this.detailView.Children.Clear(); this.controlCache.Clear(); // return to "new" overview Dispatcher.BeginInvoke((System.Action)(() => this.listBoxMessages_SelectionChanged(this, null))); } private void DetailControl_ResetControlCache(string messageGroupName) { if (messageGroupName.IsNullOrEmpty()) return; if (controlCache.ContainsKey(messageGroupName)) controlCache.Remove(messageGroupName); } private void buttonSave_Click(object sender, RoutedEventArgs e) { MessageBoxResult result = MessageBox.Show(Properties.Resources.textQuestionSavePage, Properties.Resources.textConfirmation, MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { Util.UIHelper.SetBusyState(); if (this.detailView.Children[0] is DetailBaseControl currentControl) { foreach (Message message in currentControl.ControlMessages) { this.SaveMessage(message); } this.buttonSave.Visibility = Visibility.Hidden; if (currentControl is OverViewDetailControl) { // ggf. hat sich die Ticketnr geändert.. DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(currentControl.Core); } } } this.DetailControl_RequestReload(this.Core.Id.Value); } private void SaveMessage(Message message) { if (message.IsDirty || message.IsNew) { if ((message.Status == Message.MessageStatus.ACCEPTED) && ((message.InternalStatus == Message.BSMDStatus.CONFIRMED) || (message.InternalStatus == Message.BSMDStatus.VIOLATION))) message.InternalStatus = Message.BSMDStatus.UPDATED; else message.InternalStatus = Message.BSMDStatus.SAVED; string userName = "?"; if(App.UserId.HasValue && DBManager.Instance.GetReportingPartyDict().ContainsKey(App.UserId.Value)) { userName = DBManager.Instance.GetReportingPartyDict()[App.UserId.Value].Logon; } message.ChangedBy = string.Format("{0} at {1}", userName, DateTime.Now); message.IsDirty = false; message.UnsentMessageWarningShown = false; message.UnconfirmedMessageWarningShown = false; DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(message); message.SaveElements(); if(message.MessageNotificationClass == Message.NotificationClass.ATA) { DetailBaseControl currentControl = this.detailView.Children[0] as DetailBaseControl; // ggf. hat sich die Ticketnr geändert.. DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(currentControl.Core); } if(message.MessageNotificationClass == Message.NotificationClass.STAT) { DetailBaseControl currentControl = this.detailView.Children[0] as DetailBaseControl; currentControl.Core.IsSmallShip = ((STAT)message.Elements[0]).GrossTonnage < 500; DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(currentControl.Core); } } } private void buttonSaveAll_Click(object sender, RoutedEventArgs e) { MessageBoxResult result = MessageBox.Show(Properties.Resources.textQuestionSaveAll, Properties.Resources.textConfirmation, MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { Util.UIHelper.SetBusyState(); foreach (Message message in this._messages) { this.SaveMessage(message); } DetailBaseControl currentControl = this.detailView.Children[0] as DetailBaseControl; if (currentControl is OverViewDetailControl) { // ggf. hat sich die Ticketnr geändert.. DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(currentControl.Core); } this.buttonSaveAll.Visibility = Visibility.Hidden; this.buttonSave.Visibility = Visibility.Hidden; } this.DetailControl_RequestReload(this.Core.Id.Value); } private void DetailControl_NotificationClassChanged(Message.NotificationClass notificationClass) { // in der Übersicht die Meldeklasse als geändert markieren..? this.buttonSaveAll.Visibility = Visibility.Visible; this.buttonSave.Visibility = Visibility.Visible; } /* private void DetailControl_RequestLock(bool shouldLock) { if(App.LockingServiceClient == null) { return; } if (shouldLock) { try { Guid lockedUserId = App.LockingServiceClient.Lock(this.Core.Id.Value, this.userId); if (lockedUserId == Guid.Empty) { // lock successful this.Core.Locked = true; } else { // TODO: locking failed: Notify User } } catch(Exception ex) { Trace.WriteLine(ex.ToString()); // TODO } } else { App.LockingServiceClient.Unlock(this.Core.Id.Value, this.userId); this.Core.Locked = false; } } */ private void ShortCutReload(object sender, ExecutedRoutedEventArgs e) { this.DetailControl_RequestReload(this.Core.Id.Value); } private void ShortCutValidate(object sender, ExecutedRoutedEventArgs e) { this.DetailControl_RequestValidate(); } private void DetailControl_RequestReload(Guid id) { /// core und messages neu laden /// if (this.Core.Id.Value != id) { this.ReloadCoreRequested?.Invoke(id); } else { this.Core = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessageCoreById(this.Core.Id.Value); this._messages = DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).GetMessagesForCore(this.Core, DBManager.MessageLoad.ALL); // clear existing controls this.detailView.Children.Clear(); this.controlCache.Clear(); // return to "new" overviewdan Dispatcher.BeginInvoke((System.Action)(() => { this.listBoxMessages_SelectionChanged(this, null); shipNameLabel.Text = this.Core.Shipname; shipEMailLabel.Text = this.Core.HerbergEmailContactReportingVessel; } )); // if the entity has been highlighted (through remote change detection), reset this here this.OnHighlightReset(); } } private void DetailControl_RequestSendValidation() { this.Validate(false, out _, out List errorList); foreach (Message aMessage in this._messages) { if(aMessage.InternalStatus == Message.BSMDStatus.TOSEND) { foreach(MessageError messageError in errorList) { if(messageError.NotificationClass == aMessage.MessageNotificationClassDisplay) { aMessage.InternalStatus = Message.BSMDStatus.SUSPENDED; aMessage.ChangedBy = ""; aMessage.StatusInfo = string.Format("Validation error: {0}", messageError.ErrorText); DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); break; } } } } // NOA_NOD vor ATA vor ATD Versendereihenfolge sicherstellen if (this.Core.InitialHIS == Message.NSWProvider.DUDR) { bool noa_nod_queued_or_sent = false; bool ata_queued_or_sent = false; foreach (Message aMessage in this._messages) { if (aMessage.MessageNotificationClass == Message.NotificationClass.NOA_NOD) { if ((aMessage.SendSuccess ?? false) || (aMessage.InternalStatus == Message.BSMDStatus.TOSEND)) noa_nod_queued_or_sent = true; } if (aMessage.MessageNotificationClass == Message.NotificationClass.ATA) { if ((aMessage.SendSuccess ?? false) || (aMessage.InternalStatus == Message.BSMDStatus.TOSEND)) ata_queued_or_sent = true; } } foreach (Message aMessage in this._messages) { if ((aMessage.MessageNotificationClass == Message.NotificationClass.ATA) && (aMessage.InternalStatus == Message.BSMDStatus.TOSEND)) { if (!noa_nod_queued_or_sent) { aMessage.InternalStatus = Message.BSMDStatus.SUSPENDED; aMessage.ChangedBy = ""; aMessage.StatusInfo = string.Format("Validation error 142: NOA_NOD must be sent before ATA"); DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); } } if ((aMessage.MessageNotificationClass == Message.NotificationClass.ATD) && (aMessage.InternalStatus == Message.BSMDStatus.TOSEND)) { if (!noa_nod_queued_or_sent) { aMessage.InternalStatus = Message.BSMDStatus.SUSPENDED; aMessage.ChangedBy = ""; aMessage.StatusInfo = string.Format("Validation error 142: NOA_NOD must be sent before ATD"); DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); } if ((!ata_queued_or_sent ) && !DBManager.Instance.GetReportingPartyDict()[App.UserId.Value].IsAdmin) { aMessage.InternalStatus = Message.BSMDStatus.SUSPENDED; aMessage.ChangedBy = ""; aMessage.StatusInfo = string.Format("Validation error 143: ATA must be sent before ATD"); DBManager.GetSingleCon(Properties.Settings.Default.ConnectionString).Save(aMessage); } } } } } private void DetailControl_RequestValidate() { this.Validate(true, out _, out _); } private void Validate(bool showMessages, out List vViolations, out List vErrors) { vViolations = new List(); vErrors = new List(); // TODO: clear highlighting Util.UIHelper.SetBusyState(); RuleEngine ruleEngine = new RuleEngine(); foreach (Message aMessage in _messages) { if (!aMessage.EvaluateForValidation(this.Core.IsTransit)) continue; List errors = new List(); List violations = new List(); ruleEngine.ValidateMessage(aMessage, out errors, out violations); string messageGroup = this.MessageGroupForMessage(aMessage); if (messageGroup != null) { foreach (MessageError me in errors) me.MessageGroupName = messageGroup; foreach (MessageViolation mv in violations) mv.MessageGroupName = messageGroup; } vErrors.AddRange(errors); vViolations.AddRange(violations); } #region 12.11.18 / 6.3.21 / 23.5.22: globale Plausi-Prüfungen Message crewaMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.CREWA); Message crewdMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.CREWD); Message pasaMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.PASA); Message pasdMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.PASD); Message pobaMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.POBA); Message pobdMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.POBD); Message secMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.SEC); Message noanodMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.NOA_NOD); Message mdhMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.MDH); Message was_rcptMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.WAS_RCPT); Message wasMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.WAS); Message servMessage = _messages.Find(message => message.MessageNotificationClass == Message.NotificationClass.SERV); #region CREW / PAS Count Plausibility if (pobaMessage.Elements.Count == 1) { POBA poba = pobaMessage.Elements[0] as POBA; if (crewaMessage.Elements.Count != (poba.TotalCrewMembersOnBoardUponArrival ?? 0)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "POBA crew member count different from CREW count!", null, "Crew count mismatch", null, "CREWA"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } if(pasaMessage.Elements.Count != (poba.TotalPassengersOnBoardUponArrival ?? 0)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "POBA passenger count different from PAS count!", null, "Passenger count mismatch", null, "PASA"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } POBD pobd = pobdMessage.Elements[0] as POBD; if (crewdMessage.Elements.Count != (pobd.TotalCrewMembersOnBoardUponDeparture ?? 0)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "POBD crew member count different from CREW count!", null, "Crew count mismatch", null, "CREWD"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } if (pasdMessage.Elements.Count != (pobd.TotalPassengersOnBoardUponDeparture ?? 0)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "POBD passenger count different from PAS count!", null, "Passenger count mismatch", null, "PASD"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } } #endregion #region CREW/PAS Schengen Plausibility // Wir können davon ausgehen, dass bei allen Unterelementen die Flags gleich gesetzt sind. Das wird im Import und BorderPoliceDetailControl sichergestellt. if(crewaMessage.Elements.Count > 0) { CREW crewaFirst = crewaMessage.Elements[0] as CREW; bool crewaIsSchengen = crewaFirst.NotificationSchengen ?? false; if (!((crewaFirst.NotificationPAX ?? false) || crewaIsSchengen)) // mindestens eins der beiden { vErrors.Add(RuleEngine.CreateError(ValidationCode.V181, "Pax / Schengen: one must be set", null, Properties.Resources.textOverview, null, "CREWA")); ; } } if (crewdMessage != null) { if (crewdMessage.Elements.Count > 0) { CREWD crewdFirst = crewdMessage.Elements[0] as CREWD; bool crewdIsSchengen = crewdFirst.NotificationSchengen ?? false; if (!((crewdFirst.NotificationPAX ?? false) || crewdIsSchengen)) // mindestens eins der beiden { vErrors.Add(RuleEngine.CreateError(ValidationCode.V181, "Pax / Schengen: one must be set", null, Properties.Resources.textOverview, null, "CREWD")); ; } } } if(pasaMessage.Elements.Count > 0) { PAS pasFirst = pasaMessage.Elements[0] as PAS; bool pasIsSchengen = pasFirst.NotificationSchengen ?? false; bool pasIsPAX = pasFirst.NotificationPAX ?? false; if (!(pasIsPAX || pasIsSchengen)) // mindestens eins der beiden { vErrors.Add(RuleEngine.CreateError (ValidationCode.V201, "Pax / Schengen: one must be set", null, Properties.Resources.textOverview, null, "PASA")); } } if (pasdMessage != null) { if (pasdMessage.Elements.Count > 0) { PASD pasdFirst = pasdMessage.Elements[0] as PASD; bool pasdIsSchengen = pasdFirst.NotificationSchengen ?? false; bool pasdIsPAX = pasdFirst.NotificationPAX ?? false; if (!(pasdIsPAX || pasdIsSchengen)) // mindestens eins der beiden { vErrors.Add(RuleEngine.CreateError(ValidationCode.V201, "Pax / Schengen: one must be set", null, Properties.Resources.textOverview, null, "PASD")); } } } #endregion #region 7.11.23 > 12 Passagiere in PASA oder PASD -> CREW* und PAS* muss NotificationPAX gesetzt haben if ((pasaMessage != null) && (pasaMessage.Elements.Count > 12)) { PAS firstPASA = pasaMessage.Elements[0] as PAS; if(!(firstPASA.NotificationPAX ?? false)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.NO_PAX, "No PAX set (PASA > 12)", null, Properties.Resources.textOverview, null, "PASA"); vViolations.Add(mv); } if((crewaMessage != null) && (crewaMessage.Elements.Count > 0)) { CREW firstCREW = crewaMessage.Elements[0] as CREW; if(!(firstCREW.NotificationPAX ?? false)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.NO_PAX, "No PAX set (PASA > 12)", null, Properties.Resources.textOverview, null, "CREWA"); vViolations.Add(mv); } } } if((pasdMessage != null) && (pasdMessage.Elements.Count > 12)) { PASD firstPASD = pasdMessage.Elements[0] as PASD; if(!(firstPASD.NotificationPAX ?? false)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.NO_PAX, "No PAX set (PASD > 12)", null, Properties.Resources.textOverview, null, "PASD"); vViolations.Add(mv); } if ((crewdMessage != null) && (crewdMessage.Elements.Count > 0)) { CREWD firstCREW = crewdMessage.Elements[0] as CREWD; if(!(firstCREW.NotificationPAX ?? false)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.NO_PAX, "No PAX set (PASD > 12)", null, Properties.Resources.textOverview, null, "CREWD"); vViolations.Add(mv); } } } #endregion #region 4.1.23 no CREW effects warning for DE if (crewaMessage != null) { MessageViolation mv = crewaMessage.ViolationList.Find((x) => x.PropertyName.Equals("Effects") && (x.ViolationCode == (int)ValidationCode.TRUNCATE)); if ((mv != null) && !Core.IsDK) crewaMessage.ViolationList.Remove(mv); } if(crewdMessage != null) { MessageViolation mvd = crewdMessage.ViolationList.Find((x) => x.PropertyName.Equals("Effects") && (x.ViolationCode == (int)ValidationCode.TRUNCATE)); if ((mvd != null) && !Core.IsDK) crewdMessage.ViolationList.Remove(mvd); } #endregion #region WAS_RCPT double numbers Dictionary identDict = new Dictionary(); foreach (WAS_RCPT was_rcpt in was_rcptMessage.Elements.Cast()) { if (!was_rcpt.IdentificationNumber.IsNullOrEmpty()) { // auf doppelte Ident Nummern prüfen if (identDict.ContainsKey(was_rcpt.IdentificationNumber)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.V221, "Identification number", null, "WAS_RCPT", was_rcpt.Identifier, "WAS_RCPT"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); break; } else { identDict.Add(was_rcpt.IdentificationNumber, ""); } } } #endregion #region Kiel Canal Timing Plausibility if (!this.Core.IsTransit && (secMessage?.Elements.Count > 0) && (noanodMessage?.Elements.Count > 0)) { SEC sec = secMessage.Elements[0] as SEC; NOA_NOD noa_nod = noanodMessage.Elements[0] as NOA_NOD; if (sec.KielCanalPassagePlanned ?? false) { // Überprüfung, ob die eingehende NOK-Durchfahrt auch wirklich innerhalb der eingehenden Reise liegt (bei VISIT) bool isValidIncoming = (noa_nod.ETDFromLastPort < sec.KielCanalPassagePlannedIncomming) && (sec.KielCanalPassagePlannedIncomming < noa_nod.ETAToPortOfCall); if (!sec.KielCanalPassagePlannedIncomming.HasValue) isValidIncoming = true; if(!isValidIncoming) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "Kiel Canal incoming implausible (ETD / ETA ports)!", null, "Kiel Canal timing", null, "SEC"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } bool isValidOutgoing = (noa_nod.ETDFromPortOfCall < sec.KielCanalPassagePlannedOutgoing) && ((noa_nod.NextPort == "ZZUKN") || (sec.KielCanalPassagePlannedOutgoing < noa_nod.ETAToNextPort)); if (!sec.KielCanalPassagePlannedOutgoing.HasValue) isValidOutgoing = true; if(!isValidOutgoing) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "Kiel Canal outgoing implausible (ETD / ETA ports)!", null, "Kiel Canal timing", null, "SEC"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } } } #endregion #region IMO plausibility if (!this.Core.IMO.IsNullOrEmpty() && !bsmd.database.Util.IsIMOValid(this.Core.IMO)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "IMO number does not follow the rules", null, "GLOBAL"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } #endregion #region last port plausibility SEC vs NOANOD if((secMessage.Elements.Count > 0) && (noanodMessage.Elements.Count > 0)) { SEC sec = secMessage.Elements[0] as SEC; NOA_NOD noanod = noanodMessage.Elements[0] as NOA_NOD; if((sec.LastTenPortFacilitesCalled.Count > 0) && (!sec.SECSimplification ?? false)) { if((sec.LastTenPortFacilitesCalled[0].PortFacilityPortLoCode == null) || !sec.LastTenPortFacilitesCalled[0].PortFacilityPortLoCode.Equals(noanod.LastPort)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "SEC last port doesn't match NOANOD last port", null, "Last port mismatch", null, "SEC"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } DateTime comparison; if (noanod.ETDFromLastPort.HasValue) comparison = noanod.ETDFromLastPort.Value.ToLocalTime().Date; else comparison = DateTime.Now.Date; if((sec.LastTenPortFacilitesCalled[0].PortFacilityDateOfDeparture == null) || (sec.LastTenPortFacilitesCalled[0].PortFacilityDateOfDeparture != comparison)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, "SEC last port departure doesn't match NOANOD ETDFromLastPort", null, "Last port departure mismatch", null, "SEC"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } } } #endregion #region SEC / MDH last port { // scope to hide sec // 5.1.22 "Generell zu Überprüfen wäre: // Sind alle Einträge aus SEC, die innerhalb der letzten 30 Tag liegen, gem. LoCode und ETD auch in der MDH-Liste enthalten ?" if ((secMessage.Elements[0] is SEC sec) && (mdhMessage.Elements[0] is MDH mdh)) { NOA_NOD noa_nod = noanodMessage.Elements[0] as NOA_NOD; DateTime refDate = DateTime.Now; if (this.Core.IsTransit) { if (noa_nod.ETAToKielCanal.HasValue) refDate = noa_nod.ETAToKielCanal.Value.Date; else refDate = this.Core.ETAKielCanal.Value; } else { if (noa_nod.ETAToPortOfCall.HasValue) refDate = noa_nod.ETAToPortOfCall.Value.Date; else refDate = this.Core.ETA.Value; } foreach (LastTenPortFacilitiesCalled l10c in sec.LastTenPortFacilitesCalled) { if (!l10c.PortFacilityDateOfDeparture.HasValue) continue; if ((refDate - l10c.PortFacilityDateOfDeparture.Value).TotalDays < 31) { if (!l10c.PortFacilityPortLoCode.IsNullOrEmpty() && l10c.PortFacilityDateOfDeparture.HasValue) // this valid entry needs a match in mdh { bool matchIsFound = false; foreach (PortOfCallLast30Days poc30d in mdh.PortOfCallLast30Days) { if (poc30d.PortOfCallLast30DaysDateOfDeparture.HasValue && !poc30d.PortOfCallLast30DaysLocode.IsNullOrEmpty()) { if((poc30d.PortOfCallLast30DaysDateOfDeparture.Value.Date == l10c.PortFacilityDateOfDeparture.Value.Date) && poc30d.PortOfCallLast30DaysLocode.Equals(l10c.PortFacilityPortLoCode, StringComparison.OrdinalIgnoreCase)) { matchIsFound = true; break; } } } if (!matchIsFound) { string msg = string.Format("SEC / MDH last ports do not match (Locode, Date) at {0}", l10c.Identifier); MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.IMPLAUSIBLE, msg, null, "SEC/MDH comparison", null, "SEC"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); break; // report this only once.. } } } } } } #endregion #region Waste disposal service provider for DEHAM if ((!this.Core.DisplayId.IsNullOrEmpty() && this.Core.DisplayId.StartsWith("DEHAM")) || this.Core.PoC.Equals("DEHAM")) { if (wasMessage.Elements.Count > 0) { WAS was = wasMessage.Elements[0] as WAS; if ((!was.WasteDisposalValidExemption ?? true) && (was.WasteDisposalServiceProvider.Count == 0)) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.NOT_NULL, "Check waste disposal service provider", null, "WAS service provider", null, "WAS"); mv.MessageGroupName = Properties.Resources.textOverview; vViolations.Add(mv); } } } #endregion #region SERV existence for DEHAM / DEBRE / DEBRV if ((!this.Core.DisplayId.IsNullOrEmpty() && this.Core.DisplayId.StartsWith("DEHAM")) || this.Core.PoC.Equals("DEHAM") || (!this.Core.DisplayId.IsNullOrEmpty() && this.Core.DisplayId.StartsWith("DEBRE")) || this.Core.PoC.Equals("DEBRE") || (!this.Core.DisplayId.IsNullOrEmpty() && this.Core.DisplayId.StartsWith("DEBRV")) || this.Core.PoC.Equals("DEBRV")) { if (servMessage.Elements.Count == 0) { MessageViolation mv = RuleEngine.CreateViolation(ValidationCode.LIST_EMPTY, "No entry for SERV found", null, "SERV service provider", null, "SERV"); mv.MessageGroupName = Properties.Resources.textPortNotification; vViolations.Add(mv); } } #endregion #endregion foreach (MessageError me in vErrors) { this.highlightService.HighlightError(me, this.GetContainerForMessageGroupName(me.MessageGroupName)); } foreach (MessageViolation mv in vViolations) { this.highlightService.HighlightViolation(mv, this.GetContainerForMessageGroupName(mv.MessageGroupName)); } // "neue" regelbasierte Validierung: Hier werden die einzelnen Regeln geprüft. bsmd.database.ValidationRule.PrepareNameLookupDict(this.Core, this._messages); List validationRules = DBManager.Instance.GetValidationRules(); foreach (bsmd.database.ValidationRule validationRule in validationRules) { if (!validationRule.IsActive ?? false) continue; // Regel auspacken ConditionGroup cg = ValidationCondition.LoadFromString(validationRule.Rule); // außer der Nachricht müssten noch die "gescheiterten" Felder zurückgeliefert werden, damit ein Highlighting stattfinden kann! List failedFieldList = new List(); string resultMessage = string.Empty; if (cg != null) { if (ValidationCondition.CheckConditions(this.Core, this._messages, cg, failedFieldList, out resultMessage) ?? false) { // Regel hat zugeschlagen -> reporting Trace.WriteLine(string.Format("Rule {0} resulted in {1}", validationRule.Name, resultMessage)); } else { Trace.WriteLine(string.Format("Rule {0} passed", validationRule.Name)); } } } if (showMessages) { // Show error and violation dialog if (vErrors.Count > 0) { if(this._errorListDialog == null) { this._errorListDialog = new ErrorListDialog(); this._errorListDialog.Closed += (o, e) => this._errorListDialog = null; this._errorListDialog.Loaded += (o, e) => this._errorListDialog.RefreshVisible = true; this._errorListDialog.ErrorSelected += _errorListDialog_ErrorSelected; this._errorListDialog.RefreshClicked += _errorListDialog_RefreshClicked; this._errorListDialog.Show(); } else { this._errorListDialog.BringUp(); } this._errorListDialog.Errors = vErrors; } if (vViolations.Count > 0) { if(this._violationListDialog == null) { this._violationListDialog = new ViolationListDialog(); this._violationListDialog.Closed += (o, e) => this._violationListDialog = null; this._violationListDialog.Loaded += (o, e) => this._violationListDialog.RefreshVisible = true; this._violationListDialog.ViolationSelected += _errorListDialog_ErrorSelected; this._violationListDialog.RefreshClicked += _errorListDialog_RefreshClicked; this._violationListDialog.Show(); } else { this._violationListDialog.BringUp(); } _violationListDialog.Violations = vViolations; } if((vErrors.Count == 0) && (vViolations.Count == 0)) { MessageBox.Show(Properties.Resources.textValidationOK, Properties.Resources.textValidation, MessageBoxButton.OK, MessageBoxImage.Information); } } } private void _errorListDialog_RefreshClicked() { DetailControl_RequestValidate(); } private void _errorListDialog_ErrorSelected(DatabaseEntity obj) { string msgGroupName = null; if(obj is MessageError) { MessageError me = obj as MessageError; msgGroupName = me.MessageGroupName; } else if(obj is MessageViolation) { MessageViolation mv = obj as MessageViolation; msgGroupName = mv.MessageGroupName; } if(msgGroupName != null) { for(int i=0;i<_listBoxList.Count;i++) { if(_listBoxList[i].MessageGroupName.Equals(msgGroupName)) { this.listBoxMessages.SelectedIndex = i; break; } } } } #endregion #region private / protected methods protected virtual void OnHighlightReset() { this.HighlightReset?.Invoke(this.Core); } protected virtual void OnOpenNewCoreRequested(MessageCore newCore) { this.OpenNewCoreRequested?.Invoke(newCore); } private DependencyObject GetContainerForMessageGroupName(string messageGroupName) { if (messageGroupName != null) { if (controlCache.ContainsKey(messageGroupName)) return controlCache[messageGroupName]; } return null; } protected string MessageGroupForMessage(Message mh) { if (mh == null) return null; switch(mh.MessageNotificationClass) { case Message.NotificationClass.ATA: case Message.NotificationClass.TIEFA: case Message.NotificationClass.POBA: case Message.NotificationClass.BKRA: return Properties.Resources.textArrivalNotification; case Message.NotificationClass.BPOL: case Message.NotificationClass.CREWA: case Message.NotificationClass.CREWD: case Message.NotificationClass.PASA: case Message.NotificationClass.PASD: return Properties.Resources.textBorderPolice; case Message.NotificationClass.HAZA: return Properties.Resources.textDGArrival; case Message.NotificationClass.HAZD: return Properties.Resources.textDGDeparture; case Message.NotificationClass.ATD: case Message.NotificationClass.TIEFD: case Message.NotificationClass.POBD: case Message.NotificationClass.BKRD: return Properties.Resources.textDepartureNotification; case Message.NotificationClass.MDH: return Properties.Resources.textMDH; case Message.NotificationClass.NOA_NOD: case Message.NotificationClass.AGNT: return Properties.Resources.textPortCall; case Message.NotificationClass.NAME: case Message.NotificationClass.INFO: case Message.NotificationClass.SERV: case Message.NotificationClass.LADG: return Properties.Resources.textPortNotification; case Message.NotificationClass.PRE72H: return Properties.Resources.textPSC72h; case Message.NotificationClass.SEC: return Properties.Resources.textSecurity; case Message.NotificationClass.STAT: return Properties.Resources.textShipData; case Message.NotificationClass.TOWA: case Message.NotificationClass.TOWD: return Properties.Resources.textTowage; case Message.NotificationClass.WAS: case Message.NotificationClass.WAS_RCPT: return Properties.Resources.textWaste; } return null; } #endregion } }