// Copyright (c) 2017- schick Informatik // Description: Display dialog for customs XML data upload app // using bsmd.database.EasyPeasy; using ENI2.Util; using Microsoft.Win32; using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media.Imaging; using System.Xml; using System.Xml.Serialization; namespace ENI2.Controls { /// /// Interaction logic for EasyPeasyControl.xaml /// public partial class EasyPeasyControl : UserControl { private ProofRequest _vm; #region Construction public EasyPeasyControl() { InitializeComponent(); this.dataGridGoodsItems.ContextMenu = new ContextMenu(); MenuItem addItem = new MenuItem(); addItem.Header = Properties.Resources.textAdd; addItem.Icon = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/Resources/add.png")) }; addItem.Click += AddItem_Click; this.dataGridGoodsItems.ContextMenu.Items.Add(addItem); MenuItem deleteItem = new MenuItem(); deleteItem.Header = Properties.Resources.textDelete; deleteItem.Icon = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/Resources/delete.png")) }; deleteItem.Click += DeleteItem_Click; this.dataGridGoodsItems.ContextMenu.Items.Add(deleteItem); // Add separator and paste option this.dataGridGoodsItems.ContextMenu.Items.Add(new Separator()); MenuItem pasteItem = new MenuItem(); pasteItem.Header = "Paste"; pasteItem.Click += (s, e) => HandlePasteOperation(); this.dataGridGoodsItems.ContextMenu.Items.Add(pasteItem); // Add command bindings for proper keyboard handling this.dataGridGoodsItems.CommandBindings.Add(new CommandBinding( ApplicationCommands.Paste, (s, e) => HandlePasteOperation(), (s, e) => e.CanExecute = Clipboard.ContainsText())); } #endregion public void SaveState() { try { EasyPeasyState.Save(_vm); } catch { } } #region context menu event handler private void AddItem_Click(object sender, RoutedEventArgs e) { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) return; var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF; int nextItemNo = list.Any() ? list.Max(x => x.GoodsItemNumber) + 1 : 1; var item = new GoodsItemForT2LT2LF { GoodsItemNumber = nextItemNo }; list.Add(item); } private void DeleteItem_Click(object sender, RoutedEventArgs e) { foreach(GoodsItemForT2LT2LF item in this.dataGridGoodsItems.SelectedItems.Cast().ToArray()) { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) return; var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF; list.Remove(item); } } #endregion #region button event handler private void buttonClear_Click(object sender, RoutedEventArgs e) { CleanupAutoCalculation(); this._vm = EasyPeasyState.CreateDefault(); if (_vm.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF = new ObservableCollection(); _vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today _vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays = 90; // default 90 days this.DataContext = this._vm; SetupAutoCalculation(); } private void buttonExport_Click(object sender, RoutedEventArgs e) { var dlg = new SaveFileDialog { FileName = "proofRequest.xml", Filter = "XML file|*.xml", OverwritePrompt = true }; if (dlg.ShowDialog() == true) { try { var ser = new XmlSerializer(typeof(ProofRequest)); // Namespaces (if needed) // var ns = new XmlSerializerNamespaces(); // ns.Add("xsd", "http://www.w3.org/2001/XMLSchema"); // ns.Add("xsi", "http://www.w3.org/2001/XMLSchema-instance"); var settings = new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }; using (var fs = File.Create(dlg.FileName)) using (var xw = XmlWriter.Create(fs, settings)) { ser.Serialize(xw, _vm); //, ns); } MessageBox.Show("Exported successfully.", "easy-peasy", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show("Export failed:\n" + ex.Message, "easy-peasy", MessageBoxButton.OK, MessageBoxImage.Error); } } } private void buttonImport_Click(object sender, RoutedEventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = "XML file|*.xml"; ofd.RestoreDirectory = true; ofd.Multiselect = false; if (ofd.ShowDialog() == true) { using (var fs = File.OpenRead(ofd.FileName)) { CleanupAutoCalculation(); var ser = new XmlSerializer(typeof(ProofRequest)); _vm = (ProofRequest)ser.Deserialize(fs); // after loading/creating _vm if (_vm.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF = new ObservableCollection(); _vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today if(_vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays == 9) _vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays = 90; // default 90 days this.DataContext = _vm; SetupAutoCalculation(); } } } #endregion #region loaded/unloaded event handler private void UserControl_Loaded(object sender, RoutedEventArgs e) { _vm = EasyPeasyState.LoadOrCreate(); if (_vm.ProofInformationT2LT2LF == null) _vm.ProofInformationT2LT2LF = new ProofInformationT2LT2LF(); if (_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF == null) _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF = new GoodsShipmentForT2LT2LF { LocationOfGoods = new LocationOfGoods(), TransportDocuments = new TransportDocuments() }; _vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today this.DataContext = _vm; SetupAutoCalculation(); } private void UserControl_Unloaded(object sender, RoutedEventArgs e) { CleanupAutoCalculation(); try { EasyPeasyState.Save(_vm); } catch { } } #endregion #region auto calculation total gross mass private void SetupAutoCalculation() { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null) { // Subscribe to collection changes (add/remove items) _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.CollectionChanged += GoodsItems_CollectionChanged; // Subscribe to DataGrid cell changes dataGridGoodsItems.CellEditEnding += DataGridGoodsItems_CellEditEnding; // Calculate initial total CalculateTotalGrossMass(); } } private void CleanupAutoCalculation() { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null) { // Unsubscribe from collection changes _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.CollectionChanged -= GoodsItems_CollectionChanged; dataGridGoodsItems.CellEditEnding -= DataGridGoodsItems_CellEditEnding; } } private void DataGridGoodsItems_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) { // Check if the edited column is GrossMass if (e.Column.Header.ToString() == "Gross") { // Delay calculation to allow the binding to update Dispatcher.BeginInvoke(new Action(() => { CalculateTotalGrossMass(); }), System.Windows.Threading.DispatcherPriority.Background); } } private void GoodsItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { // Recalculate total after any collection change CalculateTotalGrossMass(); } private void CalculateTotalGrossMass() { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null) { var total = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF .Sum(item => item.GoodsMeasure?.GrossMass ?? 0m); _vm.ProofInformationT2LT2LF.TotalGrossMassKg = total; // Force UI update by refreshing the binding var binding = BindingOperations.GetBindingExpression( FindTotalGrossMassTextBox(), TextBox.TextProperty); binding?.UpdateTarget(); } } // Simple property changed notification helper public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private TextBox FindTotalGrossMassTextBox() { // Find the TextBox that displays TotalGrossMassKg return this.FindName("textBoxTotalGrossMass") as TextBox ?? this.GetTemplateChild("textBoxTotalGrossMass") as TextBox; } #endregion #region cut & paste logic private void HandlePasteOperation() { if (Clipboard.ContainsText()) { var text = Clipboard.GetText(); if (!TryPaste_EspHsPkgsGross(text)) PasteGoodsItems(text); } } private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { if (Clipboard.ContainsText()) { var text = Clipboard.GetText(); if(!TryPaste_EspHsPkgsGross(text)) PasteGoodsItems(text); e.Handled = true; } } } private void PasteGoodsItems(string text) { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF == null) return; var lines = text.Replace("\r\n", "\n").Replace('\r', '\n') .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { // Split by tab first; if only one column, try CSV var cells = line.Split('\t'); if (cells.Length == 1) cells = SplitCsv(line); // Expected order: HS, Item#, Description, Gross, Net, Pkgs, Type, Marks var item = new GoodsItemForT2LT2LF(); if (cells.Length > 0) item.Commodity.HarmonizedSystemSubHeadingCode = cells[0].Trim(); if (cells.Length > 1 && int.TryParse(cells[1], out var n)) item.GoodsItemNumber = n; if (cells.Length > 2) item.DescriptionOfGoods = cells[2].Trim(); if (cells.Length > 3 && decimal.TryParse(cells[3], out var gross)) item.GoodsMeasure.GrossMass = gross; if (cells.Length > 4 && decimal.TryParse(cells[4], out var net)) item.GoodsMeasure.NetMass = net; if (cells.Length > 5 && int.TryParse(cells[5], out var pkgs)) item.Packaging.NumberOfPackages = pkgs; if (cells.Length > 6) item.Packaging.TypeOfPackages = cells[6].Trim(); if (cells.Length > 7) item.Packaging.ShippingMarks = cells[7].Trim(); _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.Add(item); } } // Very small CSV splitter (handles quotes) private static string[] SplitCsv(string line) { var res = new System.Collections.Generic.List(); var sb = new StringBuilder(); bool inQuotes = false; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '\"') { if (inQuotes && i + 1 < line.Length && line[i + 1] == '\"') { sb.Append('\"'); i++; } else { inQuotes = !inQuotes; } } else if (c == ',' && !inQuotes) { res.Add(sb.ToString()); sb.Clear(); } else { sb.Append(c); } } res.Add(sb.ToString()); return res.ToArray(); } private bool TryPaste_EspHsPkgsGross(string text) { if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF == null) return false; // Normalize and split lines var lines = text.Replace("\r\n", "\n").Replace('\r', '\n') .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length == 0) return false; // Determine next item number var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF; int nextItemNo = list.Any() ? list.Max(x => x.GoodsItemNumber) + 1 : 1; bool anyAdded = false; bool countrySeen = false; foreach (var raw in lines) { var line = raw; // do not Trim() entirely; keep leading tab as empty first cell var cells = line.Split('\t'); // keeps empty entries // Expected: // - 4 cells: [ESP or ""], [HS], [Pkgs], [Gross] // - 3 cells: [HS], [Pkgs], [Gross] string hs = null, pkgs = null, gross = null; if (cells.Length >= 4) { string c0 = cells[0]?.Trim(); // Optionally capture the first token like "ESP" (country tag), // only once and only if alphabetic (won't throw if numeric) if (!countrySeen && !string.IsNullOrWhiteSpace(c0) && c0.All(ch => char.IsLetter(ch))) { // If you decide later this should set a field, uncomment: // if (string.IsNullOrWhiteSpace(_vm.Country)) _vm.Country = c0; countrySeen = true; } hs = (cells.Length > 1 ? cells[1] : null); pkgs = (cells.Length > 2 ? cells[2] : null); gross = (cells.Length > 3 ? cells[3] : null); } else if (cells.Length == 3) { hs = cells[0]; pkgs = cells[1]; gross = cells[2]; } else { // Not enough data for this format; skip the row continue; } if (string.IsNullOrWhiteSpace(hs)) continue; var item = new GoodsItemForT2LT2LF { GoodsItemNumber = nextItemNo++, DescriptionOfGoods = "" // per spec }; item.Commodity.HarmonizedSystemSubHeadingCode = hs.Trim(); if (TryParseIntFlexible(pkgs, out var pk)) item.Packaging.NumberOfPackages = pk; if (TryParseDecimalFlexible(gross, out var g)) { item.GoodsMeasure.GrossMass = g; var net = g - 1m; if (net < 0m) net = 0m; item.GoodsMeasure.NetMass = net; } item.DescriptionOfGoods = "Brand New Vehicles"; // per spec item.Packaging.TypeOfPackages = "UN"; // per spec item.Packaging.ShippingMarks = "-"; // per spec list.Add(item); anyAdded = true; } return anyAdded; } #endregion #region static utils // this will go somewhere else later // Try parse decimal with current culture, invariant, and comma/dot flip private static bool TryParseDecimalFlexible(string s, out decimal value) { s = (s ?? "").Trim(); // 1) current culture if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.CurrentCulture, out value)) return true; // 2) invariant if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) return true; // 3) flip comma/dot and retry (helps when clipboard mixes locales) string flipped = s.Contains(",") ? s.Replace(",", ".") : s.Replace(".", ","); if (decimal.TryParse(flipped, NumberStyles.Number, CultureInfo.CurrentCulture, out value)) return true; if (decimal.TryParse(flipped, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) return true; value = 0m; return false; } private static bool TryParseIntFlexible(string s, out int value) { s = (s ?? "").Trim(); // Extract leading integer if something like "12 pcs" var digits = new string(s.TakeWhile(ch => char.IsDigit(ch) || ch == '-' || ch == '+').ToArray()); if (string.IsNullOrEmpty(digits)) digits = s; return int.TryParse(digits, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) || int.TryParse(digits, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); } #endregion } }