TaskPanel.xaml.cs 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using Comal.Classes;
  9. using InABox.Clients;
  10. using InABox.Configuration;
  11. using InABox.Core;
  12. using InABox.DynamicGrid;
  13. using InABox.WPF;
  14. using Syncfusion.Pdf.Graphics;
  15. using Syncfusion.Pdf;
  16. using System.Drawing;
  17. using System.ComponentModel;
  18. namespace PRSDesktop
  19. {
  20. public class TaskPanelProperties : BaseObject, IGlobalConfigurationSettings
  21. {
  22. [CheckBoxEditor(ToolTip = "Require that all tasks are given a task type.")]
  23. public bool RequireTaskTypes { get; set; } = false;
  24. }
  25. /// <summary>
  26. /// Interaction logic for TaskPanel.xaml
  27. /// </summary>
  28. public partial class TaskPanel : UserControl, IPanel<Kanban>, ITaskHost, IJobControl, IPropertiesPanel<TaskPanelProperties>
  29. {
  30. private bool _bTabChanging;
  31. private KanbanType[] kanbanTypes = null!; // Initialized in Setup()
  32. public IList<KanbanType> KanbanTypes => kanbanTypes;
  33. public Job Job { get; set; }
  34. public JobPanelSettings Settings { get; set; }
  35. public TaskPanel()
  36. {
  37. InitializeComponent();
  38. foreach (TabItem tab in TaskPanels.Items)
  39. {
  40. var panel = (tab.Content as ITaskControl)!;
  41. _viewmap[panel.KanbanViewType] = tab;
  42. panel.Host = this;
  43. }
  44. }
  45. #region Menu
  46. private void CompleteTask(ITaskControl control, RoutedEventArgs e, DateTime completed)
  47. {
  48. if (MessageBox.Show($"Are you sure you want to complete the selected tasks?", "Confirm Completion",
  49. MessageBoxButton.YesNo) != MessageBoxResult.Yes)
  50. return;
  51. var tasks = (((FrameworkElement)e.Source).Tag as IEnumerable<TaskModel>)!;
  52. Progress.ShowModal("Completing Tasks", progress =>
  53. {
  54. var kanbans = LoadKanbans(tasks, new Columns<Kanban>(x => x.ID, x => x.Completed, x => x.Category));
  55. foreach (var kanban in kanbans)
  56. {
  57. kanban.Completed = completed;
  58. kanban.Category = "Complete";
  59. }
  60. new Client<Kanban>().Save(kanbans, $"Kanban Marked as Complete");
  61. });
  62. control.Refresh(true);
  63. }
  64. private void AddChangeStatusButton(ITaskControl control, TaskModel[] models, MenuItem menu, string header, string status)
  65. {
  66. menu.AddItem(header, null, Tuple.Create(control, models, status), ChangeStatus_Click);
  67. }
  68. private void ChangeStatus_Click(Tuple<ITaskControl, TaskModel[], string> obj)
  69. {
  70. var (control, tasks, status) = obj;
  71. if (MessageBox.Show($"Are you sure you want to mark the selected tasks as {status}?", "Confirm Change Status",
  72. MessageBoxButton.YesNo) != MessageBoxResult.Yes)
  73. return;
  74. Progress.ShowModal("Changing Status", progress =>
  75. {
  76. var kanbans = LoadKanbans(tasks, new Columns<Kanban>(x => x.ID, x => x.Completed, x => x.Category));
  77. foreach (var kanban in kanbans)
  78. {
  79. if (status == "Complete")
  80. {
  81. kanban.Completed = DateTime.Now;
  82. }
  83. kanban.Category = status;
  84. }
  85. new Client<Kanban>().Save(kanbans, $"Kanban Marked as {status}");
  86. });
  87. control.Refresh(true);
  88. }
  89. public bool CanChangeTasks(IEnumerable<TaskModel> models)
  90. {
  91. foreach (var task in models)
  92. {
  93. if (!App.EmployeeID.Equals(task.ManagerID) && !App.EmployeeID.Equals(task.EmployeeID))
  94. {
  95. // If you can change others tasks, IsFullControl is true - but we don't check at the beginning of the function
  96. // to save checking security tokens every time.
  97. return Security.IsAllowed<CanChangeOthersTasks>();
  98. }
  99. }
  100. return true;
  101. }
  102. public void PopulateMenu(ITaskControl control, TaskModel task, ContextMenu menu)
  103. {
  104. menu.Items.Clear();
  105. var models = control.SelectedModels(task).ToArray();
  106. var references = GetReferences(models);
  107. var bLinks = references.Any(x => x.ReferenceType() != null);
  108. var referencetypes = references.Select(x => x.ReferenceType()).Distinct().ToArray();
  109. var bSingle = models.Length == 1;
  110. var canChange = CanChangeTasks(models);
  111. var edit = new MenuItem
  112. {
  113. Tag = models,
  114. Header = referencetypes.SingleOrDefault() == typeof(Requisition)
  115. ? "Edit Requisition Details"
  116. : referencetypes.SingleOrDefault() == typeof(Setout)
  117. ? "Edit Setout Details"
  118. : referencetypes.SingleOrDefault() == typeof(Delivery)
  119. ? "Edit Delivery Details"
  120. : referencetypes.SingleOrDefault() == typeof(PurchaseOrder)
  121. ? "Edit Order Details"
  122. : "Edit Task" + (bSingle ? "" : "s")
  123. };
  124. edit.Click += (o, e) =>
  125. {
  126. var tasks = (((MenuItem)e.Source).Tag as IEnumerable<TaskModel>)!;
  127. if (EditReferences(tasks))
  128. control.Refresh(true);
  129. e.Handled = true;
  130. };
  131. edit.IsEnabled = referencetypes.Length == 1;
  132. menu.Items.Add(edit);
  133. if (!bLinks && models.Length == 1)
  134. {
  135. var digitalForms = new MenuItem { Header = "Digital Forms" };
  136. var model = models.First();
  137. DynamicGridUtils.PopulateFormMenu<KanbanForm, Kanban, KanbanLink>(
  138. digitalForms,
  139. model.ID,
  140. () => new Client<Kanban>().Load(new Filter<Kanban>(x => x.ID).IsEqualTo(model.ID)).First(),
  141. model.EmployeeID == App.EmployeeID);
  142. menu.Items.Add(digitalForms);
  143. }
  144. if (!models.Any(x => !x.CompletedDate.IsEmpty()) && !bLinks)
  145. {
  146. menu.Items.Add(new Separator());
  147. var job = new MenuItem
  148. {
  149. Tag = models,
  150. Header = "Link to Job"
  151. };
  152. job.SubmenuOpened += (o, e) => CreateJobSubMenu(control, job, models);
  153. menu.Items.Add(job);
  154. if (bSingle)
  155. {
  156. menu.AddItem("Create Setout from Task", null, models.First(), task =>
  157. {
  158. if (MessageBox.Show("This will convert this task into a Setout.\n\nDo you wish to continue?", "Confirmation", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
  159. return;
  160. ManufacturingTemplate? template = new Client<ManufacturingTemplate>()
  161. .Load(new Filter<ManufacturingTemplate>(x => x.Code).IsEqualTo("PRS")).FirstOrDefault();
  162. if (template == null)
  163. {
  164. MessageBox.Show("[Pressing] Template does not exist!");
  165. return;
  166. }
  167. string? setoutNumber = null;
  168. Kanban? kanban = null;
  169. ManufacturingTemplateStage[] tstages = Array.Empty<ManufacturingTemplateStage>();
  170. Progress.ShowModal("Creating Setout", (progress) =>
  171. {
  172. var kanbanFilter = new Filter<Kanban>(x => x.ID).IsEqualTo(task.ID);
  173. var tables = Client.QueryMultiple(new Dictionary<string, IQueryDef>
  174. {
  175. { "ManufacturingTemplateStage", new QueryDef<ManufacturingTemplateStage>(
  176. new Filter<ManufacturingTemplateStage>(x => x.Template.ID).IsEqualTo(template.ID),
  177. null,
  178. new SortOrder<ManufacturingTemplateStage>(x => x.Sequence)) },
  179. { "Kanban", new QueryDef<Kanban>(
  180. kanbanFilter,
  181. null,
  182. null) },
  183. { "Setout", new QueryDef<Setout>(
  184. new Filter<Setout>(x => x.JobLink.ID)
  185. .InQuery(new SubQuery<Kanban>(kanbanFilter, new Column<Kanban>(x => x.JobLink.ID))),
  186. new Columns<Setout>(x => x.JobLink.JobNumber, x => x.Number),
  187. null) }
  188. });
  189. tstages = tables["ManufacturingTemplateStage"].Rows
  190. .Select(x => x.ToObject<ManufacturingTemplateStage>()).ToArray();
  191. kanban = tables["Kanban"].Rows.FirstOrDefault()?.ToObject<Kanban>();
  192. if (kanban == null)
  193. {
  194. MessageBox.Show("Task does not exist!");
  195. return;
  196. }
  197. progress.Report("Creating Setouts");
  198. CoreTable setouts = tables["Setout"];
  199. int ireq = 0;
  200. string sreq = "";
  201. while (true)
  202. {
  203. ireq++;
  204. sreq = string.Format("{0}-{1:yyMMdd}-{2}", kanban.JobLink.JobNumber, DateTime.Now, ireq);
  205. if (!setouts.Rows.Any(r => sreq.Equals(r.Get<Setout, String>(c => c.Number))))
  206. break;
  207. }
  208. setoutNumber = sreq;
  209. });
  210. if (setoutNumber == null || kanban == null)
  211. {
  212. return;
  213. }
  214. var result = CreateSetout(
  215. task,
  216. s =>
  217. {
  218. s.Number = setoutNumber;
  219. s.JobLink.ID = task.JobID;
  220. var notes = kanban.Notes.ToList();
  221. var description = kanban.Summary;
  222. if (string.IsNullOrWhiteSpace(description))
  223. {
  224. description = CoreUtils.StripHTML(kanban.Description);
  225. }
  226. if (!string.IsNullOrWhiteSpace(description))
  227. {
  228. notes.Insert(0, description);
  229. }
  230. s.Description = string.Join("\n==========================================\n", notes);
  231. }
  232. );
  233. if (result != null)
  234. {
  235. Progress.ShowModal("Creating Manufacturing Packet", progress =>
  236. {
  237. ManufacturingPacket packet = new ManufacturingPacket()
  238. {
  239. Serial = template.Code,
  240. Title = kanban.Title,
  241. Quantity = 1,
  242. BarcodeQty = 1,
  243. DueDate = kanban.DueDate
  244. };
  245. packet.ManufacturingTemplateLink.ID = template.ID;
  246. packet.ManufacturingTemplateLink.Code = template.Code;
  247. packet.ManufacturingTemplateLink.Factory.ID = template.Factory.ID;
  248. packet.SetoutLink.ID = result.ID;
  249. new Client<ManufacturingPacket>().Save(packet, "Created from Task");
  250. DoLink<ManufacturingPacketKanban, ManufacturingPacket, ManufacturingPacketLink>(task, packet.ID);
  251. List<ManufacturingPacketStage> pstages = new List<ManufacturingPacketStage>();
  252. foreach (var tstage in tstages)
  253. {
  254. var pstage = new ManufacturingPacketStage()
  255. {
  256. Time = tstage.Time,
  257. Sequence = tstage.Sequence,
  258. SequenceType = tstage.SequenceType,
  259. Started = DateTime.MinValue,
  260. PercentageComplete = 0.0F,
  261. Completed = DateTime.MinValue,
  262. QualityChecks = tstage.QualityChecks,
  263. QualityStatus = QualityStatus.NotChecked,
  264. QualityNotes = "",
  265. };
  266. pstage.Parent.ID = packet.ID;
  267. pstage.ManufacturingSectionLink.ID = tstage.Section.ID;
  268. pstage.ManufacturingSectionLink.Name = tstage.Section.Name;
  269. pstages.Add(pstage);
  270. }
  271. new Client<ManufacturingPacketStage>().Save(pstages, "Created from Task", (_, __) => { });
  272. progress.Report("Processing Documents");
  273. List<SetoutDocument> _setoutdocuments = new List<SetoutDocument>();
  274. List<KanbanDocument> _kanbandocuments = new List<KanbanDocument>();
  275. KanbanDocument[] docrefs = new Client<KanbanDocument>()
  276. .Load(new Filter<KanbanDocument>(x => x.EntityLink.ID).IsEqualTo(kanban.ID));
  277. foreach (var docref in docrefs)
  278. {
  279. // Convert the document to a PDF
  280. var docid = ProcessKanbanDocument(docref);
  281. var newdoc = new SetoutDocument();
  282. newdoc.EntityLink.ID = result.ID;
  283. newdoc.DocumentLink.ID = docid;
  284. _setoutdocuments.Add(newdoc);
  285. if (docid != docref.DocumentLink.ID)
  286. {
  287. docref.DocumentLink.ID = docid;
  288. _kanbandocuments.Add(docref);
  289. }
  290. }
  291. new Client<SetoutDocument>().Save(_setoutdocuments, "Converted from Task", (_, __) => { });
  292. new Client<KanbanDocument>().Save(_kanbandocuments, "Converted to PDF", (_, __) => { });
  293. progress.Report("Updating Task");
  294. kanban.Title = kanban.Title + " (" + result.Number + ")";
  295. new Client<Kanban>().Save(kanban, "Converting Kanban to Setout");
  296. });
  297. control.Refresh(true);
  298. }
  299. });
  300. menu.AddItem("Create Requisition from Task", null, models, tasks =>
  301. {
  302. var taskModel = tasks.First();
  303. var kanbanTable = new Client<Kanban>().Query(new Filter<Kanban>(x => x.ID).IsEqualTo(taskModel.ID));
  304. var kanban = kanbanTable.Rows.First().ToObject<Kanban>();
  305. var result = CreateRequisition(
  306. taskModel,
  307. r =>
  308. {
  309. r.RequestedBy.ID = kanban.ManagerLink.ID;
  310. r.Employee.ID = Guid.Empty;
  311. r.Title = kanban.Title;
  312. r.Request = string.IsNullOrWhiteSpace(kanban.Summary)
  313. ? String.IsNullOrWhiteSpace(kanban.Description)
  314. ? String.Join("\n", kanban.Notes)
  315. : CoreUtils.StripHTML(kanban.Description)
  316. : kanban.Summary;
  317. r.Notes = kanban.Notes;
  318. r.Due = kanban.DueDate;
  319. r.JobLink.ID = taskModel.JobID;
  320. }
  321. );
  322. if (result != null)
  323. {
  324. Progress.ShowModal("Updating Documents", progress =>
  325. {
  326. progress.Report("Updating Documents");
  327. List<RequisitionDocument> requiDocuments = new();
  328. KanbanDocument[] kanbanDocuments = new Client<KanbanDocument>()
  329. .Load(new Filter<KanbanDocument>(x => x.EntityLink.ID).IsEqualTo(kanban.ID));
  330. foreach (var document in kanbanDocuments)
  331. {
  332. var newdoc = new RequisitionDocument();
  333. newdoc.EntityLink.ID = result.ID;
  334. newdoc.DocumentLink.ID = document.DocumentLink.ID;
  335. requiDocuments.Add(newdoc);
  336. }
  337. new Client<RequisitionDocument>().Save(requiDocuments, "Converted from Task", (_, __) => { });
  338. /*RequisitionKanban link = new();
  339. link.Entity.ID = result.ID;
  340. link.Kanban.ID = kanban.ID;
  341. new Client<RequisitionKanban>().Save(link, "Converting Task -> Requisition", (_, __) => { });*/
  342. progress.Report("Updating Task");
  343. kanban.Category = "Open";
  344. kanban.Completed = DateTime.MinValue;
  345. kanban.Title += $" (Requi #{result.Number})";
  346. new Client<Kanban>().Save(kanban, "Converted to Requisition", (_, __) => { });
  347. });
  348. MessageBox.Show(String.Format("Created Requisition {0}", result.Number));
  349. control.Refresh(true);
  350. }
  351. });
  352. menu.AddItem("Create Delivery from Task", null, models, tasks =>
  353. {
  354. var result = CreateDelivery(
  355. tasks.First(),
  356. d =>
  357. {
  358. // Post-Process Requi Here
  359. }
  360. );
  361. if (result != null)
  362. control.Refresh(true);
  363. });
  364. menu.AddItem("Create Purchase Order from Task", null, models, tasks =>
  365. {
  366. var result = CreateOrder(
  367. tasks.First(),
  368. p =>
  369. {
  370. // Post-Process Requi Here
  371. }
  372. );
  373. if (result != null)
  374. control.Refresh(true);
  375. });
  376. }
  377. }
  378. if (!bLinks && canChange)
  379. {
  380. menu.Items.Add(new Separator());
  381. var changeStatus = new MenuItem { Header = "Change Status" };
  382. AddChangeStatusButton(control, models, changeStatus, "Open", "Open");
  383. AddChangeStatusButton(control, models, changeStatus, "In Progress", "In Progress");
  384. AddChangeStatusButton(control, models, changeStatus, "Waiting", "Waiting");
  385. if (models.Any(x => x.CompletedDate.IsEmpty()))
  386. {
  387. var complete = new MenuItem
  388. {
  389. Tag = models,
  390. Header = models.Length > 1 ? "Complete Tasks" : "Complete Task"
  391. };
  392. complete.Click += (o, e) =>
  393. {
  394. CompleteTask(control, e, DateTime.Now);
  395. };
  396. menu.Items.Add(complete);
  397. if (Security.IsAllowed<CanSetKanbanCompletedDate>())
  398. {
  399. var completeDate = new MenuItem
  400. {
  401. Tag = models,
  402. Header = "Set Completed Date"
  403. };
  404. var dateItem = new MenuItem();
  405. var dateCalendar = new System.Windows.Controls.Calendar { SelectedDate = DateTime.MinValue };
  406. dateCalendar.Tag = models;
  407. dateCalendar.SelectedDatesChanged += (o, e) =>
  408. {
  409. if (e.Source is not System.Windows.Controls.Calendar calendar) return;
  410. menu.IsOpen = false;
  411. var selectedDate = calendar.SelectedDate ?? DateTime.Now;
  412. CompleteTask(control, e, selectedDate);
  413. };
  414. dateItem.Header = dateCalendar;
  415. dateItem.Style = Resources["calendarItem"] as Style;
  416. completeDate.Items.Add(dateItem);
  417. menu.Items.Add(completeDate);
  418. }
  419. }
  420. else
  421. {
  422. menu.AddItem(models.Length > 1 ? "Archive Tasks" : "Archive Task", null, models, tasks =>
  423. {
  424. if (MessageBox.Show("Are you sure you want to remove the selected tasks from the list?", "Confirm removal",
  425. MessageBoxButton.YesNo) != MessageBoxResult.Yes)
  426. return;
  427. Progress.ShowModal("Closing Kanbans", progress =>
  428. {
  429. var kanbans = LoadKanbans(tasks, new Columns<Kanban>(x => x.ID, x => x.Closed));
  430. foreach (var kanban in kanbans)
  431. kanban.Closed = DateTime.Now;
  432. new Client<Kanban>().Save(kanbans, "Kanban Marked as Closed");
  433. });
  434. control.Refresh(true);
  435. });
  436. }
  437. menu.Items.Add(changeStatus);
  438. var changeType = new MenuItem { Header = "Change Task Type", Tag = models };
  439. foreach(var type in KanbanTypes)
  440. {
  441. changeType.AddItem($"{type.Code}: {type.Description}", null, type, type =>
  442. {
  443. Progress.ShowModal("Changing Task Type", progress =>
  444. {
  445. var kanbans = LoadKanbans(models, new Columns<Kanban>(x => x.ID, x => x.Type.ID));
  446. foreach (var kanban in kanbans)
  447. {
  448. kanban.Type.ID = type.ID;
  449. }
  450. new Client<Kanban>().Save(kanbans, $"Kanban Task Type changed to {type}");
  451. });
  452. control.Refresh(true);
  453. });
  454. }
  455. menu.Items.Add(changeType);
  456. var changeDueDate = new MenuItem { Header = "Change Due Date" };
  457. var calendarItem = new MenuItem();
  458. var calendar = new System.Windows.Controls.Calendar { SelectedDate = models.Length == 1 ? models[0].DueDate : DateTime.Today };
  459. calendar.Tag = models;
  460. calendar.SelectedDatesChanged += (o, e) =>
  461. {
  462. if (e.Source is not System.Windows.Controls.Calendar calendar) return;
  463. var selectedDate = calendar.SelectedDate ?? DateTime.Now;
  464. var models = (calendar.Tag as IList<TaskModel>)!;
  465. Progress.ShowModal("Changing Due Date", progress =>
  466. {
  467. var kanbans = LoadKanbans(models, new Columns<Kanban>(x => x.ID, x => x.DueDate));
  468. foreach (var kanban in kanbans)
  469. {
  470. kanban.DueDate = selectedDate;
  471. }
  472. new Client<Kanban>().Save(kanbans, $"Kanban Due Date changed to {selectedDate:dd MMM yyyy}");
  473. });
  474. control.Refresh(true);
  475. menu.IsOpen = false;
  476. };
  477. calendarItem.Header = calendar;
  478. calendarItem.Style = Resources["calendarItem"] as Style;
  479. changeDueDate.Items.Add(calendarItem);
  480. menu.Items.Add(changeDueDate);
  481. }
  482. }
  483. /// <summary>
  484. /// Takes a <see cref="KanbanDocument"/>, and if it is a .txt or an image (".png", ".jpg", ".jpeg" or ".bmp"), converts to a PDF
  485. /// with the content of the document, saving a new document with extension changed to ".pdf".
  486. /// </summary>
  487. /// <param name="docref">The original document.</param>
  488. /// <returns>
  489. /// The ID of the new <see cref="Document"/> or,
  490. /// if not one of the given types, the original document ID.
  491. /// </returns>
  492. private static Guid ProcessKanbanDocument(KanbanDocument docref)
  493. {
  494. var result = docref.DocumentLink.ID;
  495. var ext = System.IO.Path.GetExtension(docref.DocumentLink.FileName).ToLower();
  496. if (ext.EndsWith("txt"))
  497. {
  498. var doc = new Client<Document>().Load(new Filter<Document>(x => x.ID).IsEqualTo(docref.DocumentLink.ID)).FirstOrDefault();
  499. if (doc is null)
  500. {
  501. Logger.Send(LogType.Error, "", $"Document {docref.DocumentLink.ID} does not exist!");
  502. return docref.DocumentLink.ID;
  503. }
  504. PdfDocument pdf = new PdfDocument();
  505. PdfPage page = pdf.Pages.Add();
  506. PdfGraphics graphics = page.Graphics;
  507. PdfFont font = new PdfStandardFont(PdfFontFamily.Courier, 12);
  508. String text = System.Text.Encoding.UTF8.GetString(doc.Data);
  509. graphics.DrawString(text, font, PdfBrushes.Black, new PointF(0, 0));
  510. MemoryStream ms = new MemoryStream();
  511. pdf.Save(ms);
  512. pdf.Close(true);
  513. byte[] data = ms.ToArray();
  514. var newdoc = new Document()
  515. {
  516. Data = data,
  517. FileName = System.IO.Path.ChangeExtension(docref.DocumentLink.FileName, "pdf"),
  518. CRC = CoreUtils.CalculateCRC(data),
  519. TimeStamp = DateTime.Now,
  520. };
  521. new Client<Document>().Save(newdoc, "Converted from Text");
  522. return newdoc.ID;
  523. }
  524. else if (ext.EndsWith("png") || ext.EndsWith("bmp") || ext.EndsWith("jpg") || ext.EndsWith("jpeg"))
  525. {
  526. var doc = new Client<Document>().Load(new Filter<Document>(x => x.ID).IsEqualTo(docref.DocumentLink.ID)).FirstOrDefault();
  527. if (doc is null)
  528. {
  529. Logger.Send(LogType.Error, "", $"Document {docref.DocumentLink.ID} does not exist!");
  530. return docref.DocumentLink.ID;
  531. }
  532. PdfBitmap image = new PdfBitmap(new MemoryStream(doc.Data));
  533. PdfDocument pdf = new PdfDocument();
  534. pdf.PageSettings.Orientation = image.Height > image.Width ? PdfPageOrientation.Portrait : PdfPageOrientation.Landscape;
  535. pdf.PageSettings.Size = new SizeF(image.Width, image.Height);
  536. PdfPage page = pdf.Pages.Add();
  537. PdfGraphics graphics = page.Graphics;
  538. graphics.DrawImage(image, 0.0F, 0.0F);
  539. MemoryStream ms = new MemoryStream();
  540. pdf.Save(ms);
  541. pdf.Close(true);
  542. byte[] data = ms.ToArray();
  543. var newdoc = new Document()
  544. {
  545. Data = data,
  546. FileName = System.IO.Path.ChangeExtension(docref.DocumentLink.FileName, "pdf"),
  547. CRC = CoreUtils.CalculateCRC(data),
  548. TimeStamp = DateTime.Now,
  549. };
  550. new Client<Document>().Save(newdoc, "Converted from Image");
  551. return newdoc.ID;
  552. }
  553. return result;
  554. }
  555. private void CreateJobSubMenu(ITaskControl control, MenuItem job, IEnumerable<TaskModel> tasks)
  556. {
  557. job.Items.Clear();
  558. job.Items.Add(new MenuItem { Header = "Loading...", IsEnabled = false });
  559. using (new WaitCursor())
  560. {
  561. job.Items.Clear();
  562. var jobs = new Client<Job>().Query(
  563. LookupFactory.DefineFilter<Job>(),
  564. LookupFactory.DefineColumns<Job>(),
  565. LookupFactory.DefineSort<Job>()
  566. );
  567. foreach (var row in jobs.Rows)
  568. {
  569. var jobNumber = row.Get<Job, string>(x => x.JobNumber);
  570. var jobName = row.Get<Job, string>(x => x.Name);
  571. job.AddItem($"{jobNumber}: {jobName}", null, tasks, tasks =>
  572. {
  573. using (new WaitCursor())
  574. {
  575. var kanbans = LoadKanbans(tasks, new Columns<Kanban>(x => x.ID, x => x.JobLink.ID));
  576. foreach (var kanban in kanbans)
  577. kanban.JobLink.ID = row.Get<Job, Guid>(x => x.ID);
  578. new Client<Kanban>().Save(kanbans, "Updated Job Number");
  579. control.Refresh(false);
  580. }
  581. });
  582. }
  583. }
  584. }
  585. #endregion
  586. private void TaskPanels_SelectionChanged(object sender, SelectionChangedEventArgs e)
  587. {
  588. if (!IsReady)
  589. return;
  590. if (e.Source is not TabControl)
  591. return;
  592. if (_bTabChanging)
  593. return;
  594. try
  595. {
  596. _bTabChanging = true;
  597. var panel = GetCurrentPanel();
  598. if(panel is not null)
  599. {
  600. KanbanSettings.ViewType = panel.KanbanViewType;
  601. new UserConfiguration<KanbanSettings>().Save(KanbanSettings);
  602. panel.Refresh(false);
  603. }
  604. }
  605. finally
  606. {
  607. _bTabChanging = false;
  608. }
  609. }
  610. #region Get/Save Settings
  611. private KanbanSettings? _settings;
  612. public KanbanSettings KanbanSettings
  613. {
  614. get
  615. {
  616. _settings ??= new UserConfiguration<KanbanSettings>().Load();
  617. return _settings;
  618. }
  619. }
  620. public void SaveSettings()
  621. {
  622. if(_settings != null)
  623. new UserConfiguration<KanbanSettings>().Save(_settings);
  624. }
  625. #endregion
  626. #region IPanel Stuff
  627. public event DataModelUpdateEvent? OnUpdateDataModel;
  628. public bool IsReady { get; set; }
  629. public void CreateToolbarButtons(IPanelHost host)
  630. {
  631. host.CreatePanelAction(
  632. new PanelAction
  633. {
  634. Caption = "New Task",
  635. OnExecute = a => {
  636. if(CreateKanban(k => { }) != null)
  637. {
  638. Refresh();
  639. }
  640. },
  641. Image = PRSDesktop.Resources.add
  642. }
  643. );
  644. if (Security.CanView<KanbanType>())
  645. {
  646. host.CreateSetupAction(
  647. new PanelAction
  648. {
  649. Caption = "Task Types",
  650. Image = PRSDesktop.Resources.kanbantype,
  651. OnExecute = a =>
  652. {
  653. var list = new MasterList(typeof(KanbanType));
  654. list.ShowDialog();
  655. }
  656. });
  657. }
  658. }
  659. public Dictionary<string, object[]> Selected()
  660. {
  661. return new Dictionary<string, object[]>();
  662. }
  663. public void Heartbeat(TimeSpan time)
  664. {
  665. }
  666. private readonly Dictionary<KanbanViewType, TabItem> _viewmap = new();
  667. private readonly List<ITaskControl> _initialized = new();
  668. private ITaskControl GetCurrentPanel()
  669. {
  670. var result = (TaskPanels.SelectedContent as ITaskControl)!;
  671. if (result == null)
  672. result = (TaskPanels.Items[0] as DynamicTabItem)?.Content as ITaskControl;
  673. try
  674. {
  675. //if (result != null)
  676. if (!_initialized.Contains(result))
  677. {
  678. result.Setup();
  679. result.IsReady = true;
  680. _initialized.Add(result);
  681. }
  682. }
  683. catch (Exception e)
  684. {
  685. Logger.Send(LogType.Error, "", $"Error in TaskPanel.GetCurrentPanel: {CoreUtils.FormatException(e)}");
  686. }
  687. return result;
  688. }
  689. public void Setup()
  690. {
  691. _settings = new UserConfiguration<KanbanSettings>().Load();
  692. TaskPanels.SelectedItem = _viewmap[_settings.ViewType];
  693. kanbanTypes = new Client<KanbanType>()
  694. .Query(new Filter<KanbanType>(x => x.Hidden).IsEqualTo(false), new Columns<KanbanType>(x => x.ID, x => x.Code, x => x.Description))
  695. .Rows.Select(x => x.ToObject<KanbanType>()).ToArray();
  696. }
  697. public void Shutdown(CancelEventArgs? cancel)
  698. {
  699. }
  700. public void Refresh()
  701. {
  702. if ((Job == null) || (Job.ID == Guid.Empty))
  703. {
  704. if (TaskPanels.SelectedItem == TasksPlannerTabItem)
  705. TaskPanels.SelectedItem = _viewmap[KanbanViewType.Status];
  706. TasksPlannerTabItem.Visibility = Visibility.Collapsed;
  707. }
  708. else
  709. TasksPlannerTabItem.Visibility = Visibility.Visible;
  710. GetCurrentPanel()?.Refresh(false);
  711. }
  712. public string SectionName => GetCurrentPanel().SectionName;
  713. public TaskPanelProperties Properties { get; set; }
  714. public DataModel DataModel(Selection selection)
  715. {
  716. return GetCurrentPanel().DataModel(selection);
  717. //return new AutoDataModel<Kanban>(new Filter<Kanban>(x => x.ID).IsEqualTo(Guid.Empty));
  718. }
  719. #endregion
  720. #region CRUD Functionality
  721. private TEntity? DoCreate<TEntity>(Action<TEntity> customise)
  722. where TEntity : Entity, IRemotable, IPersistent, new()
  723. {
  724. var result = new TEntity();
  725. customise?.Invoke(result);
  726. if (DoEdit(new[] { result }, null))
  727. return result;
  728. return null;
  729. }
  730. private readonly Dictionary<Type, IDynamicGrid> _grids = new();
  731. private readonly List<Tuple<Guid, Entity>> _entitycache = new();
  732. private DynamicDataGrid<TEntity> GetGrid<TEntity>() where TEntity : Entity, IRemotable, IPersistent, new()
  733. {
  734. if(!_grids.TryGetValue(typeof(TEntity), out var grid))
  735. {
  736. grid = (DynamicGridUtils.CreateDynamicGrid(typeof(DynamicDataGrid<>), typeof(TEntity)) as DynamicDataGrid<TEntity>)!;
  737. _grids[typeof(TEntity)] = grid;
  738. if (typeof(TEntity) == typeof(Kanban))
  739. {
  740. CustomiseKanbanGrid((grid as DynamicDataGrid<Kanban>)!);
  741. }
  742. }
  743. return (grid as DynamicDataGrid<TEntity>)!;
  744. }
  745. private IEnumerable<TEntity> DoLoad<TEntity>(IEnumerable<TaskModel> models, Columns<TEntity> columns)
  746. where TEntity : Entity, IRemotable, IPersistent, new()
  747. {
  748. var result = new List<TEntity>();
  749. var load = new List<Guid>();
  750. foreach (var model in models)
  751. {
  752. var entity = _entitycache.FirstOrDefault(x => Equals(x.Item1, model.ID) && x.Item2 is TEntity) as TEntity;
  753. if (entity is not null)
  754. result.Add(entity);
  755. else
  756. load.Add(model.ID);
  757. }
  758. if (load.Any())
  759. {
  760. var entities = new Client<TEntity>()
  761. .Query(new Filter<TEntity>(x => x.ID).InList(load.ToArray()), columns)
  762. .Rows.Select(x => x.ToObject<TEntity>()).ToList();
  763. foreach (var entity in entities)
  764. _entitycache.Add(new Tuple<Guid, Entity>(entity.ID, entity));
  765. result.AddRange(entities);
  766. }
  767. return result;
  768. }
  769. private IEnumerable<TEntity> DoLoad<TEntityKanban, TEntity, TLink>(IEnumerable<TaskModel> models, Columns<TEntity> columns)
  770. where TEntityKanban : EntityKanban<TEntity, TLink>, new()
  771. where TEntity : Entity, IRemotable, IPersistent, new()
  772. where TLink : IEntityLink<TEntity>, new()
  773. {
  774. var result = DoLoad(models, columns);
  775. if (!result.Any())
  776. foreach (var model in models)
  777. {
  778. result = new Client<TEntity>().Load(
  779. new Filter<TEntity>(x => x.ID).InQuery(new Filter<TEntityKanban>(x => x.Kanban.ID).IsEqualTo(model.ID),
  780. x => x.Entity.ID));
  781. foreach (var r in result)
  782. _entitycache.Add(new Tuple<Guid, Entity>(model.ID, r));
  783. }
  784. return result;
  785. }
  786. private void DoCache<TEntity>(Guid kanbanid, TEntity entity) where TEntity : Entity
  787. {
  788. if (!_entitycache.Any(x => Equals(x.Item1, kanbanid) && x.Item2 is TEntity && Equals(x.Item2.ID, entity.ID)))
  789. _entitycache.Add(new Tuple<Guid, Entity>(kanbanid, entity));
  790. }
  791. private bool DoEdit<TEntity>(IEnumerable<TEntity> entities, Action<TEntity>? action = null)
  792. where TEntity : Entity, IRemotable, IPersistent, new()
  793. {
  794. if (entities == null || !entities.Any())
  795. return false;
  796. foreach (var entity in entities)
  797. action?.Invoke(entity);
  798. return GetGrid<TEntity>().EditItems(entities.ToArray());
  799. }
  800. private void DoLink<TEntityKanban, TEntity, TLink>(TaskModel model, Guid entityid)
  801. where TEntityKanban : EntityKanban<TEntity, TLink>, new()
  802. where TEntity : Entity, IRemotable, IPersistent, new()
  803. where TLink : IEntityLink<TEntity>, new()
  804. {
  805. var linktask = Task.Run(() =>
  806. {
  807. var link = new TEntityKanban();
  808. link.Kanban.ID = model.ID;
  809. link.Entity.ID = entityid;
  810. new Client<TEntityKanban>().Save(link, "");
  811. });
  812. var kanbantask = Task.Run(() =>
  813. {
  814. var kanban = LoadKanbans(new[] { model }, new Columns<Kanban>(x => x.ID, x => x.Locked)).FirstOrDefault();
  815. if (kanban is not null)
  816. {
  817. kanban.Locked = true;
  818. new Client<Kanban>().Save(kanban, "Locked because of linked " + typeof(TEntity).EntityName().Split('.').Last());
  819. }
  820. });
  821. Task.WaitAll(linktask, kanbantask);
  822. }
  823. private static void DoDelete<TEntity>(IList<TEntity> entities, string auditnote)
  824. where TEntity : Entity, IRemotable, IPersistent, new()
  825. {
  826. new Client<TEntity>().Delete(entities, auditnote);
  827. }
  828. public Kanban? CreateKanban(Action<Kanban> customise)
  829. {
  830. var result = DoCreate<Kanban>(
  831. kanban =>
  832. {
  833. kanban.Title = "New Task";
  834. kanban.Description = "";
  835. kanban.Category = "Open";
  836. kanban.DueDate = DateTime.Today;
  837. kanban.Private = false;
  838. kanban.JobLink.ID = Job?.ID ?? Guid.Empty;
  839. kanban.JobLink.Synchronise(Job ?? new Job());
  840. kanban.EmployeeLink.ID = App.EmployeeID;
  841. kanban.ManagerLink.ID = App.EmployeeID;
  842. customise?.Invoke(kanban);
  843. });
  844. if (result != null)
  845. DoCache(result.ID, result);
  846. return result;
  847. }
  848. public IEnumerable<Kanban> LoadKanbans(IEnumerable<TaskModel> models, Columns<Kanban> columns)
  849. {
  850. columns.Add(x => x.ID);
  851. columns.Add(x => x.Number);
  852. columns.Add(x => x.Title);
  853. columns.Add(x => x.Notes);
  854. columns.Add(x => x.Summary);
  855. columns.Add(x => x.Completed);
  856. columns.Add(x => x.DueDate);
  857. columns.Add(x => x.ManagerLink.ID);
  858. columns.Add(x => x.EmployeeLink.ID);
  859. return DoLoad(models, columns);
  860. }
  861. public void OnValidateKanban(object sender, Kanban[] items, List<string> errors)
  862. {
  863. if (Properties.RequireTaskTypes && items.Any(x => x.Type.ID == Guid.Empty))
  864. {
  865. errors.Add("[Task Type] may not be blank!");
  866. }
  867. }
  868. public void CustomiseKanbanGrid(DynamicDataGrid<Kanban> grid)
  869. {
  870. grid.OnValidate += OnValidateKanban;
  871. }
  872. public bool EditKanbans(IEnumerable<TaskModel> models, Action<Kanban>? customise = null)
  873. {
  874. var entities = LoadKanbans(models, GetGrid<Kanban>().LoadEditorColumns());
  875. return DoEdit(entities, customise);
  876. }
  877. public void DeleteKanbans(IEnumerable<TaskModel> models, string auditnote)
  878. {
  879. var kanbans = models.Select(x => new Kanban { ID = x.ID }).ToList();
  880. DoDelete(kanbans, auditnote);
  881. }
  882. public Requisition? CreateRequisition(TaskModel model, Action<Requisition>? customise)
  883. {
  884. var result = DoCreate<Requisition>(
  885. requi =>
  886. {
  887. requi.JobLink.ID = Job.ID;
  888. requi.JobLink.Synchronise(Job);
  889. customise?.Invoke(requi);
  890. });
  891. if (result != null)
  892. {
  893. DoCache(model.ID, result);
  894. DoLink<RequisitionKanban, Requisition, RequisitionLink>(model, result.ID);
  895. }
  896. return result;
  897. }
  898. public bool EditRequisitions(IEnumerable<TaskModel> models, Action<Requisition>? customise = null)
  899. {
  900. var requis = DoLoad<RequisitionKanban, Requisition, RequisitionLink>(models, GetGrid<Requisition>().LoadEditorColumns());
  901. if (requis.Any())
  902. return DoEdit(requis, customise);
  903. return false;
  904. }
  905. public Setout? CreateSetout(TaskModel model, Action<Setout> customise)
  906. {
  907. var result = DoCreate<Setout>(
  908. setout =>
  909. {
  910. setout.JobLink.ID = Job.ID;
  911. setout.JobLink.Synchronise(Job);
  912. customise?.Invoke(setout);
  913. });
  914. if (result != null)
  915. {
  916. DoCache(model.ID, result);
  917. //DoLink<SetoutKanban, Setout, SetoutLink>(model, result.ID);
  918. }
  919. return result;
  920. }
  921. public bool EditSetouts(IEnumerable<TaskModel> models, Action<Setout>? customise = null)
  922. {
  923. var setouts = DoLoad<SetoutKanban, Setout, SetoutLink>(models, GetGrid<Setout>().LoadEditorColumns());
  924. if (setouts.Any())
  925. return DoEdit(setouts, customise);
  926. return false;
  927. }
  928. public Delivery? CreateDelivery(TaskModel model, Action<Delivery> customise)
  929. {
  930. var result = DoCreate<Delivery>(
  931. delivery =>
  932. {
  933. delivery.Job.ID = Job.ID;
  934. delivery.Job.Synchronise(Job);
  935. customise?.Invoke(delivery);
  936. });
  937. if (result != null)
  938. {
  939. DoCache(model.ID, result);
  940. DoLink<DeliveryKanban, Delivery, DeliveryLink>(model, result.ID);
  941. }
  942. return result;
  943. }
  944. public bool EditDeliveries(IEnumerable<TaskModel> models, Action<Delivery>? customise = null)
  945. {
  946. var deliveries = DoLoad<DeliveryKanban, Delivery, DeliveryLink>(models, GetGrid<Delivery>().LoadEditorColumns());
  947. if (deliveries.Any())
  948. return DoEdit(deliveries, customise);
  949. return false;
  950. }
  951. public PurchaseOrder? CreateOrder(TaskModel model, Action<PurchaseOrder> customise)
  952. {
  953. var result = DoCreate<PurchaseOrder>(
  954. order => { customise?.Invoke(order); });
  955. if (result != null)
  956. {
  957. DoCache(model.ID, result);
  958. DoLink<PurchaseOrderKanban, PurchaseOrder, PurchaseOrderLink>(model, result.ID);
  959. }
  960. return result;
  961. }
  962. public bool EditPurchaseOrders(IEnumerable<TaskModel> models, Action<PurchaseOrder>? customise = null)
  963. {
  964. var orders = DoLoad<PurchaseOrderKanban, PurchaseOrder, PurchaseOrderLink>(models, GetGrid<PurchaseOrder>().LoadEditorColumns());
  965. if (orders.Any())
  966. return DoEdit(orders, customise);
  967. return false;
  968. }
  969. #endregion
  970. #region EntityReferences
  971. private static void AddQuery<TEntityKanban, TEntity, TLink>(MultiQuery query, Guid[] taskids)
  972. where TEntityKanban : EntityKanban<TEntity, TLink>, new()
  973. where TEntity : Entity
  974. where TLink : IEntityLink<TEntity>, new()
  975. {
  976. query.Add(
  977. new Filter<TEntityKanban>(x => x.Kanban.ID).InList(taskids),
  978. new Columns<TEntityKanban>(x => x.Entity.ID).Add(x => x.Kanban.ID)
  979. );
  980. }
  981. private static Guid[] ExtractIDs<TEntityKanban, TEntity, TLink>(MultiQuery query)
  982. where TEntityKanban : EntityKanban<TEntity, TLink>, new()
  983. where TEntity : Entity
  984. where TLink : IEntityLink<TEntity>, new()
  985. {
  986. var lookup = query.Get<TEntityKanban>().ToLookup<TEntityKanban, Guid, Guid>(x => x.Kanban.ID, x => x.Entity.ID);
  987. return query.Get<TEntityKanban>().ExtractValues<TEntityKanban, Guid>(x => x.Entity.ID).ToArray();
  988. }
  989. public KanbanReferences[] GetReferences(IEnumerable<TaskModel> models)
  990. {
  991. var result = new List<KanbanReferences>();
  992. var ids = models.Select(x => x.ID).ToArray();
  993. var query = new MultiQuery();
  994. AddQuery<RequisitionKanban, Requisition, RequisitionLink>(query, ids);
  995. AddQuery<SetoutKanban, Setout, SetoutLink>(query, ids);
  996. AddQuery<DeliveryKanban, Delivery, DeliveryLink>(query, ids);
  997. AddQuery<PurchaseOrderKanban, PurchaseOrder, PurchaseOrderLink>(query, ids);
  998. query.Query();
  999. var requis = query.Get<RequisitionKanban>().ToLookup<RequisitionKanban, Guid, Guid>(x => x.Kanban.ID, x => x.Entity.ID);
  1000. var setouts = query.Get<SetoutKanban>().ToLookup<SetoutKanban, Guid, Guid>(x => x.Kanban.ID, x => x.Entity.ID);
  1001. var deliveries = query.Get<DeliveryKanban>().ToLookup<DeliveryKanban, Guid, Guid>(x => x.Kanban.ID, x => x.Entity.ID);
  1002. var orders = query.Get<PurchaseOrderKanban>().ToLookup<PurchaseOrderKanban, Guid, Guid>(x => x.Kanban.ID, x => x.Entity.ID);
  1003. foreach (var id in ids)
  1004. {
  1005. var references = new KanbanReferences
  1006. {
  1007. Kanban = id,
  1008. Requisitions = requis.Contains(id) ? requis[id].ToArray() : Array.Empty<Guid>(),
  1009. Setouts = setouts.Contains(id) ? setouts[id].ToArray() : Array.Empty<Guid>(),
  1010. Deliveries = deliveries.Contains(id) ? deliveries[id].ToArray() : Array.Empty<Guid>(),
  1011. Orders = orders.Contains(id) ? orders[id].ToArray() : Array.Empty<Guid>()
  1012. };
  1013. result.Add(references);
  1014. }
  1015. return result.ToArray();
  1016. }
  1017. public bool EditReferences(IEnumerable<TaskModel> models)
  1018. {
  1019. var result = false;
  1020. var refs = GetReferences(models).First();
  1021. if (refs.ReferenceType() == typeof(Requisition))
  1022. result = EditRequisitions(
  1023. models,
  1024. requi =>
  1025. {
  1026. requi.Notes = Utility.ProcessNotes(requi.Notes, requi.Request);
  1027. requi.Request = "";
  1028. }
  1029. );
  1030. else if (refs.ReferenceType() == typeof(Setout))
  1031. result = EditSetouts(models);
  1032. else if (refs.ReferenceType() == typeof(Delivery))
  1033. result = EditDeliveries(models);
  1034. else if (refs.ReferenceType() == typeof(PurchaseOrder))
  1035. result = EditPurchaseOrders(models);
  1036. else
  1037. result = EditKanbans(models);
  1038. return result;
  1039. }
  1040. #endregion
  1041. }
  1042. }