IssuesGrid.cs 27 KB

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