using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.Core.Postable; using InABox.DynamicGrid; using InABox.Wpf; using InABox.WPF; using Syncfusion.UI.Xaml.Diagram; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Imaging; namespace PRSDesktop; public class PullResultGrid : DynamicItemsListGrid where T : BaseObject, IPostable, new() { private static readonly BitmapImage tick = InABox.Wpf.Resources.tick.AsBitmapImage(); private static readonly BitmapImage link = PRSDesktop.Resources.link.AsBitmapImage(); private static readonly BitmapImage refresh = PRSDesktop.Resources.refresh.AsBitmapImage(); private class ResultItem(PullResultItem item, bool selected) { public PullResultItem Item { get; set; } = item; public bool Selected { get; set; } = selected; } public bool CanSave => _items.Any(x => x.Selected); private List _items; public IEnumerable> Selected => _items.Where(x => x.Selected).Select(x => x.Item); protected DynamicGridCustomColumnsComponent ColumnsComponent; public PullResultGrid(IPullResult result) { _items = result.PulledEntities.Where(x => x.Item.PostedStatus != PostedStatus.PostFailed).Select(x => new ResultItem(x, false)).ToList(); Items = _items.Select(x => x.Item.Item).ToList(); ColumnsComponent = new DynamicGridCustomColumnsComponent(this, typeof(T).Name); } protected override DynamicGridColumns LoadColumns() { return ColumnsComponent.LoadColumns(); } protected override void SaveColumns(DynamicGridColumns columns) { ColumnsComponent.SaveColumns(columns); } protected override void LoadColumnsMenu(ContextMenu menu) { ColumnsComponent.LoadColumnsMenu(menu); } private ResultItem? GetItem(CoreRow? row) { return row is not null ? _items[_recordmap[row].Index] : null; } protected override void Init() { base.Init(); ActionColumns.Add(new DynamicImageColumn(Selected_Image, Selected_Click) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicImageColumn(Action_Image) { Position = DynamicActionColumnPosition.Start, ToolTip = Action_ToolTip }); } private FrameworkElement? Action_ToolTip(DynamicActionColumn column, CoreRow? row) { var item = GetItem(row); if(item is null) { return column.TextToolTip("Item Import Action"); } else if(item.Item.Type == PullResultType.Linked) { return column.TextToolTip("Existing PRS item linked to external item."); } else if(item.Item.Type == PullResultType.Updated) { return column.TextToolTip("Existing PRS item updated to external item."); } else if(item.Item.Type == PullResultType.New) { return column.TextToolTip("New item imported."); } else { return null; } } private BitmapImage? Action_Image(CoreRow? row) { var item = GetItem(row); if(item is null) { return null; } else if(item.Item.Type == PullResultType.Linked) { return link; } else if(item.Item.Type == PullResultType.Updated) { return refresh; } else { return null; } } private BitmapImage? Selected_Image(CoreRow? row) { var item = GetItem(row); return (item is null || item.Selected) ? tick : null; } private bool Selected_Click(CoreRow? row) { var item = GetItem(row); if(item is not null) { item.Selected = !item.Selected; DoChanged(); InvalidateRow(row!); return false; } else { var menu = new ContextMenu(); menu.AddItem("Select All", null, () => { foreach (var item in _items) { item.Selected = true; } DoChanged(); InvalidateGrid(); }); menu.AddItem("Deselect All", null, () => { foreach (var item in _items) { item.Selected = false; } DoChanged(); InvalidateGrid(); }); menu.IsOpen = true; return false; } } protected override void DoReconfigure(DynamicGridOptions options) { base.DoReconfigure(options); options.Clear(); options.SelectColumns = true; options.FilterRows = true; } } public static class PostUtils { private static readonly Inflector.Inflector inflector = new(new CultureInfo("en")); public static void PostEntities(IDataModel model, Action refresh, Action? configurePost = null) where T : Entity, IPostable, IRemotable, IPersistent, new() { bool retry; do { retry = false; try { var result = PosterUtils.Process(model); if (result is null) { MessageWindow.ShowMessage($"Processing failed", "Processing failed"); refresh(); } else { var failedMessages = new List(); var successCount = 0; foreach (var entity in result.PostedEntities) { if (entity.PostedStatus == PostedStatus.PostFailed) { failedMessages.Add(entity.PostedNote); } else { successCount++; } } if (successCount == 0) { MessageWindow.ShowMessage($"Processing failed:\n - {string.Join("\n - ", failedMessages)}", "Processing failed."); } else if (failedMessages.Count == 0) { MessageWindow.ShowMessage($"Processing successful; {successCount} items processed", "Processing successful."); } else { MessageWindow.ShowMessage($"{successCount} items succeeded, but {failedMessages.Count} failed:\n - {string.Join("\n - ", failedMessages)}", "Partial success"); } refresh(); } } catch (EmptyPostException) { MessageWindow.ShowMessage($"Please select at least one {typeof(T).Name}.", "Select items"); } catch (PostFailedMessageException e) { MessageWindow.ShowMessage(e.Message, "Post failed"); } catch (RepostedException) { MessageWindow.ShowMessage("At least one of the items you selected has already been processed. Processing cancelled.", "Already processed"); } catch (PostCancelledException) { MessageWindow.ShowMessage("Processing cancelled.", "Cancelled"); } catch (MissingSettingException e) { if (configurePost is not null && Security.CanConfigurePost()) { if (MessageWindow.ShowYesNo($"'{e.Setting}' has not been set-up for {inflector.Pluralize(typeof(T).Name)}. Would you like to configure this now?", "Configure Processing?")) { bool success = false; if (e.SettingsType.IsAssignableTo(typeof(IGlobalPosterSettings))) { success = PostableSettingsGrid.ConfigureGlobalPosterSettings(e.SettingsType); } else { success = PostableSettingsGrid.ConfigurePosterSettings(e.SettingsType); } if (success && MessageWindow.ShowYesNo("Settings updated; Would you like to retry the post?", "Retry?")) { retry = true; } else { MessageWindow.ShowMessage("Processing cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage("Processing cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage($"'{e.Setting}' has not been set-up for {inflector.Pluralize(typeof(T).Name)}", "Unconfigured"); } } catch (MissingSettingsException) { if (configurePost is not null && Security.CanConfigurePost()) { if (MessageWindow.ShowYesNo($"Processing has not been configured for {inflector.Pluralize(typeof(T).Name)}. Would you like to configure this now?", "Configure Processing?")) { configurePost(); } else { MessageWindow.ShowMessage("Processing cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage($"Processing has not been configured for {inflector.Pluralize(typeof(T).Name)}!", "Unconfigured"); } } catch (Exception e) { MessageWindow.ShowError("Processing failed.", e); refresh(); } } while (retry); } public static bool ShowPullResultGrid(IPullResult result, [NotNullWhen(true)] out List>? items) where T : Entity, IPostable, IRemotable, IPersistent, new() { var resultGrid = new PullResultGrid(result); var window = new DynamicContentDialog(resultGrid) { Title = "Select items to import:", CanSave = false }; resultGrid.OnChanged += (o, e) => window.CanSave = resultGrid.CanSave; resultGrid.Refresh(true, true); if(window.ShowDialog() == true) { items = resultGrid.Selected.ToList(); Client.Save(items.Select(x => x.Item), "Posted by user."); return true; } else { items = null; return false; } } public static void PullEntities(Action refresh, Action? configurePost = null) where T : Entity, IPostable, IRemotable, IPersistent, new() { bool retry; do { retry = false; try { var result = PosterUtils.Pull(); if (result is null) { MessageWindow.ShowMessage($"Import failed", "Import failed"); refresh(); } else { List>? items; if (!result.PulledEntities.Any(x => x.Item.PostedStatus != PostedStatus.PostFailed)) { items = result.PulledEntities.ToList(); } else { ShowPullResultGrid(result, out items); } if (items is null) { MessageWindow.ShowMessage("Import cancelled.", "Cancelled"); } else { var failedMessages = new List(); var successCount = 0; var importCount = 0; var updateCount = 0; var linkCount = 0; foreach (var item in items) { if (item.Item.PostedStatus == PostedStatus.PostFailed) { failedMessages.Add(item.Item.PostedNote); } else { successCount++; switch (item.Type) { case PullResultType.New: importCount++; break; case PullResultType.Linked: linkCount++; break; case PullResultType.Updated: default: updateCount++; break; } } } if (failedMessages.Count > 0 && successCount == 0) { MessageWindow.ShowMessage($"Import failed:\n - {string.Join("\n - ", failedMessages)}", "Import failed."); } else if (failedMessages.Count == 0) { if (successCount == 0) { MessageWindow.ShowMessage($"Nothing imported.", "Import successful."); } else { MessageWindow.ShowMessage($"Import successful; {importCount} items imported, {linkCount} items linked.", "Import successful."); } } else { MessageWindow.ShowMessage($"{successCount} items succeeded, but {failedMessages.Count} failed:\n - {string.Join("\n - ", failedMessages)}", "Partial success"); } refresh(); } } } catch (PullFailedMessageException e) { MessageWindow.ShowMessage(e.Message, "Import failed"); } catch (PullCancelledException) { MessageWindow.ShowMessage("Import cancelled.", "Cancelled"); } catch (MissingSettingException e) { if (configurePost is not null && Security.CanConfigurePost()) { if (MessageWindow.ShowYesNo($"'{e.Setting}' has not been set-up for {inflector.Pluralize(typeof(T).Name)}. Would you like to configure this now?", "Configure Import?")) { bool success = false; if (e.SettingsType.IsAssignableTo(typeof(IGlobalPosterSettings))) { success = PostableSettingsGrid.ConfigureGlobalPosterSettings(e.SettingsType); } else { success = PostableSettingsGrid.ConfigurePosterSettings(e.SettingsType); } if (success && MessageWindow.ShowYesNo("Settings updated; Would you like to retry the import?", "Retry?")) { retry = true; } else { MessageWindow.ShowMessage("Import cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage("Import cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage($"'{e.Setting}' has not been set-up for {inflector.Pluralize(typeof(T).Name)}", "Unconfigured"); } } catch (MissingSettingsException) { if (configurePost is not null && Security.CanConfigurePost()) { if (MessageWindow.ShowYesNo($"Importing has not been configured for {inflector.Pluralize(typeof(T).Name)}. Would you like to configure this now?", "Configure Import?")) { configurePost(); } else { MessageWindow.ShowMessage("Import cancelled.", "Cancelled"); } } else { MessageWindow.ShowMessage($"Importing has not been configured for {inflector.Pluralize(typeof(T).Name)}!", "Unconfigured"); } } catch (Exception e) { MessageWindow.ShowError("Import failed.", e); refresh(); } } while (retry); } public static void CreateToolbarButtons(IPanelHost host, Func> model, Action refresh, Action? configurePost = null) where T : Entity, IPostable, IRemotable, IPersistent, new() { if (!Security.CanPost()) return; var postSettings = PosterUtils.LoadPostableSettings(); if (!postSettings.PosterType.IsNullOrWhiteSpace()) { var posterEngine = PosterUtils.GetEngine(typeof(T)); Bitmap? image = null; if (postSettings.Thumbnail.ID != Guid.Empty) { var icon = new Client() .Load(new Filter(x => x.ID).IsEqualTo(postSettings.Thumbnail.ID)).FirstOrDefault(); if (icon?.Data?.Any() == true) image = new ImageConverter().ConvertFrom(icon.Data) as Bitmap; } host.CreatePanelAction(new PanelAction { Caption = postSettings.ButtonName.NotWhiteSpaceOr($"Process {inflector.Pluralize(typeof(T).Name)}"), Image = image ?? PRSDesktop.Resources.edit, OnExecute = action => { PostEntities( model(), refresh, configurePost); } }); if(posterEngine.Get(out var posterEngineType, out var _) && posterEngineType.HasInterface(typeof(IPullerEngine<>)) && postSettings.ShowPullButton) { host.CreatePanelAction(new PanelAction($"Import {inflector.Pluralize(typeof(T).Name)}", image ?? PRSDesktop.Resources.doc_xls, action => { PullEntities(refresh); })); } if (postSettings.ShowClearButton) { host.CreatePanelAction(new PanelAction { Caption = "Clear Posted Flag", Image = image ?? PRSDesktop.Resources.refresh, OnExecute = action => { var dataModel = model(); foreach(var (key, table) in dataModel.ModelTables) { table.IsDefault = false; } dataModel.SetColumns(Columns.Required().Add(x => x.PostedStatus).Add(x => x.PostedReference).Add(x => x.PostedNote).Add(x => x.Posted)); dataModel.SetIsDefault(true); dataModel.LoadModel(); var items = dataModel.GetTable().ToArray(); foreach(var item in items) { item.PostedStatus = PostedStatus.NeverPosted; item.PostedReference = ""; item.PostedNote = ""; item.Posted = DateTime.MinValue; } Client.Save(items, "Cleared posted flag"); refresh(); } }); } } if (configurePost is not null) { host.CreateSetupAction(new PanelAction { Caption = $"Configure {CoreUtils.Neatify(typeof(T).Name)} Processing", OnExecute = action => { configurePost(); } }); } } public static void ConfigurePost() where T : Entity, IPostable, IRemotable, IPersistent, new() { var postSettings = PosterUtils.LoadPostableSettings(); var grid = (DynamicGridUtils.CreateDynamicGrid(typeof(DynamicGrid<>), typeof(PostableSettings)) as DynamicGrid)!; if (grid.EditItems(new PostableSettings[] { postSettings })) { PosterUtils.SavePostableSettings(postSettings); } } public static void CreateToolbarButtons(IPanelHost host, Func> model, Action refresh, bool allowConfig) where T : Entity, IPostable, IRemotable, IPersistent, new() { CreateToolbarButtons(host, model, refresh, allowConfig ? ConfigurePost : null); } #region PostColumn private static readonly BitmapImage? post = PRSDesktop.Resources.post.AsBitmapImage(); private static readonly BitmapImage? tick = PRSDesktop.Resources.tick.AsBitmapImage(); private static readonly BitmapImage? warning = PRSDesktop.Resources.warning.AsBitmapImage(); private static readonly BitmapImage? refresh = PRSDesktop.Resources.refresh.AsBitmapImage(); public static void AddPostColumn(DynamicGrid grid) where T : Entity, IPostable, IRemotable, IPersistent, new() { grid.HiddenColumns.Add(x => x.PostedStatus); grid.HiddenColumns.Add(x => x.PostedNote); grid.ActionColumns.Add(new DynamicImageColumn( row => { if (row is null) return post; return row.Get(x => x.PostedStatus) switch { PostedStatus.PostFailed => warning, PostedStatus.Posted => tick, PostedStatus.RequiresRepost => refresh, PostedStatus.NeverPosted or _ => null, }; }, null) { ToolTip = (column, row) => { if (row is null) { return column.TextToolTip($"{CoreUtils.Neatify(typeof(T).Name)} Processed Status"); } return column.TextToolTip(row.Get(x => x.PostedStatus) switch { PostedStatus.PostFailed => "Post failed: " + row.Get(x => x.PostedNote), PostedStatus.RequiresRepost => "Repost required: " + row.Get(x => x.PostedNote), PostedStatus.Posted => "Processed", PostedStatus.NeverPosted or _ => "Not posted yet", }); } }); } #endregion }