using Comal.Classes; using InABox.Clients; using InABox.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; using InABox.Mobile; using Syncfusion.SfImageEditor.XForms; using Xamarin.CommunityToolkit.Extensions; using Xamarin.CommunityToolkit.UI.Views; using Xamarin.Essentials; using Xamarin.Forms; using Xamarin.Forms.Xaml; using XF.Material.Forms.UI.Dialogs; using Location = InABox.Core.Location; using LogType = InABox.Core.LogType; using TextAlignment = Xamarin.Forms.TextAlignment; namespace PRS.Mobile { public class StockLocationStatusConverter : AbstractConverter { protected override string? Convert(StockLocationShell? value, object? parameter = null) { return value != null ? $"{value.LocationType}, {(value.Active ? "Active" : "Inactive")}" : ""; } } public class StockHoldingShellColorConverter : AbstractConverter { public DateTime CurrentStockTake { get; set; } = DateTime.MinValue; protected override Color Convert(StockHoldingShell? value, object? parameter = null) { var count = value?.Transactions.Aggregate(0, (agg, trans) => agg += 1) ?? 0; var prevcounted = !CurrentStockTake.IsEmpty() && (value?.LastStocktake ?? DateTime.MinValue) >= CurrentStockTake; return count > 0 || prevcounted ? Color.LightGreen : Color.Orange; } } public class StockHoldingShellAdjustmentConverter : AbstractConverter { public DateTime CurrentStockTake { get; set; } = DateTime.MinValue; protected override String Convert(StockHoldingShell? value, object? parameter = null) { var basevalue = value?.Units ?? 0.0d; var count = value?.Transactions.Aggregate(0, (agg, trans) => agg += 1) ?? 0; var quantity = value?.Transactions.Aggregate(0.0d, (agg, trans) => agg += trans.Quantity) ?? 0.0d; var prevcounted = !CurrentStockTake.IsEmpty() && (value?.LastStocktake ?? DateTime.MinValue) >= CurrentStockTake; return count == 0 ? prevcounted ? "ALREADY\nCOUNTED" : "NOT\nCHECKED" : quantity.IsEffectivelyEqual(basevalue) ? "CORRECT\nQUANTITY" : quantity < basevalue ? $"{basevalue-quantity:F2}\nSHORT" : $"{quantity-basevalue:F2}\nOVER"; } } public class StockHoldingShellBalanceConverter : AbstractConverter { protected override String Convert(StockHoldingShell? value, object? parameter = null) { var basevalue = value?.Units ?? 0.0d; var count = value?.Transactions.Aggregate(0, (agg, trans) => agg += 1) ?? 0; var quantity = value?.Transactions.Aggregate(0.0d, (agg, trans) => agg += trans.Quantity) ?? 0.0d; return count == 0 || quantity.IsEffectivelyEqual(basevalue) ? $"{basevalue:F2}" : $"{quantity:F2}"; } } [XamlCompilation(XamlCompilationOptions.Compile)] public partial class StocktakeModule : MobilePage { private bool _isNewStockTake = true; public StocktakeModule(Guid locationid) { InitializeComponent(); if (locationid != Guid.Empty) { App.Data.StockLocations.Refresh(false); var location = App.Data.StockLocations.FirstOrDefault(x => x.ID == locationid); ViewModel.Location = location ?? new StockLocationShell(); Title = $"Stock Take: {ViewModel.Location.Description}"; } else ViewModel.Location = new StockLocationShell(); _isNewStockTake = ViewModel.Location.CurrentStocktake.IsEmpty(); _stockHoldingShellColorConverter.CurrentStockTake = ViewModel.Location.CurrentStocktake; _stockHoldingShellAdjustmentConverter.CurrentStockTake = ViewModel.Location.CurrentStocktake; } protected override async Task OnBackButtonClicked() { bool result = true; if (ViewModel.Location.ID != Guid.Empty && ViewModel.Transactions.Any()) result = await DisplayAlert("Stocktake in progress", "This Stocktake is not complete.\nAre you sure you wish to close it?", "No", "Yes") == false; if (result && _isNewStockTake && !ViewModel.Location.CurrentStocktake.IsEmpty()) { ViewModel.Location.CurrentStocktake = DateTime.MinValue; ViewModel.Location.Save("Stocktake cancelled by user"); } return result; } private void Holding_Clicked(object sender, EventArgs e) { if (ViewModel.Location.ID == Guid.Empty) { DisplayAlert("ERROR", "Please select a Location!", "OK"); return; } if ((sender as MobileCard)?.BindingContext is StockHoldingShell shell) { var transaction = ViewModel.Transactions.FirstOrDefault(x => x.Source.Shell == shell); if (transaction == null) { transaction = new StockTransaction(StockTransactionType.StockTake, shell, shell); transaction.Allocations = shell.Allocations.Select(x => new StockTransactionAllocation() { ID = x.ID, Description = x.Description, Quantity = x.Quantity, Maximum = double.MaxValue } ).ToArray(); } var popup = new StockTakeEdit(transaction); popup.TransactionSaved += (o,e) => { if (ViewModel.Location.CurrentStocktake.IsEmpty()) { ViewModel.Location.CurrentStocktake = DateTime.Now; ViewModel.Location.Save("Stocktake marked as started on mobile device"); } if (shell.Parent.Transactions != null && !shell.Parent.Transactions.Contains(transaction)) shell.Parent.Transactions?.Add(transaction); shell.ProductID = transaction.ProductID; shell.ProductCode = transaction.ProductCode; shell.ProductName = transaction.ProductName; var imageid = transaction.ImageID; if (imageid != Guid.Empty) shell.Parent.Images[imageid] = transaction.Image; shell.ImageID = imageid; shell.DimensionsUnitID = transaction.DimensionsUnitID; shell.DimensionsHeight = transaction.DimensionsHeight; shell.DimensionsWidth = transaction.DimensionsWidth; shell.DimensionsLength = transaction.DimensionsLength; shell.DimensionsQuantity = transaction.DimensionsQuantity; shell.DimensionsValue = transaction.DimensionsValue; shell.DimensionsUnitSize = transaction.DimensionsUnitSize; _holdings.ItemsSource = null; _holdings.ItemsSource = ViewModel.Holdings.Items; }; Navigation.PushAsync(popup); } } private async void TakePhoto_Clicked(object sender, EventArgs args) { try { MobileDocument file = await MobileDocument.From(PhotoUtils.CreateCameraOptions()); if (file?.Data?.Any() == true) ViewModel.Images.Add( new StockTransactionImage( file, MobileUtils.ImageTools.CreateThumbnail(file.Data, 256, 256) ) ); } catch (Exception e) { await MaterialDialog.Instance.AlertAsync(e.Message, "ERROR"); } } private async void PickPhoto_Clicked(object sender, EventArgs args) { try { MobileDocument file = await MobileDocument.From(PhotoUtils.CreatePhotoLibraryOptions()); if (file?.Data?.Any() == true) ViewModel.Images.Add( new StockTransactionImage( file, MobileUtils.ImageTools.CreateThumbnail(file.Data, 256, 256) ) ); } catch (Exception e) { await MaterialDialog.Instance.AlertAsync(e.Message, "ERROR"); } } private void Image_Clicked(object sender, EventArgs e) { if ((sender as MobileCard)?.BindingContext is StockTransactionImage image) { ShowPopup(() => { var editor = new SfImageEditor() { Source = ImageSource.FromStream(() => new MemoryStream(image.Document.Data)), BackgroundColor = Color.Transparent, }; editor.ToolbarSettings.HeaderToolbarHeight = 0; editor.ImageEdited += (o, args) => { // Trigger the Dispatcher to allow the edit to complete before saving // Otherwise the autosaved image is one edit behind the editor itself Dispatcher.BeginInvokeOnMainThread(() => editor.Save()); }; editor.ImageSaving += (o, args) => { using (var ms = new MemoryStream()) { args.Stream.CopyTo(ms); image.Document.Data = ms.GetBuffer(); image.Thumbnail = MobileUtils.ImageTools.CreateThumbnail(image.Document.Data, 256, 256); } args.Cancel = true; }; return editor; }); } } private async void Save_Clicked(object sender, MobileMenuButtonClickedEventArgs args) { if (!ViewModel.Holdings.Any() && !ViewModel.Transactions.Any() && ViewModel.Location.LocationType == StockLocationType.Transient) { if (await MaterialDialog.Instance.ConfirmAsync("This location is empty!\nMark it as Inactive?", "Empty Location") == true) { using(var dialog = await MaterialDialog.Instance.LoadingDialogAsync(message: "Closing Location")) { ViewModel.Location.Active = false; ViewModel.Location.Save("Closed by Mobile Device"); Navigation.PopAsync(); return; } } } if (ViewModel.Holdings.Any() && !ViewModel.Transactions.Any()) { await DisplayAlert("Error", "There are no transactions to save!", "OK"); return; } if (ViewModel.Holdings.Any() && !ViewModel.Images.Any()) { await DisplayAlert("Error", "Please provide at least one photo.", "OK"); return; } if (await DisplayAlert("Confirm", "Save StockTake Batch?", "OK", "CANCEL") == true) { using(var dialog = await MaterialDialog.Instance.LoadingDialogAsync(message: "Saving Data")) { Progress progress = new Progress((s) => dialog.MessageText = s); await ViewModel.Save(progress); } Navigation.PopAsync(); } } private async void AddHolding_Clicked(object sender, MobileMenuButtonClickedEventArgs args) { if (ViewModel.Location.ID == Guid.Empty) { DisplayAlert("ERROR", "Please select a Location!", "OK"); return; } var holding = ViewModel.Holdings.CreateItem(); holding.LocationID = ViewModel.Location.ID; holding.LocationCode = ViewModel.Location.Code; holding.LocationDescription = ViewModel.Location.Description; var transaction = new StockTransaction(StockTransactionType.StockTake, holding, holding); transaction.Allocations = new StockTransactionAllocation[] { new StockTransactionAllocation() { ID = Guid.Empty, Description = "General Stock", Quantity = 0.0F, Maximum = double.MaxValue } }; var popup = new StockTakeEdit(transaction); popup.TransactionSaved += (o,e) => { transaction.Source.JobID = transaction.Target.JobID; transaction.Source.JobNumber = transaction.Target.JobNumber; transaction.Source.JobName = transaction.Target.JobName; transaction.Source.StyleID = transaction.Target.StyleID; transaction.Source.StyleCode = transaction.Target.StyleCode; transaction.Source.StyleDescription = transaction.Target.StyleDescription; transaction.Source.Units = transaction.Target.Units; ViewModel.Transactions.Add(transaction); _holdings.ItemsSource = null; _holdings.ItemsSource = ViewModel.Holdings.Items; }; Navigation.PushAsync(popup); } private void ChangeArea_Clicked(object sender, MobileButtonClickEventArgs args) { ShowPopup( () => SelectionView.Execute( (columns) => { columns.Add(new MobileGridTextColumn() { Column = x => x.Code, Width = GridLength.Auto, Caption = "Code", Alignment = TextAlignment.Start }); columns.Add(new MobileGridTextColumn() { Column = x => x.Description, Width = GridLength.Star, Caption = "Description", Alignment = TextAlignment.Start }); }, (refresh) => { App.Data.StockAreas.Refresh(true); return App.Data.StockAreas.Where(x => x.Active); }, (areas) => { ViewModel.Location.AreaID = areas?.FirstOrDefault()?.ID ?? Guid.Empty; ViewModel.Location.AreaCode = areas?.FirstOrDefault()?.Code ?? string.Empty; ViewModel.Location.AreaDescription = areas?.FirstOrDefault()?.Description ?? string.Empty; ViewModel.Location.WarehouseID = areas?.FirstOrDefault()?.WarehouseID ?? Guid.Empty; ViewModel.Location.WarehouseCode = areas?.FirstOrDefault()?.WarehouseCode ?? string.Empty; ViewModel.Location.WarehouseDescription = areas?.FirstOrDefault()?.WarehouseDescription ?? string.Empty; DismissPopup(); } ) ); } private void ToggleActive_Clicked(object sender, MobileButtonClickEventArgs args) { ViewModel.Location.Active = !ViewModel.Location.Active; } } }