using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.DynamicGrid; using InABox.Wpf; using InABox.WPF; using NPOI.SS.Formula.Functions; using Syncfusion.Data; using Syncfusion.Data.Extensions; using Syncfusion.UI.Xaml.Grid; using Syncfusion.Windows.Shared; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using Columns = InABox.Core.Columns; namespace PRSDesktop.Panels.StockForecast.OrderScreen; public enum StockForecastOrderingType { StockOrder, JobOrder } public class StockForecastOrderingItemQuantity { public event Action? Changed; private double _stockTotal; public double StockTotal { get => _stockTotal; set { _stockTotal = value; Changed?.Invoke(); } } public Dictionary JobTotals { get; init; } = []; public void DoChanged() { Changed?.Invoke(); } public double JobTotal => JobTotals.Sum(x => x.Value); public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? StockTotal : JobTotal; } public class StockForecastOrderingItem : BaseObject { [EditorSequence(1)] public ProductLink Product { get; set; } [EditorSequence(2)] public ProductStyleLink Style { get; set; } [EditorSequence(3)] public StockDimensions Dimensions { get; set; } [EditorSequence(4)] [DoubleEditor] public double RequiredQuantity { get; set; } private Dictionary JobRequiredQuantities { get; set; } = []; public Dictionary GetJobRequiredQuantities() { return JobRequiredQuantities; } public void SetJobRequiredQuantity(Guid jobID, double requiredQty) { JobRequiredQuantities[jobID] = requiredQty; } private StockForecastOrderingItemQuantity[] Quantities = []; public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i]; public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? Quantities.Sum(x => x.StockTotal) : Quantities.Sum(x => x.JobTotal); public void SetQuantities(StockForecastOrderingItemQuantity[] quantities) { Quantities = quantities; } } public class StockForecastOrderingResult { public SupplierLink Supplier { get; set; } public JobLink? Job { get; set; } public StockForecastOrderingItem Item { get; set; } public SupplierProduct SupplierProduct { get; set; } public double Quantity { get; set; } public StockForecastOrderingResult(SupplierLink supplier, JobLink? job, StockForecastOrderingItem item, double quantity, SupplierProduct supplierProduct) { Supplier = supplier; Job = job; Item = item; Quantity = quantity; SupplierProduct = supplierProduct; } } public class StockForecastOrderingGrid : DynamicItemsListGrid, ISpecificGrid { private List SupplierProducts = []; private SupplierLink[] Suppliers = []; public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType)); private DynamicActionColumn[] QuantityColumns = []; private DynamicActionColumn[] CostColumns = []; private readonly Dictionary JobDetails = []; private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage(); private StockForecastOrderingType _orderType = StockForecastOrderingType.JobOrder; public StockForecastOrderingType OrderType { get => _orderType; set { if(_orderType != value) { _orderType = value; CalculateQuantities(); foreach(var control in QuantityControls) { control.UpdateControl(OrderType); } } } } public IEnumerable Results { get { for(int i = 0; i < Suppliers.Length; ++i) { var supplier = Suppliers[i]; foreach(var item in Items) { var qty = item.GetQuantity(i); var supplierProduct = GetSupplierProduct(item, supplier.ID); if (supplierProduct is null) { // If this is true, then the quantities also will have to be true. continue; } if(OrderType == StockForecastOrderingType.StockOrder && qty.StockTotal > 0) { yield return new(supplier, null, item, qty.StockTotal, supplierProduct); } else { foreach(var (jobID, q) in qty.JobTotals) { if(q > 0) { yield return new(supplier, new() { ID = jobID }, item, q, supplierProduct); } } } } } } } public StockForecastOrderingGrid() { HiddenColumns.Add(x => x.Product.Image.ID); } #region UI Component private Component? _uiComponent; private Component UIComponent { get { _uiComponent ??= new Component(this); return _uiComponent; } } protected override IDynamicGridUIComponent CreateUIComponent() { return UIComponent; } private class Component : DynamicGridGridUIComponent { private StockForecastOrderingGrid Grid; public Component(StockForecastOrderingGrid grid) { Parent = grid; Grid = grid; DataGrid.FrozenColumnCount = 7; } protected override Brush? GetCellSelectionBackgroundBrush() { return null; } protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) { var item = Grid.LoadItem(row); if(column is DynamicActionColumn ac) { var qIdx = Grid.QuantityColumns.IndexOf(ac); var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac)); if(idx != -1) { var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].ID); if(supplierProduct is null) { return new SolidColorBrush(Colors.Gainsboro); } //if(item.GetTotalQuantity(Grid.OrderType) < item.RequiredQuantity) //{ // return new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 }; //} //else //{ // return new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 }; //} } } return base.GetCellBackground(row, column); } } #endregion private bool _observing = true; private void SetObserving(bool observing) { _observing = observing; } protected override void Changed() { if (_observing) { base.Changed(); } } protected override void DoReconfigure(DynamicGridOptions options) { options.Clear(); options.FilterRows = true; } private bool _loadedData = false; private void LoadData() { var supplierColumns = Columns.None().Add(x => x.ID) .Add(x => x.SupplierLink.ID) .Add(x => x.Product.ID) .Add(x => x.Style.ID) .Add(x => x.ForeignCurrencyPrice) .Add(x => x.CostPrice) .AddDimensionsColumns(x => x.Dimensions) .Add(x => x.SupplierLink.Code); SupplierProducts = Client.Query( new Filter(x => x.Product.ID).InList(Items.Select(x => x.Product.ID).ToArray()) .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty), supplierColumns, new SortOrder(x => x.SupplierLink.Code)) .ToList(); Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray(); foreach(var (itemIdx, item) in Items.WithIndex()) { var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length]; for(int i = 0; i < Suppliers.Length; ++i) { quantities[i] = CreateQuantity(itemIdx); } item.SetQuantities(quantities); } CalculateQuantities(); _loadedData = true; } private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx) { var qty = new StockForecastOrderingItemQuantity(); qty.Changed += () => { var row = Data.Rows[itemIdx]; InvalidateRow(row); DoChanged(); }; return qty; } private void CalculateQuantities() { SetObserving(false); foreach(var item in Items) { var supplierProduct = GetSupplierProduct(item); for(int i = 0; i < Suppliers.Length; ++i) { var qty = item.GetQuantity(i); var supplier = Suppliers[i]; if(supplierProduct is not null && supplier.ID == supplierProduct.SupplierLink.ID) { if(OrderType == StockForecastOrderingType.StockOrder) { qty.StockTotal = qty.JobTotal; } else { qty.JobTotals.Clear(); foreach(var (id, q) in item.GetJobRequiredQuantities()) { qty.JobTotals[id] = q; } } } else { if(OrderType == StockForecastOrderingType.StockOrder) { qty.StockTotal = 0; } else { foreach(var id in item.GetJobRequiredQuantities().Keys) { qty.JobTotals[id] = 0; } } } } } SetObserving(true); DoChanged(); InvalidateGrid(); } protected override DynamicGridColumns LoadColumns() { if (!_loadedData) { LoadData(); } ActionColumns.Clear(); ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicImagePreviewColumn(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start }); var columns = new DynamicGridColumns(); columns.Add(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter); columns.Add(x => x.Product.Name, 200, "Product Name", "", Alignment.MiddleLeft); columns.Add(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter); columns.Add(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter); columns.Add(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter); QuantityColumns = new DynamicActionColumn[Suppliers.Length]; CostColumns = new DynamicActionColumn[Suppliers.Length]; QuantityControls.Clear(); for(int i = 0; i < Suppliers.Length; ++i) { InitialiseSupplierColumn(i); } ActionColumns.Add(new DynamicMenuColumn(BuildMenu)); return columns; } private void EditSupplierProductGrid(DynamicGrid grid) { grid.OnCustomiseEditor += (sender, items, column, editor) => { if(new Column(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Product.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Style.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Job.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Dimensions).IsEqualTo(column.ColumnName)) { editor.Editable = editor.Editable.Combine(Editable.Disabled); } }; } private void BuildMenu(DynamicMenuColumn column, CoreRow? row) { if (row is null) return; column.AddItem("New Supplier", null, row => { if (row is null) return; var selection = new MultiSelectDialog( new Filter(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()), Columns.None().Add(x => x.ID).Add(x => x.Code), multiselect: false); if (selection.ShowDialog() != true) { return; } var supplier = selection.Data().Rows.First().ToObject(); var productInstance = LoadItem(row); var supplierProduct = new SupplierProduct(); supplierProduct.Product.CopyFrom(productInstance.Product); supplierProduct.Style.CopyFrom(productInstance.Style); supplierProduct.Dimensions.CopyFrom(productInstance.Dimensions); supplierProduct.SupplierLink.CopyFrom(supplier); if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid)) { SupplierProducts.Add(supplierProduct); var newSuppliers = new SupplierLink[Suppliers.Length + 1]; var newIdx = Suppliers.Length; for (int i = 0; i < Suppliers.Length; i++) { newSuppliers[i] = Suppliers[i]; } newSuppliers[newIdx] = supplierProduct.SupplierLink; foreach (var (itemIdx, item) in Items.WithIndex()) { var populateSupplierProduct = GetSupplierProduct(item); var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length]; for (int i = 0; i < Suppliers.Length; ++i) { quantities[i] = item.GetQuantity(i); } var newQty = CreateQuantity(itemIdx); quantities[newIdx] = newQty; if (OrderType == StockForecastOrderingType.StockOrder) { newQty.StockTotal = 0; } else { foreach (var id in item.GetJobRequiredQuantities().Keys) { newQty.JobTotals[id] = 0; } } item.SetQuantities(quantities); } Suppliers = newSuppliers; Refresh(true, true); } }); } private BitmapImage? Warning_Image(CoreRow? row) { if (row is null) return _warning; var item = LoadItem(row); if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity) { return _warning; } else { return null; } } protected override void ConfigureColumnGroups() { for(int idx = 0; idx < Suppliers.Length; ++idx) { GetColumnGrouping().AddGroup(Suppliers[idx].Code, QuantityColumns[idx], CostColumns[idx]); } } private void LoadJobData(IEnumerable ids) { var neededIDs = ids.Where(x => !JobDetails.ContainsKey(x)).ToArray(); if(neededIDs.Length > 0) { var details = Client.Query( new Filter(x => x.ID).InList(neededIDs), Columns.None().Add(x => x.ID) .Add(x => x.JobNumber) .Add(x => x.Name)); foreach(var job in details.ToObjects()) { JobDetails[job.ID] = job; } } } private class QuantityControl : ContentControl { private readonly StockForecastOrderingItem Item; private readonly int SupplierIndex; private readonly StockForecastOrderingGrid Parent; public QuantityControl(StockForecastOrderingGrid parent, StockForecastOrderingItem item, int supplierIndex, StockForecastOrderingType mode) { Parent = parent; Item = item; SupplierIndex = supplierIndex; UpdateControl(mode); } public void UpdateControl(StockForecastOrderingType mode) { var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].ID); if(supplierProduct is null) { Content = null; return; } if(mode == StockForecastOrderingType.StockOrder) { var editor = new DoubleTextBox { VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, Background = new SolidColorBrush(Colors.LightYellow), BorderThickness = new Thickness(0.0), MinValue = 0.0, Value = Item.GetQuantity(SupplierIndex).StockTotal }; editor.ValueChanged += (o, e) => { Item.GetQuantity(SupplierIndex).StockTotal = editor.Value ?? default; }; Content = editor; } else if(mode == StockForecastOrderingType.JobOrder) { var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) }); var editor = new TextBox { VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Center, HorizontalContentAlignment = HorizontalAlignment.Center, Background = new SolidColorBrush(Colors.White), BorderThickness = new Thickness(0.0), IsReadOnly = true, Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal) }; Grid.SetColumn(editor, 0); grid.Children.Add(editor); var btn = new Button { VerticalAlignment = VerticalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Stretch, Content = "..", Margin = new Thickness(1), Focusable = false }; btn.SetValue(Grid.ColumnProperty, 1); btn.SetValue(Grid.RowProperty, 0); btn.Click += (o, e) => { var qty = Item.GetQuantity(SupplierIndex); Parent.LoadJobData(qty.JobTotals.Keys); var items = qty.JobTotals.Select(x => { var item = new StockForecastOrderingJobItem { JobID = x.Key, RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key), Quantity = x.Value }; if(item.JobID == Guid.Empty) { item.Job = "General Stock"; } else if(Parent.JobDetails.TryGetValue(item.JobID, out var job)) { item.Job = $"{job.JobNumber}: {job.Name}"; } return item; }).ToList(); var window = new StockForecastOrderJobScreen(); window.Items = items; if(window.ShowDialog() == true) { foreach(var item in items) { qty.JobTotals[item.JobID] = item.Quantity; } qty.DoChanged(); editor.Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal); } }; grid.Children.Add(btn); Content = grid; } } } private List QuantityControls = []; private void InitialiseSupplierColumn(int idx) { var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID).ToArray(); var contextMenuFunc = (CoreRow[]? rows) => { var row = rows?.FirstOrDefault(); if (row is null) return null; var item = LoadItem(row); var supplierProduct = GetSupplierProduct(item, Suppliers[idx].ID); if (supplierProduct is not null) { return null; } var menu = new ContextMenu(); menu.AddItem("Create Supplier Product", null, new Tuple(item, idx), CreateSupplierProduct_Click); return menu; }; // Making local copy of index so that the lambda can use it, and not the changed value of 'i'. var qtyColumn = new Tuple(null!, null); QuantityColumns[idx] = new DynamicTemplateColumn(row => { var instance = LoadItem(row); var control = new QuantityControl(this, instance, idx, OrderType); QuantityControls.Add(control); return control; }) { HeaderText = "Qty.", Width = 80, ContextMenu = contextMenuFunc }; CostColumns[idx] = new DynamicTextColumn(row => { if(row is null) { return "Cost"; } var instance = LoadItem(row); var qty = OrderType == StockForecastOrderingType.StockOrder ? instance.GetQuantity(idx).StockTotal : instance.GetQuantity(idx).JobTotal; var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].ID); if(supplierProduct is not null) { return $"{qty * supplierProduct.CostPrice:C2}"; } else { return ""; } }) { HeaderText = "Cost", Width = 80, ContextMenu = contextMenuFunc, GetSummary = () => { var summary = new GridSummaryColumn { Format = "{Sum:C2}", SummaryType = Syncfusion.Data.SummaryType.Custom, CustomAggregate = new CostAggregate(idx, this) }; return summary; } }; ActionColumns.Add(QuantityColumns[idx]); ActionColumns.Add(CostColumns[idx]); } private void CreateSupplierProduct_Click(Tuple tuple) { var (item, supplierIdx) = tuple; var supplierProduct = new SupplierProduct(); supplierProduct.Product.CopyFrom(item.Product); supplierProduct.Style.CopyFrom(item.Style); supplierProduct.Dimensions.CopyFrom(item.Dimensions); supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]); if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid)) { SupplierProducts.Add(supplierProduct); InvalidateGrid(); } } private static bool Matches(StockForecastOrderingItem item, SupplierProduct supplierProduct) { return item.Product.ID == supplierProduct.Product.ID && item.Style.ID == supplierProduct.Style.ID && item.Dimensions.Equals(supplierProduct.Dimensions); } private static bool Matches(ProductInstance instance, SupplierProduct supplierProduct) { return instance.Product.ID == supplierProduct.Product.ID && instance.Style.ID == supplierProduct.Style.ID && instance.Dimensions.Equals(supplierProduct.Dimensions); } private SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item) { var defaultSupplierProduct = SupplierProducts.FirstOrDefault(x => x.ID == item.Product.Supplier.ID); if(defaultSupplierProduct is not null && Matches(item, defaultSupplierProduct)) { return defaultSupplierProduct; } else { return SupplierProducts.FirstOrDefault(x => Matches(item, x)); } } private SupplierProduct? GetSupplierProduct(ProductInstance instance, Guid supplierID) { return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(instance, x)); } private SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item, Guid supplierID) { return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x)); } //private double GetQuantity(SupplierProduct product) //{ // var instance = ProductInstances.WithIndex().Where(x => x.Value.Product.ID == product.ID) //} private class CostAggregate : ISummaryAggregate { public double Sum { get; private set; } private int SupplierIndex; private StockForecastOrderingGrid Grid; public CostAggregate(int supplierIndex, StockForecastOrderingGrid grid) { SupplierIndex = supplierIndex; Grid = grid; } public Action CalculateAggregateFunc() { return AggregateFunc; } private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args) { if (items is IEnumerable rows) { Sum = 0; foreach (var dataRow in rows) { var rowIdx = dataRow.Row.Table.Rows.IndexOf(dataRow.Row); var item = Grid.LoadItem(Grid.Data.Rows[rowIdx]); var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[SupplierIndex].ID); if(supplierProduct is not null) { var qty = item.GetQuantity(SupplierIndex); Sum += qty.GetTotal(Grid.OrderType) * supplierProduct.CostPrice; } } } else { Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'."); } } } }