DataEntryPanel.xaml.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.DynamicGrid;
  5. using InABox.Wpf;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.ComponentModel;
  9. using System.Linq;
  10. using System.Linq.Expressions;
  11. using System.Windows;
  12. using System.Windows.Controls;
  13. using System.Windows.Media;
  14. using InABox.Configuration;
  15. using Microsoft.Xaml.Behaviors.Core;
  16. using PRSDesktop.Panels.DataEntry.Grids;
  17. namespace PRSDesktop;
  18. public class DataEntryPanelSettings : BaseObject, IUserConfigurationSettings
  19. {
  20. private static readonly double DefaultCacheAge = 7;
  21. [NullEditor]
  22. public double PreviewWidth { get; set; }
  23. [Comment("Age of cached documents before they are deleted (days)")]
  24. [DoubleEditor]
  25. public double CacheAge { get; set; } = DefaultCacheAge;
  26. }
  27. /// <summary>
  28. /// Interaction logic for DataEntryPanel.xaml
  29. /// </summary>
  30. public partial class DataEntryPanel : UserControl, IBasePanel, IDynamicEditorHost
  31. {
  32. private DataEntryPanelSettings _settings;
  33. //IPopupEditorControl? _popup;
  34. private IDynamicDataGrid? _grid;
  35. private Type? _selectedType;
  36. // The id of the entity currently being edited.
  37. private Guid _entityID;
  38. // The id of the entity we selected from the grid, as it was when we selected it; this is to cancel, because the lookup for the item
  39. // updates _entityID, and we need to set it back when cancelling.
  40. private Guid _originalID;
  41. private bool _processenabled;
  42. private Entity? _entity;
  43. private Button? _process;
  44. private bool _isChanged;
  45. private bool IsChanged
  46. {
  47. get => _isChanged;
  48. set
  49. {
  50. if(_isChanged != value)
  51. {
  52. _isChanged = value;
  53. Editor.HideButtons = !value;
  54. if (_process != null)
  55. _process.IsEnabled = value;
  56. _documents._dataEntryGrid.IsEnabled = !value;
  57. }
  58. }
  59. }
  60. public void Select(Type? type, Guid id)
  61. {
  62. ClearEditor();
  63. _selectedType = type;
  64. _entityID = id;
  65. if (_selectedType != null)
  66. {
  67. CreateEditor();
  68. PopulateEditor();
  69. //LoadPopup();
  70. }
  71. }
  72. private void ScanPanel_OnSelectScan(string appliesto, Guid entityid, bool processenabled)
  73. {
  74. _processenabled = processenabled;
  75. _originalID = entityid;
  76. Select(
  77. CoreUtils.GetEntityOrNull(appliesto),
  78. entityid
  79. );
  80. }
  81. public DataEntryPanel()
  82. {
  83. InitializeComponent();
  84. LoadSettings();
  85. }
  86. private void LoadSettings()
  87. {
  88. _settings = new UserConfiguration<DataEntryPanelSettings>().Load();
  89. _panel.AnchorWidth = _settings.PreviewWidth > 0.0F
  90. ? _settings.PreviewWidth
  91. : 500F;
  92. }
  93. public void Setup()
  94. {
  95. _documents.Setup();
  96. IsChanged = false;
  97. }
  98. public void Refresh()
  99. {
  100. if (CheckSaved())
  101. {
  102. _documents.Refresh();
  103. IsChanged = false;
  104. }
  105. }
  106. public bool IsReady { get; set; }
  107. public string SectionName => "Data Entry";
  108. public DataModel DataModel(Selection selection)
  109. {
  110. return new EmptyDataModel();
  111. }
  112. public event DataModelUpdateEvent? OnUpdateDataModel;
  113. public void CreateToolbarButtons(IPanelHost host)
  114. {
  115. if (Security.IsAllowed<CanSetupDataEntryTags>())
  116. {
  117. host.CreateSetupAction(new PanelAction("Data Entry Tags", null, (action) =>
  118. {
  119. var list = new MasterList(typeof(DataEntryTag));
  120. list.ShowDialog();
  121. }));
  122. host.CreateSetupAction(new PanelAction("Settings", null, (action) =>
  123. {
  124. var settings = new UserConfiguration<DataEntryPanelSettings>().Load();
  125. var grid = new DynamicItemsListGrid<DataEntryPanelSettings>();
  126. if (grid.EditItems(new DataEntryPanelSettings[] { settings }))
  127. {
  128. new UserConfiguration<DataEntryPanelSettings>().Save(settings);
  129. LoadSettings();
  130. }
  131. }));
  132. }
  133. }
  134. public void Heartbeat(TimeSpan time)
  135. {
  136. }
  137. public Dictionary<string, object[]> Selected()
  138. {
  139. return new Dictionary<string, object[]>();
  140. }
  141. private void CheckSaved(CancelEventArgs cancel)
  142. {
  143. if (!_isChanged)
  144. {
  145. return;
  146. }
  147. var result = MessageBox.Show("You have changes that have not been saved; do you wish to save these changes?", "Save Changes?", MessageBoxButton.YesNoCancel);
  148. if (result == MessageBoxResult.Yes)
  149. {
  150. if (!Editor.Validate())
  151. {
  152. cancel.Cancel = true;
  153. return;
  154. }
  155. DoSave(false);
  156. if (!cancel.Cancel)
  157. MessageBox.Show("Item saved.");
  158. }
  159. else if (result == MessageBoxResult.Cancel)
  160. {
  161. cancel.Cancel = true;
  162. }
  163. }
  164. private bool CheckSaved()
  165. {
  166. var cancel = new CancelEventArgs();
  167. CheckSaved(cancel);
  168. return !cancel.Cancel;
  169. }
  170. public void Shutdown(CancelEventArgs? cancel)
  171. {
  172. if (cancel != null)
  173. CheckSaved(cancel);
  174. if (cancel?.Cancel != true)
  175. _documents.Shutdown(cancel);
  176. }
  177. #region Host
  178. public Type GetEditorType() => _selectedType ?? typeof(BaseObject);
  179. public void LoadLookups(ILookupEditorControl sender)
  180. {
  181. var colname = sender.ColumnName;
  182. var values = sender.LookupEditorDefinition.Values(colname, Editor.Items);
  183. sender.LoadLookups(values);
  184. }
  185. BaseObject[] IDynamicEditorHost.GetItems() => Editor.Items;
  186. public BaseEditor? GetEditor(DynamicGridColumn column) => column.Editor.CloneEditor();
  187. #endregion
  188. private void ClearEditor()
  189. {
  190. DetailBorder.Child = null;
  191. IsChanged = false;
  192. //if (_popup is UIElement element)
  193. // DetailHeader.Children.Remove(element);
  194. }
  195. private void CreateEditor()
  196. {
  197. if (_selectedType == null)
  198. return;
  199. Editor = new EmbeddedDynamicEditorForm();
  200. if (_selectedType == typeof(Bill))
  201. Editor.SetLayoutType<SupplierBillEditDocumentLayout>();
  202. else
  203. Editor.SetLayoutType<VerticalDynamicEditorGridLayout>();
  204. Editor.HighlightButtons = true;
  205. Editor.HideButtons = true;
  206. Editor.SetValue(Grid.RowProperty, 1);
  207. Editor.SetValue(Grid.ColumnProperty, 0);
  208. Editor.SetValue(Grid.ColumnSpanProperty, 4);
  209. Editor.OnOK += () => { DoSave(false); };
  210. Editor.OnCancel += () =>
  211. {
  212. _entityID = _originalID;
  213. Select(_selectedType, _entityID);
  214. };
  215. Editor.OnChanged += (sender, args) => IsChanged = true;
  216. DetailBorder.Child = Editor;
  217. _grid = (DynamicGridUtils.CreateDynamicGrid(typeof(DynamicDataGrid<>), _selectedType) as IDynamicDataGrid)!;
  218. _grid.OnCustomiseEditor += (sender, items, column, editor) =>
  219. {
  220. if ((editor is BaseCodeEditor be) && editor.Editable.EditorVisible())
  221. {
  222. be.Buttons = new[]
  223. {
  224. new EditorButton(null, "..", 30, DoLookup, false)
  225. };
  226. }
  227. };
  228. _grid.OnAfterEditorValueChangedEvent += (sender, args) =>
  229. {
  230. IsChanged = IsChanged || (_entity?.IsChanged() == true || _originalID != _entityID);
  231. return null;
  232. };
  233. if (_grid is DynamicDataGrid<JobDocumentSetMileStone> milestoneGrid)
  234. {
  235. milestoneGrid.OnValidate += (o, items, err) =>
  236. {
  237. if (items.Any(x => x.ID == Guid.Empty))
  238. {
  239. err.Add("Cannot create a new milestone from Data Entry screen. Please select a milestone to edit.");
  240. }
  241. };
  242. }
  243. }
  244. private void DoLookup(object editor, object? item)
  245. {
  246. if (_selectedType == null)
  247. return;
  248. if (editor is CodeEditorControl ce)
  249. {
  250. Dictionary<string, string>? filter = null;
  251. if (ce.Value != null)
  252. {
  253. filter = new Dictionary<string, string>
  254. {
  255. [ce.ColumnName] = ce.Value
  256. };
  257. }
  258. Type? gridType = null;
  259. if (_selectedType == typeof(JobDocumentSetMileStone))
  260. {
  261. gridType = typeof(JobDocumentSetMileStoneDataEntryPopupGrid);
  262. }
  263. var popup = new PopupList(_selectedType, _entityID, Array.Empty<string>(), filter, gridType: gridType);
  264. popup.OnDefineFilter += LookupFactory.DefineFilter;
  265. if (popup.ShowDialog() == true)
  266. {
  267. _entityID = popup.ID;
  268. Select(_selectedType, _entityID);
  269. }
  270. }
  271. }
  272. private void SaveDocument(DataEntryDocument dataEntryDocument)
  273. {
  274. var doctype = CoreUtils.TypeList(x =>
  275. {
  276. var entityDoc = x.GetInterfaceDefinition(typeof(IEntityDocument<>));
  277. if (entityDoc is null)
  278. {
  279. return false;
  280. }
  281. var linkType = entityDoc.GenericTypeArguments[0].GetInterfaceDefinition(typeof(IEntityLink<>));
  282. if (linkType is null)
  283. {
  284. return false;
  285. }
  286. return linkType.GenericTypeArguments[0] == _selectedType;
  287. }).FirstOrDefault();
  288. if(doctype is null)
  289. {
  290. return;
  291. }
  292. var doc = (IEntityDocument)Activator.CreateInstance(doctype)!;
  293. CoreUtils.SetPropertyValue(doc, "EntityLink.ID", _entityID);
  294. doc.DocumentLink.ID = dataEntryDocument.Document.ID;
  295. doc.Thumbnail = dataEntryDocument.Thumbnail;
  296. ClientFactory.CreateClient(doctype).Save(doc,"Added from Data Entry Screen");
  297. }
  298. private void DoSave(bool markasprocessed)
  299. {
  300. var cancel = new System.ComponentModel.CancelEventArgs();
  301. if (markasprocessed && (_entity is IDataEntryInstance scannable))
  302. scannable.DataEntered = DateTime.Now;
  303. Editor.SaveItem(cancel);
  304. if (!cancel.Cancel)
  305. {
  306. _originalID = _entityID;
  307. _entityID = _entity.ID;
  308. IsChanged = false;
  309. var row = _documents._dataEntryGrid.SelectedRows.FirstOrDefault();
  310. if (row != null)
  311. {
  312. var scan = row.ToObject<DataEntryDocument>();
  313. scan.EntityID = _entity.ID;
  314. if (markasprocessed)
  315. {
  316. SaveDocument(scan);
  317. scan.Archived = DateTime.Now;
  318. }
  319. if (scan.IsChanged())
  320. {
  321. new Client<DataEntryDocument>().Save(scan, "Updated from Data Entry Screen");
  322. _documents.Refresh();
  323. }
  324. }
  325. }
  326. }
  327. private void PopulateEditor()
  328. {
  329. if (_selectedType == null)
  330. return;
  331. _entity = null;
  332. if (_entityID != Guid.Empty)
  333. {
  334. _entity = Client.Create(_selectedType)
  335. .Query(
  336. Filter.Create<Entity>(_selectedType, x => x.ID).IsEqualTo(_entityID),
  337. _grid.LoadEditorColumns()
  338. )
  339. .ToObjects(_selectedType)
  340. .OfType<Entity>()
  341. .FirstOrDefault();
  342. }
  343. _entity ??= Activator.CreateInstance(_selectedType) as Entity;
  344. if (_entity == null)
  345. return;
  346. _grid?.InitialiseEditorForm(Editor, new object[] { _entity }, null, true);
  347. _process = new Button()
  348. {
  349. Content = "Mark as Processed",
  350. BorderBrush = new SolidColorBrush(Colors.DarkBlue),
  351. Background = new SolidColorBrush(Colors.DodgerBlue),
  352. Margin = new Thickness(0, 0, 5, 0),
  353. Padding = new Thickness(10, 0, 10, 0),
  354. IsEnabled = _processenabled
  355. };
  356. _process.Click += (sender, args) =>
  357. {
  358. if (Editor.Validate())
  359. {
  360. DoSave(true);
  361. }
  362. };
  363. Editor.AddButton(_process);
  364. IsChanged = false;
  365. if (_selectedType == typeof(JobDocumentSetMileStone))
  366. {
  367. var disabled = _entityID == Guid.Empty;
  368. foreach (var page in Editor.Pages)
  369. {
  370. if (page is DynamicEditorGrid.DynamicEditPage editPage)
  371. {
  372. foreach (var editor in editPage.Editors)
  373. {
  374. if (editor.EditorDefinition is not BaseCodeEditor)
  375. {
  376. editor.IsEnabled = !disabled && editor.EditorDefinition.Editable.IsEditable();
  377. }
  378. }
  379. }
  380. else
  381. {
  382. page.ReadOnly = disabled;
  383. }
  384. }
  385. }
  386. }
  387. private void _panel_OnOnChanged(object sender, DynamicSplitPanelSettings e)
  388. {
  389. _settings.PreviewWidth = e.AnchorWidth;
  390. new UserConfiguration<DataEntryPanelSettings>().Save(_settings);
  391. }
  392. private void CreateMenuItem<T>(ContextMenu menu, Expression<Func<T,object>> column, string caption, Action<string> action)
  393. {
  394. var columnname = CoreUtils.GetFullPropertyName<T, object>(column, ".");
  395. if (Editor.FindEditor(columnname) != null)
  396. {
  397. var menuitem = new MenuItem()
  398. {
  399. Header = caption
  400. };
  401. menuitem.Click += (sender, args) => action?.Invoke(columnname);
  402. menu.Items.Add(menuitem);
  403. }
  404. }
  405. private void CreateMenuItem<T>(ContextMenu menu, Expression<Func<T,object>> column, string caption, Action<MenuItem,string> build)
  406. {
  407. var columnname = CoreUtils.GetFullPropertyName<T, object>(column, ".");
  408. if (Editor.FindEditor(columnname) != null)
  409. {
  410. var menuitem = new MenuItem()
  411. {
  412. Header = caption
  413. };
  414. build.Invoke(menuitem,columnname);
  415. menu.Items.Add(menuitem);
  416. }
  417. }
  418. private void _documents_OnOCRContextMenuOpening(object sender, DocumentOCRContextMenuArgs args)
  419. {
  420. if (GetEditorType() == typeof(Bill))
  421. {
  422. CreateMenuItem<Bill>(args.Menu, x=>x.SupplierLink.ID, "Supplier", (m,c) =>
  423. {
  424. m.Items.Add(new MenuItem() { Header = "(Loading)", IsEnabled = false });
  425. Client.Query<Supplier>(
  426. new Filter<Supplier>(x => x.ABN).IsEqualTo(args.Text).Or(x => x.Name).Contains(args.Text),
  427. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code).Add(x => x.Name),
  428. new SortOrder<Supplier>(x => x.Code),
  429. (o, e) =>
  430. {
  431. if (o != null)
  432. {
  433. Dispatcher.Invoke(() =>
  434. {
  435. m.Items.Clear();
  436. foreach (var supp in o.ToArray<Supplier>())
  437. {
  438. m.Items.Add(new MenuItem()
  439. {
  440. Header = $"{supp.Code}: {supp.Name}",
  441. Command = new ActionCommand(() => { Editor.SetEditorValue(c, supp.ID); })
  442. });
  443. }
  444. if (m.Items.Count.Equals(0))
  445. m.Items.Add(new MenuItem()
  446. { Header = "(No Suppliers Found)", IsEnabled = false });
  447. });
  448. }
  449. }
  450. );
  451. });
  452. CreateMenuItem<Bill>(args.Menu, x => x.Number, "Invoice Number", (c) => Editor.SetEditorValue(c, args.Text));
  453. if (DateTime.TryParse(args.Text, out var date))
  454. {
  455. CreateMenuItem<Bill>(args.Menu, x=>x.BillDate, "Invoice Date", (c) => Editor.SetEditorValue(c, date));
  456. }
  457. }
  458. }
  459. }