using Comal.Classes; using InABox.Core; using InABox.DynamicGrid; using InABox.WPF; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; using InABox.Wpf; using System.Windows; using InABox.Clients; using InABox.Scripting; using Microsoft.Win32; using Microsoft.CodeAnalysis; using InABox.Configuration; using Document = InABox.Core.Document; using System.Configuration; namespace PRSDesktop.Forms.Issues; public class IssuesGrid : DynamicGrid, ISpecificGrid { private readonly int ChunkSize = 500; public IQueryProviderFactory ClientFactory { get; set; } private IQueryProvider? _kanbanClient; private IQueryProvider KanbanClient { get { _kanbanClient ??= ClientFactory.Create(); return _kanbanClient; } } private IQueryProvider? _jobClient; private IQueryProvider JobClient { get { _jobClient ??= ClientFactory.Create(); return _jobClient; } } public Guid CustomerID { get; set; } // public static CustomProperty CustomerProperty = new CustomProperty // { // Name = "CustomerID", // PropertyType = typeof(string), // ClassType = typeof(Kanban) // }; private String _baseDirectory; public IssuesGrid() : base() { var cols = LookupFactory.DefineColumns(); // Minimum Columns for Lookup values foreach (var col in cols) HiddenColumns.Add(col); HiddenColumns.Add(x => x.Notes); ActionColumns.Add(new DynamicMenuColumn(BuildMenu) { Position = DynamicActionColumnPosition.End }); } private class UIComponent : DynamicGridGridUIComponent { private IssuesGrid Grid; public UIComponent(IssuesGrid grid) { Grid = grid; Parent = grid; } protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) { var status = row.Get(x => x.Status); var color = status == KanbanStatus.Open ? Colors.Orange : status == KanbanStatus.InProgress ? Colors.Plum : status == KanbanStatus.Waiting ? Colors.LightGreen : Colors.Silver; return color.ToBrush(0.5); } } protected override IDynamicGridUIComponent CreateUIComponent() { return new UIComponent(this); } protected override void Init() { base.Init(); AddButton("Check for Updates", PRSDesktop.Resources.autoupdate.AsBitmapImage(), CheckForUpdates); AddButton("Open Support Session", PRSDesktop.Resources.appicon.AsBitmapImage(), OpenSupportSession); _baseDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ""; if (File.Exists(Path.Combine(_baseDirectory, "PRSAvalonia", "PRS.Avalonia.Desktop.exe"))) { var btn = AddButton("PRS Mobile App", PRSDesktop.Resources.map.AsBitmapImage(), LaunchPRSMobile); btn.Margin = new Thickness(20, btn.Margin.Top, btn.Margin.Right, btn.Margin.Bottom); } if (File.Exists(Path.Combine(_baseDirectory, "PRSDigitalKey", "PRS.DigitalKey.Desktop.exe"))) AddButton("PRS Digital Key App", PRSDesktop.Resources.key.AsBitmapImage(), LaunchPRSDigitalKey); var scriptButton = AddButton("Check Script Status", PRSDesktop.Resources.edit.AsBitmapImage(), CheckScriptStatus); scriptButton.Margin = new Thickness(20, scriptButton.Margin.Top, scriptButton.Margin.Right, scriptButton.Margin.Bottom); } private interface IScriptReference { string Name { get; } string Script { get; } void Update(string script); } private class EntityScriptReference(Entity entity, IProperty property) : IScriptReference { public string Name => entity.ToString() ?? entity.GetType().Name; public string Script => (property.Getter()(entity) as string) ?? ""; public void Update(string script) { property.Setter()(entity, script); Client.Create(entity.GetType()).Save(entity, "Updated by User"); } public override string ToString() { return Name; } } private class GlobalSettingsScriptReference(IGlobalConfiguration configuration, IGlobalConfigurationSettings settings, IProperty property) : IScriptReference { public string Name => configuration.Section.IsNullOrWhiteSpace() ? settings.GetType().Name : $"{settings.GetType().Name}/{configuration.Section}"; public string Script => (property.Getter()(settings) as string) ?? ""; public void Update(string script) { property.Setter()(settings, script); configuration.Save(settings); } public override string ToString() { return Name; } } private class CustomModuleReference(CustomModule module) : IScriptReference { public string Name => $"{module.Section}/{module.Name}"; public string Script => module.Script; public void Update(string script) { module.Script = script; Client.Save(module, "Updated by User"); } public override string ToString() { return Name; } } private Tuple[]? _desktopScriptedTypes; private Tuple[] DesktopScriptedTypes { get { _desktopScriptedTypes ??= CoreUtils.Entities.Where(x => x.HasInterface(typeof(IHasDesktopScript))) .Select(x => { var property = DatabaseSchema.LocalProperties(x) .FirstOrDefault(x => x.Editor is ScriptEditor); if(property is null) { return null; } else { return new Tuple(x, property); } }) .NotNull() .ToArray(); return _desktopScriptedTypes; } } private IEnumerable GetScripts(bool showHidden) { var filter = showHidden ? Filter.All() : Filter.Where(x => x.Visible).IsEqualTo(true); var queries = new List>>>(); var extraTasks = new List>>(); foreach(var (type, property) in DesktopScriptedTypes) { if (type.IsSubclassOf(typeof(Entity))) { if(type == typeof(CustomModule)) { queries.Add(new( new KeyedQueryDef( filter, Columns.None() .Add(x => x.ID) .Add(x => x.Section) .Add(x => x.Name) .Add(x => x.Script)), x => x.ToArray().ToArray(x => new CustomModuleReference(x)))); } else { queries.Add(new( new KeyedQueryDef(type.Name, type, Filter.Create(type, property.Name, Operator.IsNotEqualTo, ""), Columns.None(type) .Add(x => x.ID) .Add(property.Name)), x => x.ToObjects(type).Cast().Select(x => new EntityScriptReference(x, property)))); } } else if (type.HasInterface(typeof(IGlobalConfigurationSettings))) { extraTasks.Add(Task.Run(() => { var config = (Activator.CreateInstance(typeof(GlobalConfiguration<>).MakeGenericType(type), "") as IGlobalConfiguration)!; var result = config.LoadAll(); return result.Select, IScriptReference>(x => { var thisConfig = (Activator.CreateInstance(typeof(GlobalConfiguration<>).MakeGenericType(type), x.Key) as IGlobalConfiguration)!; return new GlobalSettingsScriptReference(thisConfig, x.Value, property); }); })); } } var results = Client.QueryMultiple(queries.Select(x => x.Item1)); return queries.SelectMany(x => x.Item2(results.Get(x.Item1.Key))) .Concat(extraTasks.SelectMany(x => x.Result)); } private List> CheckCustomModules(bool showHidden, bool showWarnings) { var results = new List>(); Progress.ShowModal("Loading Data", progress => { var scripts = GetScripts(showHidden); foreach(var script in scripts) { try { progress.Report(script.Name); var scriptDocument = new ScriptDocument(script.Script); var ok = scriptDocument.Compile(); if (!ok || (showWarnings && scriptDocument.Diagnostics.Any(x => x.Severity == DiagnosticSeverity.Error || x.Severity == DiagnosticSeverity.Warning))) { var errors = scriptDocument.Diagnostics.Select(x => x.Contents); results.Add(new(script, string.Join('\n', errors.Select(x => $"- {x}")))); } } catch(Exception e) { results.Add(new(script, e.Message)); } } }); return results; } private bool CheckScriptStatus(Button button, CoreRow[] rows) { var showHidden = MessageWindow.ShowYesNo("Include Hidden Scripts?", "Confirm"); var showWarnings = MessageWindow.ShowYesNo("Include Warnings?", "Confirm"); var results = CheckCustomModules(showHidden, showWarnings); if (results.Count != 0) { var grid = new Grid(); grid.AddRow(GridUnitType.Auto); grid.AddRow(GridUnitType.Star); grid.AddRow(GridUnitType.Auto); grid.AddChild(new Label { Content = "The following errors were found in custom modules:" }, 0, 0); var list = new ListBox(); var label = new Label { HorizontalContentAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; void RebuildList() { list.Items.Clear(); foreach(var (script, errors) in results) { var itemGrid = new Grid { }; itemGrid.AddColumn(GridUnitType.Auto); itemGrid.AddColumn(GridUnitType.Star); var itemBtn = new Button { Content = new Image { Source = PRSDesktop.Resources.pencil.AsBitmapImage(), }, Width = 30, Height = 30, Padding = new(5), Margin = new(0, 0, 5, 0), Tag = script }; itemBtn.VerticalAlignment = VerticalAlignment.Top; itemBtn.Click += ModuleOpen_Click; itemGrid.AddChild(itemBtn, 0, 0); itemGrid.AddChild(new Label { Content = $"{script.Name}:\n{errors}" }, 0, 1); list.Items.Add(itemGrid); } label.Content = $"{list.Items.Count} items"; } grid.AddChild(list, 1, 0); var dockPanel = new DockPanel { LastChildFill = true }; var exportButton = new Button { Content = "Export", Padding = new(5), Margin = new(0, 5, 0, 0) }; exportButton.Click += (o, e) => { var result = string.Join("\n\n", results.Select(x => $"{x.Item1.Name}:\n{x.Item2}")); var dlg = new SaveFileDialog() { Filter = "Text Files (*.txt)|*.txt" }; if(dlg.ShowDialog() == true) { using var writer = new StreamWriter(dlg.FileName); writer.Write(result); } }; DockPanel.SetDock(exportButton, Dock.Left); dockPanel.Children.Add(exportButton); var refreshButton = new Button { Content = "Refresh", Padding = new(5), Margin = new(5, 5, 0, 0) }; refreshButton.Click += (o, e) => { results = CheckCustomModules(showHidden, showWarnings); RebuildList(); }; DockPanel.SetDock(refreshButton, Dock.Left); dockPanel.Children.Add(refreshButton); DockPanel.SetDock(label, Dock.Right); dockPanel.Children.Add(label); grid.AddChild(dockPanel, 2, 0); RebuildList(); var window = new DynamicContentDialog(grid, buttonsVisible: false) { Title = "Custom Module Errors" }; window.ShowDialog(); } return false; } private void ModuleOpen_Click(object sender, RoutedEventArgs e) { if (sender is not FrameworkElement element || element.Tag is not IScriptReference script) return; var editor = new ScriptEditorWindow(script.Script, scriptTitle: script.Name); if (editor.ShowDialog() == true) { script.Update(editor.Script); } } private bool LaunchPRSMobile(Button button, CoreRow[] rows) { var _mobileApp = System.IO.Path.Combine(_baseDirectory, "PRSAvalonia", "PRS.Avalonia.Desktop.exe"); var _info = new ProcessStartInfo(_mobileApp); Process.Start(_info); return false; } private bool LaunchPRSDigitalKey(Button button, CoreRow[] rows) { var _mobileApp = Path.Combine(_baseDirectory, "PRSDigitalKey", "PRS.DigitalKey.Desktop.exe"); var _info = new ProcessStartInfo(_mobileApp); Process.Start(_info); return false; } private bool OpenSupportSession(Button button, CoreRow[] rows) { SupportUtils.OpenSupportSession(); return false; } private bool CheckForUpdates(Button button, CoreRow[] rows) { if (SupportUtils.CheckForUpdates()) { Application.Current.Shutdown(); } else { if (MessageWindow.ShowYesNo( "You appear to be using the latest version already!\n\nRun the installer anyway?", "Update")) { if (SupportUtils.DownloadAndRunInstaller()) { Application.Current.Shutdown(); } } } return false; } protected override void DoReconfigure(DynamicGridOptions options) { options.Clear(); options.AddRows = true; options.EditRows = true; options.FilterRows = true; options.HideDatabaseFilters = true; } private void BuildMenu(DynamicMenuColumn column, CoreRow? row) { if (row is null) return; var menu = column.GetMenu(); menu.AddItem("Add note", null, row, AddNote_Click); menu.AddItem("Attach system logs", null, row, AttachLogs_Click); menu.AddSeparator(); menu.AddItem("Close issue", null, row, CloseTask_Click); } private void AttachLogs_Click(CoreRow row) { var logFile = CoreUtils.GetLogFile(); var data = File.ReadAllBytes(logFile); var doc = new InABox.Core.Document(); doc.Data = data; doc.CRC = CoreUtils.CalculateCRC(data); doc.FileName = Path.GetFileName(logFile); doc.TimeStamp = File.GetLastWriteTime(logFile); ClientFactory.Save(doc, "Attached logs to task."); var kanbanDocument = new KanbanDocument(); kanbanDocument.Document.CopyFrom(doc); kanbanDocument.Entity.CopyFrom(row.ToObject()); ClientFactory.Save(kanbanDocument, "Attached logs to task."); } public override Kanban CreateItem() { var item = base.CreateItem(); item.UserProperties["CustomerID"] = CustomerID.ToString(); item.Notes = [ $"Created on PRS {CoreUtils.GetVersion()} by {App.EmployeeName} ({App.EmployeeEmail})" ]; // item.Status = KanbanStatus.Open; return item; } private void AddNote_Click(CoreRow row) { var kanban = row.ToObject(); var text = ""; if(TextBoxDialog.Execute("Enter note:", ref text)) { text = string.Format("{0:yyyy-MM-dd HH:mm:ss}: {1}", DateTime.Now, text); kanban.Notes = kanban.Notes.Concatenate([text]); kanban.Status = KanbanStatus.InProgress; SaveItem(kanban); Refresh(false, true); } } private void CloseTask_Click(CoreRow row) { var kanban = row.ToObject(); kanban.Completed = DateTime.Now; kanban.Closed = DateTime.Now; SaveItem(kanban); Refresh(false, true); } private Column[] AllowedColumns = [ new(x => x.Number), new(x => x.Title), new(x => x.Description), new(x => x.Notes)]; protected override void CustomiseEditor(IDynamicEditorForm form, Kanban[] items, DynamicGridColumn column, BaseEditor editor) { base.CustomiseEditor(form, items, column, editor); if(!AllowedColumns.Any(x => x.Property == column.ColumnName)) { editor.Editable = editor.Editable.Combine(Editable.Hidden); } } public virtual CoreTable LookupValues(DataLookupEditor editor, Type parent, string columnname, BaseObject[]? items) { var client = ClientFactory.Create(editor.Type); var filter = LookupFactory.DefineLookupFilter(parent, editor.Type, columnname, items ?? (Array.CreateInstance(parent, 0) as BaseObject[])!); var columns = LookupFactory.DefineLookupColumns(parent, editor.Type, columnname); foreach (var key in editor.OtherColumns.Keys) columns.Add(key); var sort = LookupFactory.DefineSort(editor.Type); var result = client.Query(filter, columns, sort); result.Columns.Add(new CoreColumn { ColumnName = "Display", DataType = typeof(string) }); foreach (var row in result.Rows) { row["Display"] = LookupFactory.FormatLookup(parent, editor.Type, row, columnname); } return result; } protected override void DefineLookups(ILookupEditorControl sender, Kanban[] items, bool async = true) { if (sender.EditorDefinition is not DataLookupEditor editor) { base.DefineLookups(sender, items, async: async); return; } var colname = sender.ColumnName; if (async) { Task.Run(() => { try { var values = LookupValues(editor, typeof(Kanban), colname, items); Dispatcher.Invoke( () => { try { //Logger.Send(LogType.Information, typeof(T).Name, "Dispatching Results" + colname); sender.LoadLookups(values); } catch (Exception e2) { Logger.Send(LogType.Information, typeof(Kanban).Name, "Exception (2) in LoadLookups: " + e2.Message + "\n" + e2.StackTrace); } } ); } catch (Exception e) { Logger.Send(LogType.Information, typeof(Kanban).Name, "Exception (1) in LoadLookups: " + e.Message + "\n" + e.StackTrace); } }); } else { var values = LookupValues(editor, typeof(Kanban), colname, items); sender.LoadLookups(values); } } public override DynamicEditorPages LoadEditorPages(Kanban item) { var pages = new DynamicEditorPages { new DynamicDocumentGrid { Client = ClientFactory } }; return pages; } protected override DynamicGridColumns LoadColumns() { var columns = new DynamicGridColumns(); columns.Add(x => x.Number, caption: "Ticket", width: 60, alignment: Alignment.MiddleCenter); columns.Add(x => x.Title); columns.Add(x => x.CreatedBy, caption: "Created By", width: 150); columns.Add(x => x.Employee.Name, caption: "Assigned To", width: 150); columns.Add(x => x.Type.Description, caption: "Type", width: 100, alignment: Alignment.MiddleCenter); columns.Add(x => x.Status, caption: "Status", width: 80, alignment: Alignment.MiddleCenter); return columns; } #region Grid Stuff protected override string FormatRecordCount(int count) { return IsPaging ? $"{base.FormatRecordCount(count)} (loading..)" : base.FormatRecordCount(count); } protected override void Reload( Filters criteria, Columns columns, ref SortOrder? sort, CancellationToken token, Action action) { criteria.Add(Filter.Where(x => x.Closed).IsEqualTo(DateTime.MinValue)); criteria.Add(Filter.Where(x => x.Status).IsNotEqualTo(KanbanStatus.Complete)); criteria.Add(Filter.Where(x => x.Job.Customer.ID).IsEqualTo(CustomerID)); //criteria.Add(new Filter(CustomerProperty).IsEqualTo(CustomerID.ToString())); if(Options.PageSize > 0) { var inSort = sort; Task.Run(() => { var page = CoreRange.Database(Options.PageSize); var filter = criteria.Combine(); IsPaging = true; while (!token.IsCancellationRequested) { try { var data = KanbanClient.Query(filter, columns, inSort, page); data.Offset = page.Offset; IsPaging = data.Rows.Count == page.Limit; if (token.IsCancellationRequested) { break; } action(data, null); if (!IsPaging) break; // Proposal - Let's slow it down a bit to enhance UI responsiveness? Thread.Sleep(100); page.Next(); } catch (Exception e) { action(null, e); break; } } }, token); } else { KanbanClient.Query(criteria.Combine(), columns, sort, null, action); } } public override Kanban[] LoadItems(IList rows) { var results = new List(rows.Count); for (var i = 0; i < rows.Count; i += ChunkSize) { var chunk = rows.Skip(i).Take(ChunkSize); var filter = Filter.Where(x => x.ID).InList(chunk.Select(x => x.Get(x => x.ID)).ToArray()); var columns = DynamicGridUtils.LoadEditorColumns(Columns.None()); var data = KanbanClient.Query(filter, columns); results.AddRange(data.ToObjects()); } return results.ToArray(); } public override Kanban LoadItem(CoreRow row) { var id = row.Get(x => x.ID); return KanbanClient.Query( Filter.Where(x => x.ID).IsEqualTo(id), DynamicGridUtils.LoadEditorColumns(Columns.None())).ToObjects().FirstOrDefault() ?? throw new Exception($"No Kanban with ID {id}"); } public override void SaveItem(Kanban item) { CheckJob(item); KanbanClient.Save(item, "Edited by User"); } private void CheckJob(Kanban item) { if (item.ID == Guid.Empty) { item.CreatedBy = App.EmployeeName; // Check if there is an open Project Job (ie installation or periodic billing) for this Client var job = JobClient.Query( Filter.Where(x => x.Customer.ID).IsEqualTo(CustomerID) .And(x => x.JobType).IsEqualTo(JobType.Project) .And(x => x.JobStatus.Active).IsEqualTo(true), Columns.None() .Add(x => x.ID) .Add(x=>x.DefaultScope.ID) ).ToObjects().FirstOrDefault(); // No Job ? Create a service job for this ticket if (job == null) { job = new Job(); job.Name = item.Title; job.Customer.ID = CustomerID; job.JobType = JobType.Service; job.Notes = item.Notes?.ToList().ToArray() ?? []; job.UserProperties.Clear(); JobClient.Save(job, "Created by Client Issues Screen"); } // Created Tickets should always have a job #! item.Job.ID = job.ID; item.JobScope.ID = job.DefaultScope.ID; } } public override void SaveItems(IEnumerable items) { var list = items.ToArray(); foreach (var item in list) CheckJob(item); KanbanClient.Save(list, "Edited by User"); } public override void DeleteItems(params CoreRow[] rows) { var deletes = new List(); foreach (var row in rows) { var delete = new Kanban { ID = row.Get(x => x.ID) }; deletes.Add(delete); } KanbanClient.Delete(deletes, "Deleted on User Request"); } #endregion }