diff --git a/Elwig/Services/SyncService.cs b/Elwig/Services/SyncService.cs index a1fd0b9..c7485e8 100644 --- a/Elwig/Services/SyncService.cs +++ b/Elwig/Services/SyncService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Net.Http; using System.Threading.Tasks; using System.Windows; @@ -14,6 +15,9 @@ using System.Windows; namespace Elwig.Services { public static class SyncService { + public static readonly Expression> ChangedMembers = (m) => ((m.XTime == null && m.MTime > 1751328000) || m.MTime > m.XTime) && (m.ITime == null || m.MTime > m.ITime); + public static readonly Expression> ChangedDeliveries = (d) => ((d.XTime == null && d.MTime > 1751328000) || d.MTime > d.XTime) && (d.ITime == null || d.MTime > d.ITime); + public static async Task Upload(string url, string username, string password, IQueryable query, IEnumerable filterNames) { try { var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip"; @@ -43,8 +47,10 @@ namespace Elwig.Services { MessageBox.Show("Es wurden keine Mitglieder zum Hochladen ausgewählt!", "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error); } else { + var exportedAt = DateTime.Now; await ElwigData.Export(path, members, areaComs, wbKgs, filterNames); await Utils.UploadExportData(path, url, username, password); + await UpdateExportedAt(members, [], exportedAt); MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!", "Mitglieder hochgeladen", MessageBoxButton.OK, MessageBoxImage.Information); } @@ -64,7 +70,7 @@ namespace Elwig.Services { var list = await query .Select(p => p.Delivery) .Distinct() - .Include(d => d.Parts).ThenInclude(p => p.PartModifiers) + .Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier) .Include(d => d.Parts).ThenInclude(p => p.Rd) .Include(d => d.Parts).ThenInclude(p => p.Kg!.Gl) .AsSplitQuery() @@ -80,8 +86,10 @@ namespace Elwig.Services { MessageBox.Show("Es wurden keine Lieferungen zum Hochladen ausgewählt!", "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error); } else { + var exportedAt = DateTime.Now; await ElwigData.Export(path, list, wbKgs, filterNames); await Utils.UploadExportData(path, url, username, password); + await UpdateExportedAt([], list, exportedAt); MessageBox.Show($"Hochladen von {list.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochgeladen", MessageBoxButton.OK, MessageBoxImage.Information); } @@ -94,6 +102,73 @@ namespace Elwig.Services { } } + public static async Task UploadModified(string url, string username, string password) { + try { + var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip"); + List members; + List areaComs; + List deliveries; + using (var ctx = new AppDbContext()) { + members = await ctx.Members + .Where(ChangedMembers) + .Include(m => m.BillingAddress) + .Include(m => m.TelephoneNumbers) + .Include(m => m.EmailAddresses) + .Include(m => m.DefaultWbKg!.Gl) + .OrderBy(m => m.MgNr) + .AsSplitQuery() + .ToListAsync(); + areaComs = await ctx.Members + .Where(ChangedMembers) + .SelectMany(m => m.AreaCommitments) + .Include(c => c.Rd) + .Include(c => c.Kg.Gl) + .OrderBy(c => c.MgNr).ThenBy(c => c.FbNr) + .ToListAsync(); + deliveries = await ctx.Deliveries + .Where(ChangedDeliveries) + .Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier) + .Include(d => d.Parts).ThenInclude(p => p.Rd) + .Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl) + .OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr) + .AsSplitQuery() + .ToListAsync(); + } + var wbKgs = members + .Where(m => m.DefaultWbKg != null) + .Select(m => m.DefaultWbKg!) + .Union(areaComs.Select(c => c.Kg)) + .Union(deliveries.SelectMany(d => d.Parts) + .Where(p => p.Kg != null) + .Select(p => p.Kg!)) + .DistinctBy(k => k.KgNr) + .OrderBy(k => k.KgNr) + .ToList(); + if (members.Count == 0 && deliveries.Count == 0) { + MessageBox.Show("Es gibt keine geänderten Mitglieder oder Lieferungen, die hochgeladen werden könnten!", "Mitglieder und Lieferungen hochladen", + MessageBoxButton.OK, MessageBoxImage.Information); + } else { + var exportedAt = DateTime.Now; + await (new ElwigData.ElwigExport { + Members = (members, ["geändert seit letztem Export"]), + AreaComs = (areaComs, ["von exportierten Mitgliedern"]), + Deliveries = (deliveries, ["geändert seit letzem Export"]), + WbKgs = (wbKgs, ["von exportierten Mitgliedern, Flächenbindungen und Lieferungen"]), + }).Export(path); + await Utils.UploadExportData(path, url, username, password); + await UpdateExportedAt(members, deliveries, exportedAt); + MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern und {deliveries.Count:N0} Lieferungen erfolgreich!", "Mitglieder und Lieferungen hochladen", + MessageBoxButton.OK, MessageBoxImage.Information); + } + } catch (HttpRequestException exc) { + MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error); + } catch (TaskCanceledException exc) { + MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + public static async Task UploadBranchDeliveries(string url, string username, string password, int? year = null) { try { year ??= Utils.CurrentLastSeason; @@ -101,7 +176,8 @@ namespace Elwig.Services { using var ctx = new AppDbContext(); var deliveries = await ctx.Deliveries .Where(d => d.Year == year && d.ZwstId == App.ZwstId) - .Include(d => d.Parts).ThenInclude(p => p.PartModifiers) + .Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier) + .Include(d => d.Parts).ThenInclude(p => p.Rd) .Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl) .OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr) .AsSplitQuery() @@ -116,8 +192,10 @@ namespace Elwig.Services { MessageBox.Show("Es gibt keine Lieferungen, die hochgeladen werden können!", "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error); } else { + var exportedAt = DateTime.Now; await ElwigData.Export(path, deliveries, wbKgs, [$"{year}", $"Zweigstelle {App.BranchName}"]); await Utils.UploadExportData(path, url, username, password); + await UpdateExportedAt([], deliveries, exportedAt); MessageBox.Show($"Hochladen von {deliveries.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Information); } @@ -130,25 +208,30 @@ namespace Elwig.Services { } } + public static async Task> GetFilesToImport(string url, string username, string password) { + var data = await Utils.GetExportMetaData(url, username, password); + var files = data + .Select(f => new { + Name = f!["name"]!.AsValue().GetValue(), + Timestamp = f!["timestamp"] != null && DateTime.TryParseExact(f!["timestamp"]!.AsValue().GetValue(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null, + ZwstId = f!["meta"]?["zwstid"]?.AsValue().GetValue() ?? f!["zwstid"]?.AsValue().GetValue(), + Device = f!["meta"]?["device"]?.AsValue().GetValue(), + Url = f!["url"]!.AsValue().GetValue(), + Size = f!["size"]!.AsValue().GetValue(), + }) + .Where(f => f.Timestamp >= new DateTime(Utils.CurrentLastSeason, 7, 1)) + .ToList(); + + var imported = await ElwigData.GetImportedFiles(); + return [.. files + .Where(f => f.Device != Environment.MachineName && !imported.Contains(f.Name)) + .Select(f => (f.Name, f.Url)) + ]; + } + public static async Task Download(string url, string username, string password) { try { - var data = await Utils.GetExportMetaData(url, username, password); - var files = data - .Select(f => new { - Name = f!["name"]!.AsValue().GetValue(), - Timestamp = f!["timestamp"] != null && DateTime.TryParseExact(f!["timestamp"]!.AsValue().GetValue(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null, - ZwstId = f!["meta"]?["zwstid"]?.AsValue().GetValue() ?? f!["zwstid"]?.AsValue().GetValue(), - Device = f!["meta"]?["device"]?.AsValue().GetValue(), - Url = f!["url"]!.AsValue().GetValue(), - Size = f!["size"]!.AsValue().GetValue(), - }) - .Where(f => f.Timestamp >= new DateTime(Utils.CurrentLastSeason, 7, 1)) - .ToList(); - - var imported = await ElwigData.GetImportedFiles(); - var import = files - .Where(f => f.Device != Environment.MachineName && !imported.Contains(f.Name)) - .ToList(); + var import = await GetFilesToImport(url, username, password); var paths = new List(); using (var client = Utils.GetHttpClient(username, password)) { foreach (var f in import) { @@ -167,5 +250,28 @@ namespace Elwig.Services { MessageBox.Show(exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error); } } + + private static async Task UpdateExportedAt(IEnumerable member, IEnumerable deliveries, DateTime dateTime) { + var timestamp = ((DateTimeOffset)dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + var mgnrs = string.Join(",", member.Select(m => $"{m.MgNr}").Append("0")); + var dids = string.Join(",", deliveries.Select(d => $"({d.Year},{d.DId})").Append("(0,0)")); + using (var cnx = await AppDbContext.ConnectAsync()) { + await cnx.ExecuteBatch($""" + BEGIN; + UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS'; + UPDATE member SET xtime = {timestamp} WHERE mgnr IN ({mgnrs}); + UPDATE area_commitment SET xtime = {timestamp} WHERE mgnr IN ({mgnrs}); + UPDATE delivery SET xtime = {timestamp} WHERE (year, did) IN ({dids}); + UPDATE delivery_part SET xtime = {timestamp} WHERE (year, did) IN ({dids}); + UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_TIME_TRIGGERS'; + COMMIT; + """); + } + App.HintContextChange(); + } + + public static async Task ChangesAvailable(AppDbContext ctx, string url, string username, string password) { + return await ctx.Members.AnyAsync(ChangedMembers) || await ctx.Deliveries.AnyAsync(ChangedDeliveries) || (Utils.HasInternetConnectivity() && (await GetFilesToImport(url, username, password)).Count > 0); + } } } diff --git a/Elwig/Windows/MainWindow.xaml b/Elwig/Windows/MainWindow.xaml index 669c477..be6cdba 100644 --- a/Elwig/Windows/MainWindow.xaml +++ b/Elwig/Windows/MainWindow.xaml @@ -64,6 +64,23 @@ + + + + + + + + + + + + + + + + + @@ -174,16 +191,18 @@ - { + await SyncService.Download(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); + await SyncService.UploadModified(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); + }); + Mouse.OverrideCursor = null; + } + + private async void Menu_Sync_Download_Click(object sender, RoutedEventArgs evt) { if (App.Config.SyncUrl == null) return; Mouse.OverrideCursor = Cursors.Wait; @@ -205,7 +227,7 @@ namespace Elwig.Windows { Mouse.OverrideCursor = null; } - private async void UploadButton_Click(object sender, RoutedEventArgs evt) { + private async void Menu_Sync_UploadBranchDeliveries_Click(object sender, RoutedEventArgs evt) { if (App.Config.SyncUrl == null) return; Mouse.OverrideCursor = Cursors.Wait; @@ -215,6 +237,16 @@ namespace Elwig.Windows { Mouse.OverrideCursor = null; } + private async void Menu_Sync_UploadModified_Click(object sender, RoutedEventArgs evt) { + if (App.Config.SyncUrl == null) + return; + Mouse.OverrideCursor = Cursors.Wait; + await Task.Run(async () => { + await SyncService.UploadModified(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); + }); + Mouse.OverrideCursor = null; + } + private async void Menu_Database_Download_Click(object sender, RoutedEventArgs evt) { if (App.Config.SyncUrl == null) return; @@ -324,9 +356,42 @@ namespace Elwig.Windows { App.FocusMailWindow(); } - protected override Task OnRenewContext(AppDbContext ctx) { + protected async override Task OnRenewContext(AppDbContext ctx) { SeasonInput_TextChanged(null, null); - return Task.CompletedTask; + CheckSync(); + } + + private void OnSyncTimer(object? sender, EventArgs? evt) { + if (Utils.HasInternetConnectivity()) { + CheckSync(); + } + } + + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs evt) { + if (!evt.IsAvailable) return; + if (Utils.HasInternetConnectivity()) { + CheckSync(1000); + } + } + + private async void CheckSync(int delay = 0) { + if (App.Config.SyncUrl == null) return; + Utils.RunBackground("Daten Synchronisieren", async () => { + await Task.Delay(delay); + var ch = false; + using (var ctx = new AppDbContext()) { + ch = await SyncService.ChangesAvailable(ctx, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); + } + await App.MainDispatcher.BeginInvoke(() => { + if (ch) { + SyncButton_1.Text = "\uEA6A"; + SyncButton_2.Text = "\uEA81"; + } else { + SyncButton_1.Text = "\uE895"; + SyncButton_2.Text = ""; + } + }); + }); } private void SeasonFinish_Expanded(object sender, RoutedEventArgs evt) {