DataEntryGrid.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Configuration;
  4. using InABox.Core;
  5. using InABox.DynamicGrid;
  6. using InABox.WPF;
  7. using PRSDesktop.Panels.DataEntry;
  8. using Syncfusion.Pdf;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Threading;
  14. using System.Windows;
  15. using System.Windows.Media.Imaging;
  16. namespace PRSDesktop;
  17. public class DataEntryCachedDocument : ICachedDocument
  18. {
  19. public DateTime TimeStamp { get; set; }
  20. public Document? Document { get; set; }
  21. public Guid ID => Document?.ID ?? Guid.Empty;
  22. public DataEntryCachedDocument() { }
  23. public DataEntryCachedDocument(Document document)
  24. {
  25. Document = document;
  26. TimeStamp = document.TimeStamp;
  27. }
  28. public void DeserializeBinary(CoreBinaryReader reader, bool full)
  29. {
  30. TimeStamp = reader.ReadDateTime();
  31. if (full)
  32. {
  33. Document = reader.ReadObject<Document>();
  34. }
  35. }
  36. public void SerializeBinary(CoreBinaryWriter writer)
  37. {
  38. writer.Write(TimeStamp);
  39. if (Document is null)
  40. {
  41. throw new Exception("Cannot serialize incomplete CachedDocument");
  42. }
  43. writer.WriteObject(Document);
  44. }
  45. }
  46. public class DataEntryCache : DocumentCache<DataEntryCachedDocument>
  47. {
  48. public override TimeSpan MaxAge => TimeSpan.FromDays(new UserConfiguration<DataEntryPanelSettings>().Load().CacheAge);
  49. private static DataEntryCache? _cache;
  50. public static DataEntryCache Cache
  51. {
  52. get
  53. {
  54. _cache ??= DocumentCaches.GetOrRegister<DataEntryCache>();
  55. return _cache;
  56. }
  57. }
  58. public DataEntryCache(): base(nameof(DataEntryCache)) { }
  59. protected override DataEntryCachedDocument? LoadDocument(Guid id)
  60. {
  61. var document = Client.Query(new Filter<Document>(x => x.ID).IsEqualTo(id))
  62. .ToObjects<Document>().FirstOrDefault();
  63. if(document is not null)
  64. {
  65. return new DataEntryCachedDocument(document);
  66. }
  67. else
  68. {
  69. return null;
  70. }
  71. }
  72. /// <summary>
  73. /// Fetch a bunch of documents from the cache or the database, optionally checking against the timestamp listed in the database.
  74. /// </summary>
  75. /// <param name="ids"></param>
  76. /// <param name="checkTimestamp">
  77. /// If <see langword="true"/>, then loads <see cref="Document.TimeStamp"/> from the database for all cached documents,
  78. /// and if they are older, updates the cache.
  79. /// </param>
  80. public IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids, bool checkTimestamp = false)
  81. {
  82. var cached = new List<Guid>();
  83. var toLoad = new List<Guid>();
  84. foreach (var docID in ids)
  85. {
  86. if (Has(docID))
  87. {
  88. cached.Add(docID);
  89. }
  90. else
  91. {
  92. toLoad.Add(docID);
  93. }
  94. }
  95. var loadedCached = new List<Document>();
  96. if (cached.Count > 0)
  97. {
  98. var docs = Client.Query(
  99. new Filter<Document>(x => x.ID).InList(cached.ToArray()),
  100. Columns.None<Document>().Add(x => x.TimeStamp, x => x.ID));
  101. foreach (var doc in docs.ToObjects<Document>())
  102. {
  103. try
  104. {
  105. var timestamp = GetHeader(doc.ID).Document.TimeStamp;
  106. if (doc.TimeStamp > timestamp)
  107. {
  108. toLoad.Add(doc.ID);
  109. }
  110. else
  111. {
  112. loadedCached.Add(GetFull(doc.ID).Document.Document!);
  113. }
  114. }
  115. catch (Exception e)
  116. {
  117. CoreUtils.LogException("", e, "Error loading cached file");
  118. toLoad.Add(doc.ID);
  119. }
  120. }
  121. }
  122. if (toLoad.Count > 0)
  123. {
  124. var loaded = Client.Query(new Filter<Document>(x => x.ID).InList(toLoad.ToArray()))
  125. .ToObjects<Document>().ToList();
  126. foreach (var loadedDoc in loaded)
  127. {
  128. Add(new DataEntryCachedDocument(loadedDoc));
  129. }
  130. return loaded.Concat(loadedCached);
  131. }
  132. else
  133. {
  134. return loadedCached;
  135. }
  136. }
  137. }
  138. public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
  139. {
  140. private List<DataEntryTag>? _tags;
  141. public DataEntryGrid()
  142. {
  143. HiddenColumns.Add(x => x.Tag.ID);
  144. HiddenColumns.Add(x => x.Tag.AppliesTo);
  145. HiddenColumns.Add(x => x.Document.ID);
  146. HiddenColumns.Add(x => x.Document.FileName);
  147. HiddenColumns.Add(x => x.EntityID);
  148. HiddenColumns.Add(x => x.Archived);
  149. HiddenColumns.Add(x => x.Note);
  150. ActionColumns.Add(new DynamicImageColumn(LinkedImage) { Position = DynamicActionColumnPosition.Start });
  151. var tagFilter = new Filter<DataEntryDocument>(x => x.Tag.ID).InList(GetVisibleTags().Select(x => x.ID).ToArray());
  152. if (Security.IsAllowed<CanSetupDataEntryTags>())
  153. {
  154. tagFilter.Or(x => x.Tag.ID).IsEqualTo(Guid.Empty);
  155. }
  156. var docs = Client.Query(
  157. new Filter<DataEntryDocument>(x => x.Archived).IsEqualTo(DateTime.MinValue)
  158. .And(tagFilter),
  159. Columns.None<DataEntryDocument>().Add(x => x.Document.ID));
  160. DataEntryCache.Cache.ClearOld();
  161. DataEntryCache.Cache.EnsureStrict(docs.Rows.Select(x => x.Get<DataEntryDocument, Guid>(x => x.Document.ID)).ToArray());
  162. }
  163. private static readonly BitmapImage link = PRSDesktop.Resources.link.AsBitmapImage();
  164. private BitmapImage? LinkedImage(CoreRow? arg)
  165. {
  166. return arg == null
  167. ? link
  168. : arg.Get<DataEntryDocument, Guid>(x => x.EntityID) != Guid.Empty
  169. ? link
  170. : null;
  171. }
  172. protected override void DoReconfigure(DynamicGridOptions options)
  173. {
  174. base.DoReconfigure(options);
  175. options.Clear();
  176. options.FilterRows = true;
  177. options.MultiSelect = true;
  178. options.DragSource = true;
  179. options.DragTarget = true;
  180. options.SelectColumns = true;
  181. }
  182. public static List<DataEntryTag> GetVisibleTagList()
  183. {
  184. var tags = new Client<DataEntryTag>().Query().ToObjects<DataEntryTag>().ToList();
  185. var tagsList = new List<DataEntryTag>();
  186. foreach (var tag in tags)
  187. {
  188. var entity = CoreUtils.GetEntityOrNull(tag.AppliesTo);
  189. if (entity is null || Security.CanView(entity))
  190. {
  191. var tagHasEmployee = new Client<DataEntryTagDistributionEmployee>()
  192. .Query(
  193. new Filter<DataEntryTagDistributionEmployee>(x => x.Tag.ID).IsEqualTo(tag.ID)
  194. .And(x => x.Employee.ID).IsEqualTo(App.EmployeeID),
  195. Columns.None<DataEntryTagDistributionEmployee>().Add(x => x.ID))
  196. .Rows.Any();
  197. if (tagHasEmployee)
  198. {
  199. tagsList.Add(tag);
  200. }
  201. }
  202. }
  203. return tagsList;
  204. }
  205. private List<DataEntryTag> GetVisibleTags()
  206. {
  207. _tags ??= GetVisibleTagList();
  208. return _tags;
  209. }
  210. /// <summary>
  211. /// Gets the currently selected tag ID, if all selected rows belong to the same tag.
  212. /// </summary>
  213. /// <returns></returns>
  214. private Guid GetSelectedTagID()
  215. {
  216. var tagID = Guid.Empty;
  217. foreach (var row in SelectedRows)
  218. {
  219. var rowTag = row.Get<DataEntryDocument, Guid>(x => x.Tag.ID);
  220. if (tagID == Guid.Empty)
  221. {
  222. tagID = rowTag;
  223. }
  224. else if (rowTag != tagID)
  225. {
  226. return Guid.Empty;
  227. }
  228. }
  229. return tagID;
  230. }
  231. private IEnumerable<Tuple<DataEntryDocument, List<DataEntryReGroupWindow.Page>>> ExplodeDocuments()
  232. {
  233. var dataEntryDocs = SelectedRows.ToArray<DataEntryDocument>();
  234. var docIDs = dataEntryDocs.Select(r => r.Document.ID).ToArray();
  235. var docs = new Client<Document>()
  236. .Query(
  237. new Filter<Document>(x => x.ID).InList(docIDs),
  238. Columns.None<Document>().Add(x => x.ID).Add(x => x.Data).Add(x => x.FileName))
  239. .ToObjects<Document>().ToDictionary(x => x.ID, x => x);
  240. foreach (var dataEntryDoc in dataEntryDocs)
  241. {
  242. if (docs.TryGetValue(dataEntryDoc.Document.ID, out var doc))
  243. {
  244. var ms = new MemoryStream(doc.Data);
  245. var pdfDoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, ms);
  246. yield return new(dataEntryDoc, DataEntryReGroupWindow.SplitIntoPages(doc.FileName, pdfDoc).ToList());
  247. }
  248. }
  249. }
  250. public void DoExplodeAll()
  251. {
  252. var pages = ExplodeDocuments();
  253. var groups = new List<DocumentGroup>();
  254. foreach(var (doc, docPages) in pages)
  255. {
  256. if(docPages.Count == 1)
  257. {
  258. groups.Add(new DocumentGroup(doc.Document.FileName, docPages, doc.Tag.ID));
  259. }
  260. else
  261. {
  262. var extension = Path.GetExtension(doc.Document.FileName) ?? "";
  263. var stem = Path.GetFileNameWithoutExtension(doc.Document.FileName);
  264. foreach(var (i, page) in docPages.WithIndex())
  265. {
  266. var group = new DocumentGroup($"{stem} - {i + 1}{extension}", [page], doc.Tag.ID);
  267. groups.Add(group);
  268. }
  269. }
  270. }
  271. SavePageGroups(groups);
  272. DeleteItems(SelectedRows);
  273. Refresh(false,true);
  274. }
  275. public void DoExplode()
  276. {
  277. var tagID = GetSelectedTagID();
  278. var pages = ExplodeDocuments();
  279. var filename = "";
  280. var allPages = new List<DataEntryReGroupWindow.Page>();
  281. foreach(var (doc, docPages) in pages)
  282. {
  283. filename = doc.Document.FileName;
  284. allPages.AddRange(docPages);
  285. }
  286. if (ShowDocumentWindow(allPages, filename, tagID))
  287. {
  288. // ShowDocumentWindow already saves new scans, so we just need to get rid of the old ones.
  289. DeleteItems(SelectedRows);
  290. Refresh(false,true);
  291. }
  292. }
  293. public void DoRemove()
  294. {
  295. var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
  296. foreach (var update in updates)
  297. {
  298. update.Archived = DateTime.Now;
  299. DataEntryCache.Cache.Remove(update.Document.ID);
  300. }
  301. new Client<DataEntryDocument>().Save(updates,"Removed from Data Entry Panel");
  302. Refresh(false,true);
  303. }
  304. public void DoChangeTags(Guid tagid)
  305. {
  306. var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
  307. foreach (var update in updates)
  308. {
  309. if (update.Tag.ID != tagid)
  310. {
  311. update.Tag.ID = tagid;
  312. update.EntityID = Guid.Empty;
  313. }
  314. }
  315. new Client<DataEntryDocument>().Save(updates.Where(x=>x.IsChanged()),"Updated Tags on Data Entry Panel");
  316. Refresh(false,true);
  317. }
  318. public void DoChangeNote(string note)
  319. {
  320. var updates = SelectedRows.ToObjects<DataEntryDocument>().ToArray();
  321. foreach (var update in updates)
  322. {
  323. if (!string.Equals(update.Note, note))
  324. {
  325. update.Note = note;
  326. }
  327. }
  328. Client.Save(updates.Where(x => x.IsChanged()), "Updated Note on Data Entry Panel");
  329. Refresh(false, true);
  330. }
  331. protected override DragDropEffects OnRowsDragStart(CoreRow[] rows)
  332. {
  333. var table = new CoreTable();
  334. table.Columns.Add(new CoreColumn { ColumnName = "ID", DataType = typeof(Guid) });
  335. foreach(var row in rows)
  336. {
  337. var newRow = table.NewRow();
  338. newRow.Set<Document, Guid>(x => x.ID, row.Get<DataEntryDocument, Guid>(x => x.Document.ID));
  339. table.Rows.Add(newRow);
  340. }
  341. return DragTable(typeof(Document), table);
  342. }
  343. public void UploadDocument(string filename, byte[] data, Guid tagID)
  344. {
  345. var document = new Document
  346. {
  347. FileName = filename,
  348. CRC = CoreUtils.CalculateCRC(data),
  349. TimeStamp = DateTime.Now,
  350. Data = data
  351. };
  352. new Client<Document>().Save(document, "");
  353. var dataentry = new DataEntryDocument
  354. {
  355. Document =
  356. {
  357. ID = document.ID
  358. },
  359. Tag =
  360. {
  361. ID = tagID
  362. },
  363. Employee =
  364. {
  365. ID = App.EmployeeID
  366. }
  367. };
  368. if(Path.GetExtension(filename) == ".pdf")
  369. {
  370. dataentry.Thumbnail = ImageUtils.GetPDFThumbnail(data, 256, 256);
  371. }
  372. new Client<DataEntryDocument>().Save(dataentry, "");
  373. DataEntryCache.Cache.Add(new DataEntryCachedDocument(document));
  374. Dispatcher.BeginInvoke(() =>
  375. {
  376. Refresh(false, true);
  377. });
  378. }
  379. private static PdfDocumentBase CombinePages(IEnumerable<DataEntryReGroupWindow.Page> pages)
  380. {
  381. var document = new PdfDocument();
  382. foreach (var page in pages)
  383. {
  384. document.ImportPage(page.Pdf, page.PageIndex);
  385. }
  386. return document;
  387. }
  388. private void SavePageGroups(IEnumerable<DocumentGroup> groups)
  389. {
  390. Progress.ShowModal("Uploading Files", (progress) =>
  391. {
  392. foreach (var group in groups)
  393. {
  394. progress.Report($"Uploading '{group.FileName}'");
  395. var doc = CombinePages(group.Pages);
  396. byte[] data;
  397. using (var ms = new MemoryStream())
  398. {
  399. doc.Save(ms);
  400. data = ms.ToArray();
  401. }
  402. UploadDocument(group.FileName, data, group.TagID);
  403. }
  404. });
  405. }
  406. public bool ShowDocumentWindow(List<DataEntryReGroupWindow.Page> pages, string filename, Guid tagID)
  407. {
  408. var window = new DataEntryReGroupWindow(pages, filename, tagID);
  409. if (window.ShowDialog() == true)
  410. {
  411. SavePageGroups(window.Groups);
  412. return true;
  413. }
  414. return false;
  415. }
  416. public override DynamicGridColumns GenerateColumns()
  417. {
  418. var columns = new DynamicGridColumns();
  419. columns.Add<DataEntryDocument, string>(x => x.Document.FileName, 0, "Filename", "", Alignment.MiddleLeft);
  420. columns.Add<DataEntryDocument, string>(x => x.Tag.Name, 100, "Tag", "", Alignment.MiddleLeft);
  421. return columns;
  422. }
  423. protected override void Reload(
  424. Filters<DataEntryDocument> criteria, Columns<DataEntryDocument> columns, ref SortOrder<DataEntryDocument>? sort,
  425. CancellationToken token, Action<CoreTable?, Exception?> action)
  426. {
  427. criteria.Add(new Filter<DataEntryDocument>(x => x.Archived).IsEqualTo(DateTime.MinValue));
  428. var tagFilter = new Filter<DataEntryDocument>(x => x.Tag.ID).InList(GetVisibleTags().Select(x => x.ID).ToArray());
  429. if (Security.IsAllowed<CanSetupDataEntryTags>())
  430. {
  431. tagFilter.Or(x => x.Tag.ID).IsEqualTo(Guid.Empty);
  432. }
  433. criteria.Add(tagFilter);
  434. base.Reload(criteria, columns, ref sort, token, action);
  435. }
  436. }