IssuesGrid.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. using Comal.Classes;
  2. using InABox.Core;
  3. using InABox.DynamicGrid;
  4. using InABox.WPF;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using System.Windows.Controls;
  13. using System.Windows.Media;
  14. using InABox.Wpf;
  15. using System.Windows;
  16. using InABox.Clients;
  17. using InABox.Scripting;
  18. using Microsoft.Win32;
  19. namespace PRSDesktop.Forms.Issues;
  20. public class IssuesGrid : DynamicGrid<Kanban>, ISpecificGrid
  21. {
  22. private readonly int ChunkSize = 500;
  23. public IQueryProviderFactory ClientFactory { get; set; }
  24. private IQueryProvider<Kanban>? _kanbanClient;
  25. private IQueryProvider<Kanban> KanbanClient
  26. {
  27. get
  28. {
  29. _kanbanClient ??= ClientFactory.Create<Kanban>();
  30. return _kanbanClient;
  31. }
  32. }
  33. private IQueryProvider<Job>? _jobClient;
  34. private IQueryProvider<Job> JobClient
  35. {
  36. get
  37. {
  38. _jobClient ??= ClientFactory.Create<Job>();
  39. return _jobClient;
  40. }
  41. }
  42. public Guid CustomerID { get; set; }
  43. // public static CustomProperty CustomerProperty = new CustomProperty
  44. // {
  45. // Name = "CustomerID",
  46. // PropertyType = typeof(string),
  47. // ClassType = typeof(Kanban)
  48. // };
  49. private String _baseDirectory;
  50. public IssuesGrid() : base()
  51. {
  52. var cols = LookupFactory.DefineColumns<Kanban>();
  53. // Minimum Columns for Lookup values
  54. foreach (var col in cols)
  55. HiddenColumns.Add(col);
  56. HiddenColumns.Add(x => x.Notes);
  57. ActionColumns.Add(new DynamicMenuColumn(BuildMenu) { Position = DynamicActionColumnPosition.End });
  58. }
  59. private class UIComponent : DynamicGridGridUIComponent<Kanban>
  60. {
  61. private IssuesGrid Grid;
  62. public UIComponent(IssuesGrid grid)
  63. {
  64. Grid = grid;
  65. Parent = grid;
  66. }
  67. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  68. {
  69. var status = row.Get<Kanban, KanbanStatus>(x => x.Status);
  70. var color = status == KanbanStatus.Open
  71. ? Colors.Orange
  72. : status == KanbanStatus.InProgress
  73. ? Colors.Plum
  74. : status == KanbanStatus.Waiting
  75. ? Colors.LightGreen
  76. : Colors.Silver;
  77. return color.ToBrush(0.5);
  78. }
  79. }
  80. protected override IDynamicGridUIComponent<Kanban> CreateUIComponent()
  81. {
  82. return new UIComponent(this);
  83. }
  84. protected override void Init()
  85. {
  86. base.Init();
  87. AddButton("Check for Updates", PRSDesktop.Resources.autoupdate.AsBitmapImage(), CheckForUpdates);
  88. AddButton("Open Support Session", PRSDesktop.Resources.appicon.AsBitmapImage(), OpenSupportSession);
  89. _baseDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "";
  90. if (File.Exists(Path.Combine(_baseDirectory, "PRSAvalonia", "PRS.Avalonia.Desktop.exe")))
  91. {
  92. var btn = AddButton("PRS Mobile App", PRSDesktop.Resources.map.AsBitmapImage(), LaunchPRSMobile);
  93. btn.Margin = new Thickness(20, btn.Margin.Top, btn.Margin.Right, btn.Margin.Bottom);
  94. }
  95. if (File.Exists(Path.Combine(_baseDirectory, "PRSDigitalKey", "PRS.DigitalKey.Desktop.exe")))
  96. AddButton("PRS Digital Key App", PRSDesktop.Resources.key.AsBitmapImage(), LaunchPRSDigitalKey);
  97. var scriptButton = AddButton("Check Script Status", PRSDesktop.Resources.edit.AsBitmapImage(), CheckScriptStatus);
  98. scriptButton.Margin = new Thickness(20, scriptButton.Margin.Top, scriptButton.Margin.Right, scriptButton.Margin.Bottom);
  99. }
  100. private bool CheckScriptStatus(Button button, CoreRow[] rows)
  101. {
  102. var showHidden = MessageWindow.ShowYesNo("Include Hidden Scripts?", "Confirm");
  103. var results = new List<Tuple<CustomModule, string>>();
  104. Progress.ShowModal("Loading Data", progress =>
  105. {
  106. var filter = showHidden
  107. ? Filter.All<CustomModule>()
  108. : Filter<CustomModule>.Where(x => x.Visible).IsEqualTo(true);
  109. var modules = Client.Query(
  110. filter,
  111. Columns.None<CustomModule>()
  112. .Add(x => x.ID)
  113. .Add(x => x.Section)
  114. .Add(x => x.Name)
  115. .Add(x => x.Script))
  116. .ToArray<CustomModule>();
  117. foreach(var module in modules)
  118. {
  119. try
  120. {
  121. progress.Report($"{module.Section} -> {module.Name}");
  122. var scriptDocument = new ScriptDocument(module.Script);
  123. if (!scriptDocument.Compile())
  124. {
  125. var errors = scriptDocument.Result.Split(Environment.NewLine);
  126. results.Add(new(module, string.Join('\n', errors.Select(x => $"- {x}"))));
  127. }
  128. }
  129. catch(Exception e)
  130. {
  131. results.Add(new(module, e.Message));
  132. }
  133. }
  134. });
  135. if (results.Any())
  136. {
  137. var grid = new Grid();
  138. grid.AddRow(GridUnitType.Auto);
  139. grid.AddRow(GridUnitType.Star);
  140. grid.AddRow(GridUnitType.Auto);
  141. grid.AddChild(new Label
  142. {
  143. Content = "The following errors were found in custom modules:"
  144. }, 0, 0);
  145. var list = new ListBox();
  146. foreach(var (module, errors) in results)
  147. {
  148. var itemGrid = new Grid
  149. {
  150. };
  151. itemGrid.AddColumn(GridUnitType.Auto);
  152. itemGrid.AddColumn(GridUnitType.Star);
  153. var itemBtn = new Button
  154. {
  155. Content = new Image
  156. {
  157. Source = PRSDesktop.Resources.pencil.AsBitmapImage(),
  158. },
  159. Width = 30,
  160. Height = 30,
  161. Padding = new(5),
  162. Margin = new(0, 0, 5, 0),
  163. Tag = module
  164. };
  165. itemBtn.VerticalAlignment = VerticalAlignment.Top;
  166. itemBtn.Click += ModuleOpen_Click;
  167. itemGrid.AddChild(itemBtn, 0, 0);
  168. itemGrid.AddChild(new Label
  169. {
  170. Content = $"{module.Section}/{module.Name}:\n{errors}"
  171. }, 0, 1);
  172. list.Items.Add(itemGrid);
  173. }
  174. grid.AddChild(list, 1, 0);
  175. var dockPanel = new DockPanel
  176. {
  177. LastChildFill = false
  178. };
  179. var exportButton = new Button
  180. {
  181. Content = "Export",
  182. Padding = new(5),
  183. Margin = new(0, 5, 0, 0)
  184. };
  185. exportButton.Click += (o, e) =>
  186. {
  187. var result = string.Join("\n\n", results.Select(x => $"{x.Item1.Section}/{x.Item1.Name}:\n{x.Item2}"));
  188. var dlg = new SaveFileDialog()
  189. {
  190. Filter = "Text Files (*.txt)|*.txt"
  191. };
  192. if(dlg.ShowDialog() == true)
  193. {
  194. using var writer = new StreamWriter(dlg.FileName);
  195. writer.Write(result);
  196. }
  197. };
  198. DockPanel.SetDock(exportButton, Dock.Left);
  199. dockPanel.Children.Add(exportButton);
  200. grid.AddChild(dockPanel, 2, 0);
  201. var window = new DynamicContentDialog(grid, buttonsVisible: false)
  202. {
  203. Title = "Custom Module Errors"
  204. };
  205. window.ShowDialog();
  206. }
  207. return false;
  208. }
  209. private void ModuleOpen_Click(object sender, RoutedEventArgs e)
  210. {
  211. if (sender is not FrameworkElement element
  212. || element.Tag is not CustomModule module) return;
  213. var editor = new ScriptEditorWindow(module.Script, scriptTitle: $"{module.Section}/{module.Name}");
  214. if (editor.ShowDialog() == true)
  215. {
  216. module.Script = editor.Script;
  217. Client.Save(module, "Updated by User");
  218. }
  219. }
  220. private bool LaunchPRSMobile(Button button, CoreRow[] rows)
  221. {
  222. var _mobileApp = System.IO.Path.Combine(_baseDirectory, "PRSAvalonia", "PRS.Avalonia.Desktop.exe");
  223. var _info = new ProcessStartInfo(_mobileApp);
  224. Process.Start(_info);
  225. return false;
  226. }
  227. private bool LaunchPRSDigitalKey(Button button, CoreRow[] rows)
  228. {
  229. var _mobileApp = Path.Combine(_baseDirectory, "PRSDigitalKey", "PRS.DigitalKey.Desktop.exe");
  230. var _info = new ProcessStartInfo(_mobileApp);
  231. Process.Start(_info);
  232. return false;
  233. }
  234. private bool OpenSupportSession(Button button, CoreRow[] rows)
  235. {
  236. SupportUtils.OpenSupportSession();
  237. return false;
  238. }
  239. private bool CheckForUpdates(Button button, CoreRow[] rows)
  240. {
  241. if (SupportUtils.CheckForUpdates())
  242. {
  243. Application.Current.Shutdown();
  244. }
  245. else
  246. {
  247. if (MessageWindow.ShowYesNo(
  248. "You appear to be using the latest version already!\n\nRun the installer anyway?", "Update"))
  249. {
  250. if (SupportUtils.DownloadAndRunInstaller())
  251. {
  252. Application.Current.Shutdown();
  253. }
  254. }
  255. }
  256. return false;
  257. }
  258. protected override void DoReconfigure(DynamicGridOptions options)
  259. {
  260. options.Clear();
  261. options.AddRows = true;
  262. options.EditRows = true;
  263. options.FilterRows = true;
  264. options.HideDatabaseFilters = true;
  265. }
  266. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  267. {
  268. if (row is null) return;
  269. var menu = column.GetMenu();
  270. menu.AddItem("Add note", null, row, AddNote_Click);
  271. menu.AddItem("Attach system logs", null, row, AttachLogs_Click);
  272. menu.AddSeparator();
  273. menu.AddItem("Close issue", null, row, CloseTask_Click);
  274. }
  275. private void AttachLogs_Click(CoreRow row)
  276. {
  277. var logFile = CoreUtils.GetLogFile();
  278. var data = File.ReadAllBytes(logFile);
  279. var doc = new Document();
  280. doc.Data = data;
  281. doc.CRC = CoreUtils.CalculateCRC(data);
  282. doc.FileName = Path.GetFileName(logFile);
  283. doc.TimeStamp = File.GetLastWriteTime(logFile);
  284. ClientFactory.Save(doc, "Attached logs to task.");
  285. var kanbanDocument = new KanbanDocument();
  286. kanbanDocument.DocumentLink.CopyFrom(doc);
  287. kanbanDocument.EntityLink.CopyFrom(row.ToObject<Kanban>());
  288. ClientFactory.Save(kanbanDocument, "Attached logs to task.");
  289. }
  290. public override Kanban CreateItem()
  291. {
  292. var item = base.CreateItem();
  293. item.UserProperties["CustomerID"] = CustomerID.ToString();
  294. item.Notes = [
  295. $"Created on PRS {CoreUtils.GetVersion()} by {App.EmployeeName} ({App.EmployeeEmail})"
  296. ];
  297. // item.Status = KanbanStatus.Open;
  298. return item;
  299. }
  300. private void AddNote_Click(CoreRow row)
  301. {
  302. var kanban = row.ToObject<Kanban>();
  303. var text = "";
  304. if(TextBoxDialog.Execute("Enter note:", ref text))
  305. {
  306. text = string.Format("{0:yyyy-MM-dd HH:mm:ss}: {1}", DateTime.Now, text);
  307. kanban.Notes = kanban.Notes.Concatenate([text]);
  308. kanban.Status = KanbanStatus.InProgress;
  309. SaveItem(kanban);
  310. Refresh(false, true);
  311. }
  312. }
  313. private void CloseTask_Click(CoreRow row)
  314. {
  315. var kanban = row.ToObject<Kanban>();
  316. kanban.Completed = DateTime.Now;
  317. kanban.Closed = DateTime.Now;
  318. SaveItem(kanban);
  319. Refresh(false, true);
  320. }
  321. private Column<Kanban>[] AllowedColumns = [
  322. new(x => x.Number),
  323. new(x => x.Title),
  324. new(x => x.Description),
  325. new(x => x.Notes)];
  326. protected override void CustomiseEditor(IDynamicEditorForm form, Kanban[] items, DynamicGridColumn column, BaseEditor editor)
  327. {
  328. base.CustomiseEditor(form, items, column, editor);
  329. if(!AllowedColumns.Any(x => x.Property == column.ColumnName))
  330. {
  331. editor.Editable = editor.Editable.Combine(Editable.Hidden);
  332. }
  333. }
  334. public virtual CoreTable LookupValues(DataLookupEditor editor, Type parent, string columnname, BaseObject[]? items)
  335. {
  336. var client = ClientFactory.Create(editor.Type);
  337. var filter = LookupFactory.DefineLookupFilter(parent, editor.Type, columnname, items ?? (Array.CreateInstance(parent, 0) as BaseObject[])!);
  338. var columns = LookupFactory.DefineLookupColumns(parent, editor.Type, columnname);
  339. foreach (var key in editor.OtherColumns.Keys)
  340. columns.Add(key);
  341. var sort = LookupFactory.DefineSort(editor.Type);
  342. var result = client.Query(filter, columns, sort);
  343. result.Columns.Add(new CoreColumn { ColumnName = "Display", DataType = typeof(string) });
  344. foreach (var row in result.Rows)
  345. {
  346. row["Display"] = LookupFactory.FormatLookup(parent, editor.Type, row, columnname);
  347. }
  348. return result;
  349. }
  350. protected override void DefineLookups(ILookupEditorControl sender, Kanban[] items, bool async = true)
  351. {
  352. if (sender.EditorDefinition is not DataLookupEditor editor)
  353. {
  354. base.DefineLookups(sender, items, async: async);
  355. return;
  356. }
  357. var colname = sender.ColumnName;
  358. if (async)
  359. {
  360. Task.Run(() =>
  361. {
  362. try
  363. {
  364. var values = LookupValues(editor, typeof(Kanban), colname, items);
  365. Dispatcher.Invoke(
  366. () =>
  367. {
  368. try
  369. {
  370. //Logger.Send(LogType.Information, typeof(T).Name, "Dispatching Results" + colname);
  371. sender.LoadLookups(values);
  372. }
  373. catch (Exception e2)
  374. {
  375. Logger.Send(LogType.Information, typeof(Kanban).Name,
  376. "Exception (2) in LoadLookups: " + e2.Message + "\n" + e2.StackTrace);
  377. }
  378. }
  379. );
  380. }
  381. catch (Exception e)
  382. {
  383. Logger.Send(LogType.Information, typeof(Kanban).Name,
  384. "Exception (1) in LoadLookups: " + e.Message + "\n" + e.StackTrace);
  385. }
  386. });
  387. }
  388. else
  389. {
  390. var values = LookupValues(editor, typeof(Kanban), colname, items);
  391. sender.LoadLookups(values);
  392. }
  393. }
  394. public override DynamicEditorPages LoadEditorPages(Kanban item)
  395. {
  396. var pages = new DynamicEditorPages
  397. {
  398. new DynamicDocumentGrid<KanbanDocument, Kanban, KanbanLink>
  399. {
  400. Client = ClientFactory
  401. }
  402. };
  403. return pages;
  404. }
  405. protected override DynamicGridColumns LoadColumns()
  406. {
  407. var columns = new DynamicGridColumns<Kanban>();
  408. columns.Add(x => x.Number, caption: "Ticket", width: 60, alignment: Alignment.MiddleCenter);
  409. columns.Add(x => x.Title);
  410. columns.Add(x => x.CreatedBy, caption: "Created By", width: 150);
  411. columns.Add(x => x.EmployeeLink.Name, caption: "Assigned To", width: 150);
  412. columns.Add(x => x.Type.Description, caption: "Type", width: 100, alignment: Alignment.MiddleCenter);
  413. columns.Add(x => x.Status, caption: "Status", width: 80, alignment: Alignment.MiddleCenter);
  414. return columns;
  415. }
  416. #region Grid Stuff
  417. protected override string FormatRecordCount(int count)
  418. {
  419. return IsPaging
  420. ? $"{base.FormatRecordCount(count)} (loading..)"
  421. : base.FormatRecordCount(count);
  422. }
  423. protected override void Reload(
  424. Filters<Kanban> criteria, Columns<Kanban> columns, ref SortOrder<Kanban>? sort,
  425. CancellationToken token, Action<CoreTable?, Exception?> action)
  426. {
  427. criteria.Add(Filter<Kanban>.Where(x => x.Closed).IsEqualTo(DateTime.MinValue));
  428. criteria.Add(Filter<Kanban>.Where(x => x.Status).IsNotEqualTo(KanbanStatus.Complete));
  429. criteria.Add(Filter<Kanban>.Where(x => x.Job.Customer.ID).IsEqualTo(CustomerID));
  430. //criteria.Add(new Filter<Kanban>(CustomerProperty).IsEqualTo(CustomerID.ToString()));
  431. if(Options.PageSize > 0)
  432. {
  433. var inSort = sort;
  434. Task.Run(() =>
  435. {
  436. var page = CoreRange.Database(Options.PageSize);
  437. var filter = criteria.Combine();
  438. IsPaging = true;
  439. while (!token.IsCancellationRequested)
  440. {
  441. try
  442. {
  443. var data = KanbanClient.Query(filter, columns, inSort, page);
  444. data.Offset = page.Offset;
  445. IsPaging = data.Rows.Count == page.Limit;
  446. if (token.IsCancellationRequested)
  447. {
  448. break;
  449. }
  450. action(data, null);
  451. if (!IsPaging)
  452. break;
  453. // Proposal - Let's slow it down a bit to enhance UI responsiveness?
  454. Thread.Sleep(100);
  455. page.Next();
  456. }
  457. catch (Exception e)
  458. {
  459. action(null, e);
  460. break;
  461. }
  462. }
  463. }, token);
  464. }
  465. else
  466. {
  467. KanbanClient.Query(criteria.Combine(), columns, sort, null, action);
  468. }
  469. }
  470. public override Kanban[] LoadItems(IList<CoreRow> rows)
  471. {
  472. var results = new List<Kanban>(rows.Count);
  473. for (var i = 0; i < rows.Count; i += ChunkSize)
  474. {
  475. var chunk = rows.Skip(i).Take(ChunkSize);
  476. var filter = Filter<Kanban>.Where(x => x.ID).InList(chunk.Select(x => x.Get<Kanban, Guid>(x => x.ID)).ToArray());
  477. var columns = DynamicGridUtils.LoadEditorColumns(Columns.None<Kanban>());
  478. var data = KanbanClient.Query(filter, columns);
  479. results.AddRange(data.ToObjects<Kanban>());
  480. }
  481. return results.ToArray();
  482. }
  483. public override Kanban LoadItem(CoreRow row)
  484. {
  485. var id = row.Get<Kanban, Guid>(x => x.ID);
  486. return KanbanClient.Query(
  487. Filter<Kanban>.Where(x => x.ID).IsEqualTo(id),
  488. DynamicGridUtils.LoadEditorColumns(Columns.None<Kanban>())).ToObjects<Kanban>().FirstOrDefault()
  489. ?? throw new Exception($"No Kanban with ID {id}");
  490. }
  491. public override void SaveItem(Kanban item)
  492. {
  493. CheckJob(item);
  494. KanbanClient.Save(item, "Edited by User");
  495. }
  496. private void CheckJob(Kanban item)
  497. {
  498. if (item.ID == Guid.Empty)
  499. {
  500. item.CreatedBy = App.EmployeeName;
  501. // Check if there is an open Project Job (ie installation or periodic billing) for this Client
  502. var job = JobClient.Query(
  503. Filter<Job>.Where(x => x.Customer.ID).IsEqualTo(CustomerID)
  504. .And(x => x.JobType).IsEqualTo(JobType.Project)
  505. .And(x => x.JobStatus.Active).IsEqualTo(true),
  506. Columns.None<Job>()
  507. .Add(x => x.ID)
  508. .Add(x=>x.DefaultScope.ID)
  509. ).ToObjects<Job>().FirstOrDefault();
  510. // No Job ? Create a service job for this ticket
  511. if (job == null)
  512. {
  513. job = new Job();
  514. job.Name = item.Title;
  515. job.Customer.ID = CustomerID;
  516. job.JobType = JobType.Service;
  517. job.Notes = item.Notes?.ToList().ToArray() ?? [];
  518. job.UserProperties.Clear();
  519. JobClient.Save(job, "Created by Client Issues Screen");
  520. }
  521. // Created Tickets should always have a job #!
  522. item.Job.ID = job.ID;
  523. item.JobScope.ID = job.DefaultScope.ID;
  524. }
  525. }
  526. public override void SaveItems(IEnumerable<Kanban> items)
  527. {
  528. var list = items.ToArray();
  529. foreach (var item in list)
  530. CheckJob(item);
  531. KanbanClient.Save(list, "Edited by User");
  532. }
  533. public override void DeleteItems(params CoreRow[] rows)
  534. {
  535. var deletes = new List<Kanban>();
  536. foreach (var row in rows)
  537. {
  538. var delete = new Kanban
  539. {
  540. ID = row.Get<Kanban, Guid>(x => x.ID)
  541. };
  542. deletes.Add(delete);
  543. }
  544. KanbanClient.Delete(deletes, "Deleted on User Request");
  545. }
  546. #endregion
  547. }