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 class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions) { public ProductLink Product { get; set; } = product; public ProductStyleLink Style { get; set; } = style; public StockDimensions Dimensions { get; set; } = dimensions; 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; } } public enum StockForecastOrderingType { StockOrder, JobOrder } public class StockForecastOrderingItemQuantity { public event Action? Changed; private double _total; public double Total { get => _total; set { _total = value; Changed?.Invoke(); } } private SupplierProduct? _supplierProduct; /// /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column. /// public SupplierProduct? SupplierProduct { get => _supplierProduct; set { _supplierProduct = value; Changed?.Invoke(); } } public void DoChanged() { Changed?.Invoke(); } } 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)] public JobLink Job { get; set; } [EditorSequence(5)] [DoubleEditor] public double RequiredQuantity { get; set; } [EditorSequence(6)] [EnumLookupEditor(typeof(SupplierProductOrderStrategy))] public SupplierProductOrderStrategy OrderStrategy { get; set; } public StockForecastOrderingItemQuantity[] Quantities = []; public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i]; public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? Quantities.Sum(x => x.Total) : Quantities.Sum(x => x.Total); 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 enum StockForecastOrderingStrategy { PerProduct, Exact, RoundUp, LowestUnitPrice, LowestOverallPrice, LowestOverstock } public class StockForecastOrderingGrid : DynamicItemsListGrid, ISpecificGrid { private List SupplierProducts = []; private SupplierLink[] Suppliers = []; public IList OrderData { get; set; } public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType)); private DynamicActionColumn[] SupplierProductColumns = []; 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(true); UIComponent.UpdateOrderType(OrderType); Refresh(true, true); } } } private StockForecastOrderingStrategy orderStrategy; public StockForecastOrderingStrategy OrderStrategy { get => orderStrategy; set { orderStrategy = value; foreach(var item in Items) { item.OrderStrategy = value switch { StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact, StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy.LowestOverallPrice, StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice, StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock, StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp, StockForecastOrderingStrategy.PerProduct or _ => item.Product.OrderStrategy }; } CalculateQuantities(false); Refresh(false, true); } } 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); if (qty.SupplierProduct is null) { continue; } if(qty.Total > 0) { if(OrderType == StockForecastOrderingType.StockOrder && qty.Total > 0) { yield return new(supplier, null, item, qty.Total, qty.SupplierProduct); } else { yield return new(supplier, item.Job, item, qty.Total, qty.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; UpdateOrderType(grid.OrderType); } public void UpdateOrderType(StockForecastOrderingType type) { DataGrid.FrozenColumnCount = type == StockForecastOrderingType.StockOrder ? 8 : 9; } 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 = item.GetQuantity(idx).SupplierProduct; 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.Style.Code) .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(OrderData.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(); LoadJobData(OrderData.SelectMany(x => x.GetJobRequiredQuantities().Keys).Distinct().Where(x => x != Guid.Empty)); CalculateQuantities(true); _loadedData = true; } private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx) { var qty = new StockForecastOrderingItemQuantity(); qty.Changed += () => { if (!_observing) return; var row = Data.Rows[itemIdx]; InvalidateRow(row); DoChanged(); }; return qty; } private void CalculateSupplierProduct(StockForecastOrderingItem item) { var selectedSupplierProducts = new List(); for(int i = 0; i < Suppliers.Length; ++i) { var supplierProduct = SelectSupplierProduct(SupplierProducts.Where(x => x.Product.ID == item.Product.ID && x.Style.ID == item.Style.ID && x.SupplierLink.ID == Suppliers[i].ID), item); var qty = item.GetQuantity(i); qty.SupplierProduct = supplierProduct; qty.Total = 0; if(supplierProduct is not null) { selectedSupplierProducts.Add(supplierProduct); } } var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item); if(selectedSupplierProduct is not null) { var supplierIdx = Suppliers.WithIndex() .FirstOrDefault(x => x.Value.ID == selectedSupplierProduct.SupplierLink.ID, new KeyValuePair(-1, null)).Key; if(supplierIdx != -1) { item.GetQuantity(supplierIdx).Total = GetRequiredQuantity(item, selectedSupplierProduct); } } } private void CalculateQuantities(bool recreateItems) { SetObserving(false); if (recreateItems) { Items.Clear(); foreach(var dataItem in OrderData) { if(OrderType == StockForecastOrderingType.StockOrder) { var item = new StockForecastOrderingItem(); item.Product.CopyFrom(dataItem.Product); item.Style.CopyFrom(dataItem.Style); item.Dimensions.CopyFrom(dataItem.Dimensions); item.RequiredQuantity = dataItem.RequiredQuantity; item.OrderStrategy = item.Product.OrderStrategy; Items.Add(item); } else { foreach(var (id, q) in dataItem.GetJobRequiredQuantities()) { var item = new StockForecastOrderingItem(); item.Product.CopyFrom(dataItem.Product); item.Style.CopyFrom(dataItem.Style); item.Dimensions.CopyFrom(dataItem.Dimensions); item.Job.ID = id; if(id != Guid.Empty) { item.Job.CopyFrom(JobDetails[id]); } item.RequiredQuantity = q; item.OrderStrategy = item.Product.OrderStrategy; Items.Add(item); } } } } 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); } foreach(var item in Items) { CalculateSupplierProduct(item); } SetObserving(true); DoChanged(); } private SupplierProduct? SelectSupplierProduct(IEnumerable supplierProducts, StockForecastOrderingItem item) { switch (item.OrderStrategy) { case SupplierProductOrderStrategy.Exact: case SupplierProductOrderStrategy.RoundUp: // First, find the cheapest in the right style and dimensions. return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions) && x.Style.ID == item.Style.ID).MinBy(x => x.CostPrice) // Otherwise, find the cheapest in the right dimensions. ?? supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions)).MinBy(x => x.CostPrice); default: return null; } } private double GetRequiredQuantity(StockForecastOrderingItem item, SupplierProduct supplierProduct) { switch (item.OrderStrategy) { case SupplierProductOrderStrategy.Exact: return item.RequiredQuantity; case SupplierProductOrderStrategy.RoundUp: return Math.Ceiling(item.RequiredQuantity); default: return 0.0; } } private bool _loadedColumns = false; protected override DynamicGridColumns LoadColumns() { if (!_loadedData) { LoadData(); } 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); if(OrderType == StockForecastOrderingType.JobOrder) { columns.Add(x => x.Job.JobNumber, 80, "Job No.", "", Alignment.MiddleCenter); } columns.Add(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter); if (!_loadedColumns) { ActionColumns.Clear(); ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicImagePreviewColumn(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicTemplateColumn(row => { var item = LoadItem(row); var box = new ComboBox(); box.ItemsSource = Enum.GetValues() .Select(x => new KeyValuePair(x, CoreUtils.Neatify(x.ToString()))); box.DisplayMemberPath = "Value"; box.SelectedValuePath = "Key"; box.SelectedValue = item.OrderStrategy; box.SelectionChanged += (o, e) => { item.OrderStrategy = (SupplierProductOrderStrategy)box.SelectedValue; CalculateSupplierProduct(item); InvalidateRow(row); }; box.Margin = new Thickness(2); box.VerticalContentAlignment = VerticalAlignment.Center; return box; }) { HeaderText = "Order Strategy.", Width = 120 }); SupplierProductColumns = new DynamicActionColumn[Suppliers.Length]; 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)); _loadedColumns = true; } 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 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; newQty.Total = 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, SupplierProductColumns[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) { // If no supplier product has been selected for this cell, we can't allow the user to select a quantity. if(Item.GetQuantity(SupplierIndex).SupplierProduct is null) { Content = null; return; } // Otherwise, simple quantity textbox editor. 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).Total }; editor.ValueChanged += (o, e) => { Item.GetQuantity(SupplierIndex).Total = editor.Value ?? default; }; Content = editor; } } private List QuantityControls = []; private void InitialiseSupplierColumn(int idx) { 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; }; var qtyColumn = new Tuple(null!, null); SupplierProductColumns[idx] = new DynamicTemplateColumn(row => { var instance = LoadItem(row); var comboBox = new ComboBox(); comboBox.ItemsSource = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID) .Select(x => new KeyValuePair( x, x.Style.ID != Guid.Empty ? $"{x.Dimensions.UnitSize}/{x.Style.Code}" : $"{x.Dimensions.UnitSize}")); comboBox.SelectedValuePath = "Key"; comboBox.DisplayMemberPath = "Value"; var qty = instance.GetQuantity(idx); comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct); comboBox.VerticalContentAlignment = VerticalAlignment.Center; comboBox.Margin = new Thickness(2); return comboBox; }) { HeaderText = "Supplier Product.", Width = 80 }; 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 = instance.GetQuantity(idx);//.Total; if(qty.SupplierProduct is not null) { return $"{qty.Total * 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(SupplierProductColumns[idx]); 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 SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item, Guid supplierID) { return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x)); } 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 qty = item.GetQuantity(SupplierIndex); if(qty.SupplierProduct is not null) { Sum += qty.Total * qty.SupplierProduct.CostPrice; } } } else { Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'."); } } } }