Pārlūkot izejas kodu

Bill panel now uses a DataEntry view list for the documents.

Kenric Nugteren 8 mēneši atpakaļ
vecāks
revīzija
a2e5d3635a

+ 1 - 1
prs.desktop/Panels/DataEntry/DataEntryDocumentWindow.xaml

@@ -6,7 +6,7 @@
         xmlns:local="clr-namespace:PRSDesktop.Panels.DataEntry"
         xmlns:wpf="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
         mc:Ignorable="d"
-        Title="DataEntryDocumentWindow" Height="800" Width="800"
+        Title="View Document" Height="800" Width="800"
         x:Name="Window"
         DataContext="{Binding ElementName=Window}">
     <wpf:ZoomPanel Background="Gray">

+ 2 - 120
prs.desktop/Panels/DataEntry/DataEntryGrid.cs

@@ -16,43 +16,7 @@ using System.Windows.Media.Imaging;
 
 namespace PRSDesktop;
 
-public class DataEntryCachedDocument : ICachedDocument
-{
-    public DateTime TimeStamp { get; set; }
-
-    public Document? Document { get; set; }
-
-    public Guid ID => Document?.ID ?? Guid.Empty;
-
-    public DataEntryCachedDocument() { }
-
-    public DataEntryCachedDocument(Document document)
-    {
-        Document = document;
-        TimeStamp = document.TimeStamp;
-    }
-
-    public void DeserializeBinary(CoreBinaryReader reader, bool full)
-    {
-        TimeStamp = reader.ReadDateTime();
-        if (full)
-        {
-            Document = reader.ReadObject<Document>();
-        }
-    }
-
-    public void SerializeBinary(CoreBinaryWriter writer)
-    {
-        writer.Write(TimeStamp);
-        if (Document is null)
-        {
-            throw new Exception("Cannot serialize incomplete CachedDocument");
-        }
-        writer.WriteObject(Document);
-    }
-}
-
-public class DataEntryCache : DocumentCache<DataEntryCachedDocument>
+public class DataEntryCache : DocumentCache
 {
     public override TimeSpan MaxAge => TimeSpan.FromDays(new UserConfiguration<DataEntryPanelSettings>().Load().CacheAge);
 
@@ -67,88 +31,6 @@ public class DataEntryCache : DocumentCache<DataEntryCachedDocument>
     }
 
     public DataEntryCache(): base(nameof(DataEntryCache)) { }
-
-    protected override DataEntryCachedDocument? LoadDocument(Guid id)
-    {
-        var document = Client.Query(new Filter<Document>(x => x.ID).IsEqualTo(id))
-            .ToObjects<Document>().FirstOrDefault();
-        if(document is not null)
-        {
-            return new DataEntryCachedDocument(document);
-        }
-        else
-        {
-            return null;
-        }
-    }
-    
-    /// <summary>
-    /// Fetch a bunch of documents from the cache or the database, optionally checking against the timestamp listed in the database.
-    /// </summary>
-    /// <param name="ids"></param>
-    /// <param name="checkTimestamp">
-    /// If <see langword="true"/>, then loads <see cref="Document.TimeStamp"/> from the database for all cached documents,
-    /// and if they are older, updates the cache.
-    /// </param>
-    public IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids, bool checkTimestamp = false)
-    {
-        var cached = new List<Guid>();
-        var toLoad = new List<Guid>();
-        foreach (var docID in ids)
-        {
-            if (Has(docID))
-            {
-                cached.Add(docID);
-            }
-            else
-            {
-                toLoad.Add(docID);
-            }
-        }
-
-        var loadedCached = new List<Document>();
-        if (cached.Count > 0)
-        {
-            var docs = Client.Query(
-                new Filter<Document>(x => x.ID).InList(cached.ToArray()),
-                Columns.None<Document>().Add(x => x.TimeStamp, x => x.ID));
-            foreach (var doc in docs.ToObjects<Document>())
-            {
-                try
-                {
-                    var timestamp = GetHeader(doc.ID).Document.TimeStamp;
-                    if (doc.TimeStamp > timestamp)
-                    {
-                        toLoad.Add(doc.ID);
-                    }
-                    else
-                    {
-                        loadedCached.Add(GetFull(doc.ID).Document.Document!);
-                    }
-                }
-                catch (Exception e)
-                {
-                    CoreUtils.LogException("", e, "Error loading cached file");
-                    toLoad.Add(doc.ID);
-                }
-            }
-        }
-
-        if (toLoad.Count > 0)
-        {
-            var loaded = Client.Query(new Filter<Document>(x => x.ID).InList(toLoad.ToArray()))
-                .ToObjects<Document>().ToList();
-            foreach (var loadedDoc in loaded)
-            {
-                Add(new DataEntryCachedDocument(loadedDoc));
-            }
-            return loaded.Concat(loadedCached);
-        }
-        else
-        {
-            return loadedCached;
-        }
-    }
 }
 
 public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
@@ -420,7 +302,7 @@ public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
 
         new Client<DataEntryDocument>().Save(dataentry, "");
 
-        DataEntryCache.Cache.Add(new DataEntryCachedDocument(document));
+        DataEntryCache.Cache.Add(new DocumentCachedDocument(document));
 
         Dispatcher.BeginInvoke(() =>
         {

+ 7 - 34
prs.desktop/Panels/DataEntry/DataEntryList.xaml

@@ -47,40 +47,13 @@
                                        Template="{StaticResource HorizontalSplitter}"
                                        PreviewStyle="{StaticResource HorizontalSplitterPreview}"/>
 
-                    <Border Grid.Row="2"
-                            BorderBrush="Gray" Background="DimGray"
-                            AllowDrop="True"
-                            DragOver="DynamicTabItem_DragOver"
-                            Drop="DynamicTabItem_Drop">
-                        <wpf:ZoomPanel x:Name="ZoomPanel">
-                            <ItemsControl Margin="10" ItemsSource="{Binding ViewList}">
-                                <ItemsControl.ItemsPanel>
-                                    <ItemsPanelTemplate>
-                                        <StackPanel Orientation="Vertical"/>
-                                    </ItemsPanelTemplate>
-                                </ItemsControl.ItemsPanel>
-                                <ItemsControl.ContextMenu>
-                                    <ContextMenu
-                                        x:Name="_contextMenu">
-                                        <ContextMenu.Items>
-                                            <MenuItem x:Name="_Explode" Header="Regroup Pages" Click="_Explode_OnClick"/>
-                                            <MenuItem x:Name="_ExplodeAll" Header="Explode All Pages" Click="_ExplodeAll_OnClick"/>
-                                            <MenuItem x:Name="_ShowImage" Header="View Image" Click="_ShowImage_Click"
-                                                      ToolTip="Show this image in a separate window." Tag="{Binding}"/>
-                                            <MenuItem x:Name="_RotateImage" Header="Rotate Document" Click="_RotateImage_Click"
-                                                      ToolTip="Rotate this document 90° clockwise" Tag="{Binding}"/>
-                                        </ContextMenu.Items>
-                                    </ContextMenu>
-                                </ItemsControl.ContextMenu>
-                                <ItemsControl.ItemTemplate>
-                                    <DataTemplate DataType="ImageSource">
-                                        <Image Source="{Binding}" Margin="0,0,0,20" ContextMenu="{Binding RelativeSource={RelativeSource            AncestorType=ItemsControl,Mode=FindAncestor},Path=ContextMenu}"
-                                               MouseLeftButtonDown="Image_MouseLeftButtonDown"/>
-                                    </DataTemplate>
-                                </ItemsControl.ItemTemplate>
-                            </ItemsControl>
-                        </wpf:ZoomPanel>
-                    </Border>
+                    <local:DataEntryViewList x:Name="ViewList"
+                                             Grid.Row="2"
+                                             AllowDrop="True" DragOver="DynamicTabItem_DragOver" Drop="DynamicTabItem_Drop"
+                                             CanExplode="True"
+                                             Explode="ViewList_Explode"
+                                             ExplodeAll="ViewList_ExplodeAll"
+                                             UpdateDocument="ViewList_UpdateDocument"/>
                 </Grid>
             </dynamic:DynamicTabItem>
             

+ 12 - 328
prs.desktop/Panels/DataEntry/DataEntryList.xaml.cs

@@ -99,32 +99,8 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
 
     public event DateEntrySelectionHandler? SelectionChanged;
 
-    private readonly object _viewListLock = new object();
-
-    private class ViewDocument
-    {
-        public ImageSource Image { get; set; }
-
-        public DataEntryDocument Document { get; set; }
-
-        public int PageNumber { get; set; }
-
-        public ViewDocument(ImageSource image, DataEntryDocument document, int page)
-        {
-            Image = image;
-            Document = document;
-            PageNumber = page;
-        }
-    }
-
-    private List<ViewDocument> ViewDocuments { get; } = new();
-    public ObservableCollection<ImageSource> ViewList { get; init; } = new();
-
-    private List<DataEntryDocumentWindow> OpenWindows = new();
-    
     public DataEntryList()
     {
-        BindingOperations.EnableCollectionSynchronization(ViewList, _viewListLock);
 
         InitializeComponent();
     }
@@ -149,159 +125,7 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
 
     public void Shutdown(CancelEventArgs? cancel)
     {
-        CloseImageWindows();
-    }
-
-    #endregion
-
-    #region View List
-
-    private static List<byte[]> RenderTextFile(string textData)
-    {
-        var pdfDocument = new PdfDocument();
-        var page = pdfDocument.Pages.Add();
-
-        var font = new PdfStandardFont(PdfFontFamily.Courier, 14);
-        var textElement = new PdfTextElement(textData, font);
-        var layoutFormat = new PdfLayoutFormat
-        {
-            Layout = PdfLayoutType.Paginate,
-            Break = PdfLayoutBreakType.FitPage
-        };
-
-        textElement.Draw(page, new RectangleF(0, 0, page.GetClientSize().Width, page.GetClientSize().Height), layoutFormat);
-
-        using var docStream = new MemoryStream();
-        pdfDocument.Save(docStream);
-
-        var loadeddoc = new PdfLoadedDocument(docStream.ToArray());
-        Bitmap[] bmpImages = loadeddoc.ExportAsImage(0, loadeddoc.Pages.Count - 1);
-
-        var jpgEncoder = ImageUtils.GetEncoder(ImageFormat.Jpeg)!;
-        var quality = Encoder.Quality;
-        var encodeParams = new EncoderParameters(1);
-        encodeParams.Param[0] = new EncoderParameter(quality, 100L);
-
-        var images = new List<byte[]>();
-        if (bmpImages != null)
-            foreach (var image in bmpImages)
-            {
-                using var data = new MemoryStream();
-                image.Save(data, jpgEncoder, encodeParams);
-                images.Add(data.ToArray());
-            }
-        return images;
-    }
-
-    private void UpdateViewList(bool force = false)
-    {
-        var selected = _dataEntryGrid.SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToList();
-        if (!force && selected.Count == SelectedScans.Count && !selected.Any(x => SelectedScans.All(y => x.ID != y.ID)))
-            return;
-
-        SelectedScans = selected;
-        ViewList.Clear();
-        ViewDocuments.Clear();
-
-        Task.Run(() =>
-        {
-            var docs = DataEntryCache.Cache.LoadDocuments(SelectedScans.Select(x => x.Document.ID).Distinct(), checkTimestamp: true);
-            LoadDocuments(docs);
-        }).ContinueWith((task) =>
-        {
-            if(task.Exception is not null)
-            {
-                MessageWindow.ShowError("An error occurred while loading the documents", task.Exception);
-            }
-        }, TaskScheduler.FromCurrentSynchronizationContext());
-    }
-
-    private void LoadDocuments(IEnumerable<Document> documents)
-    {
-        var bitmaps = new Dictionary<Guid, List<ImageSource>>();
-        foreach (var document in documents.Where(x=>x.Data?.Any() == true))
-        {
-            List<byte[]> images;
-            var bitmapImages = new List<ImageSource>();
-            var extension = Path.GetExtension(document.FileName).ToLower();
-            if (extension == ".pdf")
-            {
-                images = new List<byte[]>();
-                try
-                {
-                    bitmapImages = ImageUtils.RenderPDFToImageSources(document.Data);
-                }
-                catch (Exception e)
-                {
-                    MessageBox.Show($"Cannot load document '{document.FileName}': {e.Message}");
-                }
-            }
-            else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp")
-            {
-                images = new List<byte[]> { document.Data };
-            }
-            else
-            {
-                images = ImageUtils.RenderTextFileToImages(Encoding.UTF8.GetString(document.Data));
-            }
-
-            bitmapImages.AddRange(images.Select(x =>
-            {
-                try
-                {
-                    return ImageUtils.LoadImage(x);
-                }
-                catch (Exception e)
-                {
-                    Dispatcher.BeginInvoke(() =>
-                    {
-                        MessageWindow.ShowError($"Cannot load document '{document.FileName}", e);
-                    });
-                }
-                return null;
-            }).Where(x => x != null).Cast<ImageSource>());
-
-            foreach (var image in bitmapImages)
-            {
-                if (!bitmaps.TryGetValue(document.ID, out var list))
-                {
-                    list = new List<ImageSource>();
-                    bitmaps[document.ID] = list;
-                }
-
-                list.Add(image);
-            }
-        }
-        ViewDocuments.Clear();
-
-        var maxWidth = 0.0;
-        foreach (var scan in SelectedScans)
-        {
-            if (bitmaps.TryGetValue(scan.Document.ID, out var list))
-            {
-                int page = 1;
-                foreach (var bitmap in list)
-                {
-                    maxWidth = Math.Max(maxWidth, bitmap.Width);
-                    ViewDocuments.Add(new(bitmap, scan, page));
-                    page++;
-                }
-            }
-        }
-
-        lock (_viewListLock)
-        {
-            ViewList.Clear();
-            foreach(var doc in ViewDocuments)
-            {
-                ViewList.Add(doc.Image);
-            }
-            if(maxWidth != 0.0)
-            {
-                ZoomPanel.Scale = ZoomPanel.ActualWidth / (maxWidth * 1.1);
-                ZoomPanel.MinScale = ZoomPanel.Scale / 2;
-            }
-        }
+        ViewList.CloseImageWindows();
     }
 
     #endregion
@@ -387,7 +211,7 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
 
     private void _documents_OnSelectItem(object sender, DynamicGridSelectionEventArgs e)
     {
-        UpdateViewList(false);
+        ViewList.Documents = _dataEntryGrid.SelectedRows.ToArray(x => x.ToObject<DataEntryDocument>());
 
         DoSelect(e.Rows);
     }
@@ -405,7 +229,7 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
             : DateTime.MinValue;
         SelectionChanged?.Invoke(appliesTo, entityid, archived.IsEmpty());
 
-        CloseImageWindows();
+        ViewList.CloseImageWindows();
     }
 
     private void _historyGrid_OnOnSelectItem(object sender, DynamicGridSelectionEventArgs e)
@@ -413,59 +237,6 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
         DoSelect(e.Rows);
     }
 
-    private void CloseImageWindows()
-    {
-        while (OpenWindows.Count > 0)
-        {
-            var win = OpenWindows.Last();
-            OpenWindows.RemoveAt(OpenWindows.Count - 1);
-            win.Close();
-        }
-    }
-    private void OpenImageWindow(ImageSource image)
-    {
-        var window = OpenWindows.FirstOrDefault(x => x.Images.Contains(image));
-        if (window is not null)
-        {
-            window.Activate();
-        }
-        else
-        {
-            window = new DataEntryDocumentWindow();
-            window.Topmost = true;
-            window.Images.Add(image);
-            OpenWindows.Add(window);
-            window.Closed += OpenWindow_Closed;
-            window.Show();
-        }
-    }
-
-    private void Image_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
-    {
-        if (sender is not Image image) return;
-
-        if(e.ClickCount >= 2)
-        {
-            OpenImageWindow(image.Source);
-            e.Handled = true;
-        }
-    }
-
-    private void OpenWindow_Closed(object? sender, EventArgs e)
-    {
-        if (sender is not DataEntryDocumentWindow window) return;
-        OpenWindows.Remove(window);
-    }
-
-    private void _Explode_OnClick(object sender, RoutedEventArgs e)
-    {
-       _dataEntryGrid.DoExplode();
-    }
-    private void _ExplodeAll_OnClick(object sender, RoutedEventArgs e)
-    {
-       _dataEntryGrid.DoExplodeAll();
-    }
-
     private List<DataEntryTag>? _tags;
 
     private void _uploadMenu_OnOpened(object sender, RoutedEventArgs e)
@@ -653,114 +424,27 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
 
     }
 
-    private void _ShowImage_Click(object sender, RoutedEventArgs e)
+    private void ViewList_Explode()
     {
-        if (sender is not MenuItem item || item.Tag is not ImageSource image) return;
-
-        OpenImageWindow(image);
+        _dataEntryGrid.DoExplode();
     }
 
-    private void RotateDocument(Document doc, int pageNumber)
+    private void ViewList_ExplodeAll()
     {
-        var extension = Path.GetExtension(doc.FileName).ToLower();
-        if (extension == ".pdf")
-        {
-            var loadeddoc = new PdfLoadedDocument(doc.Data);
-
-            bool allPages = loadeddoc.PageCount() > 1;
-            if (allPages)
-            {
-                allPages = MessageWindow.New()
-                    .Message("Do you want to rotate all pages in this PDF?")
-                    .Title("Rotate all?")
-                    .AddYesButton("All pages")
-                    .AddNoButton("Just this page")
-                    .Display().Result == MessageWindowResult.Yes;
-            }
-
-            if(allPages)
-            {
-                foreach (var page in loadeddoc.GetPages())
-                {
-                    var rotation = (int)page.Rotation;
-                    rotation = (rotation + 1) % 4;
-                    page.Rotation = (PdfPageRotateAngle)rotation;
-                }
-            }
-            else if(pageNumber <= loadeddoc.PageCount())
-            {
-                var page = loadeddoc.GetPage(pageNumber - 1);
-
-                var rotation = (int)page.Rotation;
-                rotation = (rotation + 1) % 4;
-                page.Rotation = (PdfPageRotateAngle)rotation;
-            }
-
-            doc.Data = loadeddoc.SaveToBytes();
-        }
-        else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp")
-        {
-            using var stream = new MemoryStream(doc.Data);
-            var bitmap = Bitmap.FromStream(stream);
-            bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone);
-            using var outStream = new MemoryStream();
-            bitmap.Save(outStream, extension switch
-            {
-                ".jpg" or ".jpeg" => ImageFormat.Jpeg,
-                ".png" => ImageFormat.Png,
-                _ => ImageFormat.Bmp
-            });
-            doc.Data = outStream.ToArray();
-        }
-        else
-        {
-            using var stream = new MemoryStream(doc.Data);
-            var loadeddoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, stream);
-
-            foreach (var page in loadeddoc.GetPages())
-            {
-                var rotation = (int)page.Rotation;
-                rotation = (rotation + 1) % 4;
-                page.Rotation = (PdfPageRotateAngle)rotation;
-            }
-
-            doc.Data = loadeddoc.SaveToBytes();
-        }
+        _dataEntryGrid.DoExplodeAll();
     }
 
-    private void _RotateImage_Click(object sender, RoutedEventArgs e)
+    private void ViewList_UpdateDocument(DataEntryDocument document, Document doc)
     {
-        if (sender is not MenuItem item || item.Tag is not ImageSource image) return;
-
-        var document = ViewDocuments.FirstOrDefault(x => x.Image == image);
-        if (document is null)
-        {
-            MessageWindow.ShowError("An error occurred", "Document does not exist in ViewDocuments list");
-            return;
-        }
-
-        var doc = DataEntryCache.Cache.LoadDocuments(CoreUtils.One(document.Document.Document.ID), checkTimestamp: true).First();
-        try
-        {
-            RotateDocument(doc, document.PageNumber);
-        }
-        catch(Exception err)
-        {
-            MessageWindow.ShowError("Something went wrong while trying to rotate this document.", err);
-            return;
-        }
-
-        Client.Save(doc, "Rotated by user.");
-
         if (Path.GetExtension(doc.FileName) == ".pdf")
         {
-            document.Document.Thumbnail = ImageUtils.GetPDFThumbnail(doc.Data, 256, 256);
+            document.Thumbnail = ImageUtils.GetPDFThumbnail(doc.Data, 256, 256);
         }
 
-        new Client<DataEntryDocument>().Save(document.Document, "");
+        Client.Save(document, "");
 
-        DataEntryCache.Cache.Add(new DataEntryCachedDocument(doc));
+        DataEntryCache.Cache.Add(new DocumentCachedDocument(doc));
 
-        UpdateViewList(true);
+        ViewList.UpdateViewList(_dataEntryGrid.SelectedRows.ToArray(x => x.ToObject<DataEntryDocument>()), true);
     }
 }

+ 466 - 0
prs.desktop/Panels/DataEntry/DocumentViewList.cs

@@ -0,0 +1,466 @@
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.Wpf;
+using InABox.WPF;
+using PRSDesktop.Panels.DataEntry;
+using Syncfusion.Pdf;
+using Syncfusion.Pdf.Parsing;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using Image = System.Windows.Controls.Image;
+
+namespace PRSDesktop;
+
+/// <summary>
+/// Control that allows to view a list of documents, within a zoom control, and providing methods to rotate/explode data.
+/// </summary>
+/// <remarks>
+/// This is originally from the Data entry panel, and this implementation is a <i>little bit</i> scuffed. Basically, because the <see cref="DataEntryDocument"/>
+/// is not an <see cref="EntityDocument{T}"/>, there is no good shared interface, so I made this abstract, with a type argument. Then to get the "EntityDocument"
+/// ID or the Document ID, there are abstract functions.
+/// <b/>
+/// Note one needs also to provide <see cref="UpdateDocument"/>. This is a function used by the "Rotate Image" button, and its implementation needs
+/// to update the THumbnail of the Entity Document, and save it, along with refreshing the view list.
+/// </remarks>
+/// <typeparam name="TDocument"></typeparam>
+public abstract class DocumentViewList<TDocument> : UserControl, INotifyPropertyChanged
+{
+    public static readonly DependencyProperty CanRotateImageProperty = DependencyProperty.Register(nameof(CanRotateImage), typeof(bool), typeof(DocumentViewList<TDocument>), new PropertyMetadata(true, CanRotateImage_Changed));
+
+    private static void CanRotateImage_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    {
+        if (d is not DocumentViewList<TDocument> list) return;
+        list.DoPropertyChanged(e.Property.Name);
+    }
+
+    private IList<TDocument> _documents = [];
+    public IList<TDocument> Documents
+    {
+        get => _documents;
+        set
+        {
+            UpdateViewList(value);
+        }
+    }
+
+    private readonly object _viewListLock = new object();
+
+    private class ViewDocument
+    {
+        public ImageSource Image { get; set; }
+
+        public TDocument Document { get; set; }
+
+        public int PageNumber { get; set; }
+
+        public ViewDocument(ImageSource image, TDocument document, int page)
+        {
+            Image = image;
+            Document = document;
+            PageNumber = page;
+        }
+    }
+
+    private List<ViewDocument> ViewDocuments { get; } = new();
+    public ObservableCollection<ImageSource> ViewList { get; init; } = new();
+
+    private ZoomPanel ZoomPanel;
+
+    private bool _canExplode;
+    public bool CanExplode
+    {
+        get => _canExplode;
+        set
+        {
+            _canExplode = value;
+            DoPropertyChanged();
+        }
+    }
+
+    public event Action? Explode;
+    public event Action? ExplodeAll;
+    public event Action<TDocument, Document>? UpdateDocument;
+
+    public bool CanRotateImage
+    {
+        get => (bool)GetValue(CanRotateImageProperty);
+        set => SetValue(CanRotateImageProperty, value);
+    }
+
+    public DocumentViewList()
+    {
+        var border = new Border();
+        border.BorderBrush = Colors.Gray.ToBrush();
+        border.Background = Colors.DimGray.ToBrush();
+
+        ZoomPanel = new ZoomPanel();
+
+        var itemsControl = new ItemsControl();
+        itemsControl.Margin = new Thickness(10);
+        itemsControl.ItemsSource = ViewList;
+        var factory = new FrameworkElementFactory(typeof(StackPanel));
+        factory.SetValue(StackPanel.OrientationProperty, Orientation.Vertical);
+        itemsControl.ItemsPanel = new ItemsPanelTemplate(factory);
+
+        itemsControl.ContextMenu = new ContextMenu();
+        var explode = itemsControl.ContextMenu.AddItem("Regroup Pages", null, Explode_Click);
+        explode.Bind(VisibilityProperty, this, x => x.CanExplode, new InABox.WPF.BooleanToVisibilityConverter(Visibility.Visible, Visibility.Collapsed));
+        var explodeAll = itemsControl.ContextMenu.AddItem("Explode All Pages", null, ExplodeAll_Click);
+        explodeAll.Bind(VisibilityProperty, this, x => x.CanExplode, new InABox.WPF.BooleanToVisibilityConverter(Visibility.Visible, Visibility.Collapsed));
+
+        var viewImage = new MenuItem()
+        {
+            Header = "View Image"
+        };
+        viewImage.ToolTip = "Show this image in a separate window.";
+        viewImage.Bind<ImageSource, ImageSource>(MenuItem.TagProperty, x => x);
+        viewImage.Click += ViewImage_Click;
+        itemsControl.ContextMenu.Items.Add(viewImage);
+
+        var rotateImage = new MenuItem()
+        {
+            Header = "Rotate Document"
+        };
+        rotateImage.ToolTip = "Rotate this document 90° clockwise";
+        rotateImage.Bind<ImageSource, ImageSource>(MenuItem.TagProperty, x => x);
+        rotateImage.SetBinding(MenuItem.IsEnabledProperty, new Binding("CanRotateImage") { Source = this });
+        rotateImage.Click += RotateImage_Click;
+        itemsControl.ContextMenu.Items.Add(rotateImage);
+
+        itemsControl.ItemTemplate = TemplateGenerator.CreateDataTemplate(() =>
+        {
+            var img = new Image();
+            img.Bind<ImageSource, ImageSource>(Image.SourceProperty, x => x);
+            img.ContextMenu = itemsControl.ContextMenu;
+            img.MouseLeftButtonDown += Img_MouseLeftButtonDown;
+            return img;
+        });
+
+        ZoomPanel.Content = itemsControl;
+        border.Child = ZoomPanel;
+        Content = border;
+
+        BindingOperations.EnableCollectionSynchronization(ViewList, _viewListLock);
+    }
+
+    protected abstract Guid GetID(TDocument document);
+    protected abstract Guid GetDocumentID(TDocument document);
+    protected abstract IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids);
+
+    public void UpdateViewList(IList<TDocument> documents, bool force = false)
+    {
+        if (!force && documents.Count == _documents.Count && !documents.Any(x => _documents.All(y => GetID(x) != GetID(y))))
+            return;
+
+        _documents = documents;
+        ViewList.Clear();
+        ViewDocuments.Clear();
+
+        if(_documents.Count == 0)
+        {
+            return;
+        }
+
+        Task.Run(() =>
+        {
+            var docs = LoadDocuments(Documents.Select(GetDocumentID).Distinct());
+            LoadDocuments(docs);
+        }).ContinueWith((task) =>
+        {
+            if(task.Exception is not null)
+            {
+                MessageWindow.ShowError("An error occurred while loading the documents", task.Exception);
+            }
+        }, TaskScheduler.FromCurrentSynchronizationContext());
+    }
+
+    private void LoadDocuments(IEnumerable<Document> documents)
+    {
+        var bitmaps = new Dictionary<Guid, List<ImageSource>>();
+        foreach (var document in documents.Where(x=>x.Data?.Any() == true))
+        {
+            List<byte[]> images;
+            var bitmapImages = new List<ImageSource>();
+            var extension = Path.GetExtension(document.FileName).ToLower();
+            if (extension == ".pdf")
+            {
+                images = new List<byte[]>();
+                try
+                {
+                    bitmapImages = ImageUtils.RenderPDFToImageSources(document.Data);
+                }
+                catch (Exception e)
+                {
+                    MessageBox.Show($"Cannot load document '{document.FileName}': {e.Message}");
+                }
+            }
+            else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp")
+            {
+                images = new List<byte[]> { document.Data };
+            }
+            else
+            {
+                images = ImageUtils.RenderTextFileToImages(Encoding.UTF8.GetString(document.Data));
+            }
+
+            bitmapImages.AddRange(images.Select(x =>
+            {
+                try
+                {
+                    return ImageUtils.LoadImage(x);
+                }
+                catch (Exception e)
+                {
+                    Dispatcher.BeginInvoke(() =>
+                    {
+                        MessageWindow.ShowError($"Cannot load document '{document.FileName}", e);
+                    });
+                }
+                return null;
+            }).Where(x => x != null).Cast<ImageSource>());
+
+            foreach (var image in bitmapImages)
+            {
+                if (!bitmaps.TryGetValue(document.ID, out var list))
+                {
+                    list = new List<ImageSource>();
+                    bitmaps[document.ID] = list;
+                }
+
+                list.Add(image);
+            }
+        }
+        ViewDocuments.Clear();
+
+        var maxWidth = 0.0;
+        foreach (var scan in Documents)
+        {
+            if (bitmaps.TryGetValue(GetDocumentID(scan), out var list))
+            {
+                int page = 1;
+                foreach (var bitmap in list)
+                {
+                    maxWidth = Math.Max(maxWidth, bitmap.Width);
+                    ViewDocuments.Add(new(bitmap, scan, page));
+                    page++;
+                }
+            }
+        }
+
+        lock (_viewListLock)
+        {
+            ViewList.Clear();
+            foreach(var doc in ViewDocuments)
+            {
+                ViewList.Add(doc.Image);
+            }
+            if(maxWidth != 0.0)
+            {
+                ZoomPanel.Scale = ZoomPanel.ActualWidth / (maxWidth * 1.1);
+                ZoomPanel.MinScale = ZoomPanel.Scale / 2;
+            }
+        }
+    }
+
+    private void RotateDocument(Document doc, int pageNumber)
+    {
+        var extension = Path.GetExtension(doc.FileName).ToLower();
+        if (extension == ".pdf")
+        {
+            var loadeddoc = new PdfLoadedDocument(doc.Data);
+
+            bool allPages = loadeddoc.PageCount() > 1;
+            if (allPages)
+            {
+                allPages = MessageWindow.New()
+                    .Message("Do you want to rotate all pages in this PDF?")
+                    .Title("Rotate all?")
+                    .AddYesButton("All pages")
+                    .AddNoButton("Just this page")
+                    .Display().Result == MessageWindowResult.Yes;
+            }
+
+            if(allPages)
+            {
+                foreach (var page in loadeddoc.GetPages())
+                {
+                    var rotation = (int)page.Rotation;
+                    rotation = (rotation + 1) % 4;
+                    page.Rotation = (PdfPageRotateAngle)rotation;
+                }
+            }
+            else if(pageNumber <= loadeddoc.PageCount())
+            {
+                var page = loadeddoc.GetPage(pageNumber - 1);
+
+                var rotation = (int)page.Rotation;
+                rotation = (rotation + 1) % 4;
+                page.Rotation = (PdfPageRotateAngle)rotation;
+            }
+
+            doc.Data = loadeddoc.SaveToBytes();
+        }
+        else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp")
+        {
+            using var stream = new MemoryStream(doc.Data);
+            var bitmap = Bitmap.FromStream(stream);
+            bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone);
+            using var outStream = new MemoryStream();
+            bitmap.Save(outStream, extension switch
+            {
+                ".jpg" or ".jpeg" => ImageFormat.Jpeg,
+                ".png" => ImageFormat.Png,
+                _ => ImageFormat.Bmp
+            });
+            doc.Data = outStream.ToArray();
+        }
+        else
+        {
+            using var stream = new MemoryStream(doc.Data);
+            var loadeddoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, stream);
+
+            foreach (var page in loadeddoc.GetPages())
+            {
+                var rotation = (int)page.Rotation;
+                rotation = (rotation + 1) % 4;
+                page.Rotation = (PdfPageRotateAngle)rotation;
+            }
+
+            doc.Data = loadeddoc.SaveToBytes();
+        }
+    }
+
+    private void RotateImage_Click(object sender, RoutedEventArgs e)
+    {
+        if (sender is not MenuItem item || item.Tag is not ImageSource image) return;
+
+        var document = ViewDocuments.FirstOrDefault(x => x.Image == image);
+        if (document is null)
+        {
+            MessageWindow.ShowError("An error occurred", "Document does not exist in ViewDocuments list");
+            return;
+        }
+
+        var doc = LoadDocuments(CoreUtils.One(GetDocumentID(document.Document))).First();
+        try
+        {
+            RotateDocument(doc, document.PageNumber);
+        }
+        catch(Exception err)
+        {
+            MessageWindow.ShowError("Something went wrong while trying to rotate this document.", err);
+            return;
+        }
+
+        Client.Save(doc, "Rotated by user.");
+
+        UpdateDocument?.Invoke(document.Document, doc);
+    }
+
+    private void ViewImage_Click(object sender, RoutedEventArgs e)
+    {
+        if (sender is not MenuItem item || item.Tag is not ImageSource image) return;
+
+        OpenImageWindow(image);
+    }
+
+    private void ExplodeAll_Click()
+    {
+        ExplodeAll?.Invoke();
+    }
+
+    private void Explode_Click()
+    {
+        Explode?.Invoke();
+    }
+
+    #region Image Window
+
+    private List<DataEntryDocumentWindow> OpenWindows = new();
+
+    public void CloseImageWindows()
+    {
+        while (OpenWindows.Count > 0)
+        {
+            var win = OpenWindows.Last();
+            OpenWindows.RemoveAt(OpenWindows.Count - 1);
+            win.Close();
+        }
+    }
+    private void OpenImageWindow(ImageSource image)
+    {
+        var window = OpenWindows.FirstOrDefault(x => x.Images.Contains(image));
+        if (window is not null)
+        {
+            window.Activate();
+        }
+        else
+        {
+            window = new DataEntryDocumentWindow();
+            window.Topmost = true;
+            window.Images.Add(image);
+            OpenWindows.Add(window);
+            window.Closed += OpenWindow_Closed;
+            window.Show();
+        }
+    }
+
+    private void OpenWindow_Closed(object? sender, EventArgs e)
+    {
+        if (sender is not DataEntryDocumentWindow window) return;
+        OpenWindows.Remove(window);
+    }
+
+    private void Img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+    {
+        if (sender is not Image image) return;
+
+        if(e.ClickCount >= 2)
+        {
+            OpenImageWindow(image.Source);
+            e.Handled = true;
+        }
+    }
+
+    #endregion
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected void DoPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}
+
+public class DataEntryViewList : DocumentViewList<DataEntryDocument>
+{
+    protected override IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids)
+    {
+        return DataEntryCache.Cache.LoadDocuments(ids, checkTimestamp: true);
+    }
+
+    protected override Guid GetID(DataEntryDocument document)
+    {
+        return document.ID;
+    }
+
+    protected override Guid GetDocumentID(DataEntryDocument document)
+    {
+        return document.Document.ID;
+    }
+}

+ 30 - 3
prs.desktop/Panels/Suppliers/Bills/SupplierBillPanel.xaml

@@ -4,12 +4,16 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:local="clr-namespace:PRSDesktop"
+             xmlns:sf="http://schemas.syncfusion.com/wpf"
              xmlns:dynamicGrid="clr-namespace:InABox.DynamicGrid;assembly=InABox.Wpf"
+             xmlns:wpf="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
              mc:Ignorable="d"
-             d:DesignHeight="300" d:DesignWidth="1000">
+             d:DesignHeight="300" d:DesignWidth="1000"
+             x:Name="Control">
     <dynamicGrid:DynamicSplitPanel View="Combined" AnchorWidth="500"
                                    x:Name="SplitPanel"
-                                   OnChanged="SplitPanel_OnChanged">
+                                   OnChanged="SplitPanel_OnChanged"
+                                   DataContext="{Binding ElementName=Control}">
         
         <dynamicGrid:DynamicSplitPanel.Header>
             <Border BorderBrush="Gray" BorderThickness="0.75" Background="WhiteSmoke" Height="25">
@@ -18,7 +22,30 @@
         </dynamicGrid:DynamicSplitPanel.Header>
         
         <dynamicGrid:DynamicSplitPanel.Master>
-            <local:SupplierBills x:Name="Bills" OnSelectItem="Bills_OnOnSelectItem"/>
+            <Grid>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="*"/>
+                    <RowDefinition Height="Auto"/>
+                    <RowDefinition Height="2*"/>
+                </Grid.RowDefinitions>
+                <local:SupplierBills x:Name="Bills"
+                                     Grid.Row="0"
+                                     OnSelectItem="Bills_OnOnSelectItem"/>
+                
+                <sf:SfGridSplitter Grid.Row="1"
+                                   Height="4"
+                                   HorizontalAlignment="Stretch"
+                                   Background="Transparent"
+                                   ResizeBehavior="PreviousAndNext"
+                                   Template="{StaticResource HorizontalSplitter}"
+                                   PreviewStyle="{StaticResource HorizontalSplitterPreview}"/>
+                <local:BillDocumentViewList x:Name="ViewList" Grid.Row="2"
+                                            DragOver="Documents_DragOver"
+                                            Drop="Documents_Drop"
+                                            AllowDrop="True"
+                                            CanRotateImage="{Binding CanRotateImage}"
+                                            UpdateDocument="ViewList_UpdateDocument"/>
+            </Grid>
         </dynamicGrid:DynamicSplitPanel.Master>
         
         <dynamicGrid:DynamicSplitPanel.Detail>

+ 231 - 11
prs.desktop/Panels/Suppliers/Bills/SupplierBillPanel.xaml.cs

@@ -11,6 +11,11 @@ using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.WPF;
 using InABox.Wpf;
+using System.Threading.Tasks;
+using System.IO;
+using System.Collections.ObjectModel;
+using System.Windows.Media;
+using System.Runtime.CompilerServices;
 
 namespace PRSDesktop;
 
@@ -37,7 +42,7 @@ public class SupplierBillPanelProperties : BaseObject, IGlobalConfigurationSetti
     public bool AllowBlankBillNumbers { get; set; }
 }
 
-public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesPanel<SupplierBillPanelProperties, CanConfigureAccountsPanels>
+public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesPanel<SupplierBillPanelProperties, CanConfigureAccountsPanels>, INotifyPropertyChanged
 {
     private SupplierBillPanelSettings settings;
 
@@ -45,7 +50,9 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
     {
         InitializeComponent();
     }
-    
+
+    #region IPanel Interface
+
     public bool IsReady { get; set; }
 
     public event DataModelUpdateEvent? OnUpdateDataModel;
@@ -91,8 +98,10 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
             settings.ViewType == ScreenViewType.Details ? DynamicSplitPanelView.Detail : DynamicSplitPanelView.Combined;
         SplitPanel.AnchorWidth = settings.AnchorWidth;
 
-        Bill.SetLayoutType<SupplierBillEditLayout>();
+        Bill.SetLayoutType<VerticalDynamicEditorGridLayout>();
         Bills.Refresh(true, false);
+
+        BillPanelDocumentCache.Cache.ClearOld();
     }
     
     private void CheckSaved(CancelEventArgs cancel)
@@ -134,13 +143,7 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
     {
     }
     
-    public Dictionary<Type, CoreTable> DataEnvironment()
-    {
-        return new Dictionary<Type, CoreTable>
-        {
-            { typeof(Bill), Bills.Data }
-        };
-    }
+    #endregion
 
     private Bill[]? _bills = null;
     private CoreRow[]? _editRows = null;
@@ -161,12 +164,14 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
             _bills = Bills.LoadBills(_editRows);
             Bills.InitialiseEditorForm(Bill, _bills, null, true);
             Bill.Visibility = Visibility.Visible;
+            LinkDocumentPage();
         }
         else
         {
             _bills = null;
             _editRows = null;
             Bill.Visibility = Visibility.Hidden;
+            ClearDocumentPage();
         }
     }
 
@@ -199,6 +204,12 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
         bChanged = changed;
         Bills.IsEnabled = !changed;
         Bill.HideButtons = !changed;
+
+        if (!changed)
+        {
+            bDocumentsChanged = false;
+            DoPropertyChanged(nameof(CanRotateImage));
+        }
     }
 
     private bool bChanged = false;
@@ -215,4 +226,213 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
 
         new UserConfiguration<SupplierBillPanelSettings>().Save(settings);
     }
-}
+
+    #region Documents
+
+    private DynamicDocumentGrid<BillDocument, Bill, BillLink>? DocumentPage;
+
+    public bool CanRotateImage => !bDocumentsChanged;
+    
+    private void ClearDocumentPage()
+    {
+        ViewList.Documents = [];
+    }
+
+    private void LinkDocumentPage()
+    {
+        DocumentPage = Bill.Pages.OfType<DynamicDocumentGrid<BillDocument, Bill, BillLink>>().FirstOrDefault();
+        if(DocumentPage is not null)
+        {
+            DocumentPage.AfterRefresh += (o, e) =>
+            {
+                if (bRefreshingDocuments)
+                {
+                    return;
+                }
+                ReloadViewList(true);
+            };
+            DocumentPage.OnChanged += DocumentPage_OnChanged;
+        }
+    }
+
+    private bool bRefreshingDocuments = false;
+    private bool bDocumentsChanged = false;
+
+    private void DocumentPage_OnChanged(object? sender, EventArgs e)
+    {
+        bDocumentsChanged = true;
+        DoPropertyChanged(nameof(CanRotateImage));
+        if (bRefreshingDocuments)
+        {
+            return;
+        }
+        ReloadViewList();
+    }
+
+    private void ReloadViewList(bool force = false)
+    {
+        if(DocumentPage is null)
+        {
+            return;
+        }
+
+        ViewList.UpdateViewList(DocumentPage.LoadItems(DocumentPage.Data.Rows), force);
+    }
+
+    private void ViewList_UpdateDocument(BillDocument document, Document doc)
+    {
+        if(DocumentPage is null)
+        {
+            return;
+        }
+
+        if (Path.GetExtension(doc.FileName) == ".pdf")
+        {
+            document.Thumbnail = ImageUtils.GetPDFThumbnail(doc.Data, 256, 256);
+        }
+
+        Client.Save(document, "");
+
+        BillPanelDocumentCache.Cache.Add(new DocumentCachedDocument(doc));
+        DocumentPage.Refresh(false, true);
+
+        ViewList.UpdateViewList(DocumentPage.LoadItems(DocumentPage.Data.Rows), true);
+    }
+
+    private void Documents_DragOver(object sender, DragEventArgs e)
+    {
+        if(Bills.SelectedRows.Length == 0)
+        {
+            e.Effects = DragDropEffects.None;
+        }
+        else
+        {
+            if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent("FileGroupDescriptor"))
+            {
+                e.Effects = DragDropEffects.Copy;
+            }
+            else
+            {
+                e.Effects = DragDropEffects.None;
+            }
+        }
+        e.Handled = true;
+    }
+
+    private void Documents_Drop(object sender, DragEventArgs e)
+    {
+        if(Bills.SelectedRows.Length == 0 || DocumentPage is null)
+        {
+            return;
+        }
+        var selectedBill = Bills.SelectedRows[0].ToObject<Bill>();
+
+        Task.Run(() =>
+        {
+            Dispatcher.Invoke(() =>
+            {
+                Progress.Show("Uploading documents");
+                try
+                {
+                    var result = DocumentUtils.HandleFileDrop(e);
+                    if (result is not null)
+                    {
+                        var docs = new List<Document>();
+                        foreach (var (filename, stream) in result)
+                        {
+                            var doc = new Document();
+                            doc.FileName = filename;
+                            if (stream is null)
+                            {
+                                doc.Data = File.ReadAllBytes(filename);
+                                doc.TimeStamp = new FileInfo(filename).LastWriteTime;
+                            }
+                            else
+                            {
+                                using var memStream = new MemoryStream();
+                                stream.CopyTo(memStream);
+                                doc.Data = memStream.ToArray();
+                                doc.TimeStamp = DateTime.Now;
+                            }
+                            doc.CRC = CoreUtils.CalculateCRC(doc.Data);
+                            docs.Add(doc);
+
+                        }
+                        Client.Save(docs, "Initial Upload");
+
+                        var billDocs = new List<BillDocument>();
+                        foreach (var doc in docs)
+                        {
+                            var billDoc = new BillDocument();
+                            billDoc.DocumentLink.CopyFrom(doc);
+                            billDoc.EntityLink.CopyFrom(selectedBill);
+                            billDocs.Add(billDoc);
+                        }
+                        DocumentPage.SaveItems(billDocs);
+                        try
+                        {
+                            bRefreshingDocuments = true;
+                            DocumentPage.DoChanged();
+                        }
+                        finally
+                        {
+                            bRefreshingDocuments = false;
+                        }
+                    }
+                    Progress.Close();
+                }
+                catch (Exception e)
+                {
+                    Progress.Close();
+                    MessageWindow.ShowError("Could not upload documents.", e);
+                }
+            });
+        });
+    }
+
+    #endregion
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected void DoPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}
+
+public class BillDocumentViewList : DocumentViewList<BillDocument>
+{
+    protected override IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids)
+    {
+        return BillPanelDocumentCache.Cache.LoadDocuments(ids, checkTimestamp: true);
+    }
+
+    protected override Guid GetID(BillDocument document)
+    {
+        return document.ID;
+    }
+
+    protected override Guid GetDocumentID(BillDocument document)
+    {
+        return document.DocumentLink.ID;
+    }
+}
+
+public class BillPanelDocumentCache : DocumentCache
+{
+    public override TimeSpan MaxAge => TimeSpan.FromHours(1);
+
+    private static BillPanelDocumentCache? _cache;
+    public static BillPanelDocumentCache Cache
+    {
+        get
+        {
+            _cache ??= DocumentCaches.GetOrRegister<BillPanelDocumentCache>();
+            return _cache;
+        }
+    }
+
+    public BillPanelDocumentCache() : base(nameof(BillPanelDocumentCache))
+    {
+    }
+}

+ 0 - 9
prs.desktop/Panels/Suppliers/Payments/SupplierPayment.cs

@@ -60,14 +60,5 @@ namespace PRSDesktop
         public void Heartbeat(TimeSpan time)
         {
         }
-
-
-        public Dictionary<Type, CoreTable> DataEnvironment()
-        {
-            return new Dictionary<Type, CoreTable>
-            {
-                { typeof(Payment), Data }
-            };
-        }
     }
 }