DataEntryGrid.cs 15 KB

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