From 918555f2b8350c50eea080b2e2c98a94c55210be Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Wed, 8 Jul 2026 22:04:20 +0200 Subject: [PATCH] [#20] MemberAdminWindow: Add MemberBusinessSharesAdminWindow to manage member shares --- Elwig/App.xaml.cs | 4 + Elwig/Controls/CheckComboBox.cs | 2 +- Elwig/Documents/MemberDataSheet.cs | 82 +++-- Elwig/Documents/MemberList.cs | 2 +- Elwig/Helpers/AppDbContext.cs | 4 + Elwig/Helpers/Billing/Billing.cs | 10 +- Elwig/Helpers/Export/ElwigData.cs | 24 +- Elwig/Helpers/Validator.cs | 22 +- Elwig/Models/BusinessShareType.cs | 5 + Elwig/Models/Dtos/MemberListData.cs | 8 +- Elwig/Models/Entities/Member.cs | 4 +- Elwig/Models/Entities/MemberHistory.cs | 77 +++- Elwig/Resources/Sql/39-40.sql | 35 +- Elwig/Services/AreaComService.cs | 3 +- Elwig/Services/DeliveryAncmtService.cs | 1 - Elwig/Services/DeliveryService.cs | 16 +- Elwig/Services/InteractionService.cs | 13 + Elwig/Services/MemberBusinessSharesService.cs | 98 +++++ Elwig/Services/MemberService.cs | 23 +- Elwig/Services/PaymentVariantService.cs | 2 +- Elwig/Services/SyncService.cs | 5 +- .../MemberBusinessSharesViewModel.cs | 76 ++++ Elwig/Windows/AdministrationWindow.cs | 13 +- Elwig/Windows/BaseDataWindow.xaml.cs | 26 +- Elwig/Windows/DeliveryAdminWindow.xaml.cs | 7 +- Elwig/Windows/MemberAdminWindow.xaml | 2 +- Elwig/Windows/MemberAdminWindow.xaml.cs | 15 +- .../MemberBusinessSharesAdminWindow.xaml | 214 +++++++++++ .../MemberBusinessSharesAdminWindow.xaml.cs | 347 ++++++++++++++++++ 29 files changed, 987 insertions(+), 153 deletions(-) create mode 100644 Elwig/Models/BusinessShareType.cs create mode 100644 Elwig/Services/MemberBusinessSharesService.cs create mode 100644 Elwig/ViewModels/MemberBusinessSharesViewModel.cs create mode 100644 Elwig/Windows/MemberBusinessSharesAdminWindow.xaml create mode 100644 Elwig/Windows/MemberBusinessSharesAdminWindow.xaml.cs diff --git a/Elwig/App.xaml.cs b/Elwig/App.xaml.cs index 74e60a8..5e77ff4 100644 --- a/Elwig/App.xaml.cs +++ b/Elwig/App.xaml.cs @@ -344,6 +344,10 @@ namespace Elwig { return FocusWindow(() => new(true), w => w.ViewModel.IsReceipt); } + public static MemberBusinessSharesAdminWindow FocusMemberBusinessSharesWindow(int mgnr) { + return FocusWindow(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr); + } + public static DeliveryAdminWindow FocusMemberDeliveries(int mgnr) { return FocusWindow(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr); } diff --git a/Elwig/Controls/CheckComboBox.cs b/Elwig/Controls/CheckComboBox.cs index 0d4c2d6..659940e 100644 --- a/Elwig/Controls/CheckComboBox.cs +++ b/Elwig/Controls/CheckComboBox.cs @@ -122,7 +122,7 @@ namespace Elwig.Controls { private void OnSelectionChanged(object sender, SelectionChangedEventArgs evt) { SelectItemsReverse(); - var dmp = !string.IsNullOrEmpty(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrEmpty(DisplayMemberPath) ? DisplayMemberPath : null; + var dmp = !string.IsNullOrWhiteSpace(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrWhiteSpace(DisplayMemberPath) ? DisplayMemberPath : null; if (SelectedItems.Count == ItemsSource.Cast().Count() && AllItemsSelectedContent != null) { AllItemsSelected = true; } else if (SelectedItems.Count == 0) { diff --git a/Elwig/Documents/MemberDataSheet.cs b/Elwig/Documents/MemberDataSheet.cs index 732377b..ee3d3f6 100644 --- a/Elwig/Documents/MemberDataSheet.cs +++ b/Elwig/Documents/MemberDataSheet.cs @@ -1,4 +1,5 @@ using Elwig.Helpers; +using Elwig.Models; using Elwig.Models.Entities; using iText.Kernel.Colors; using iText.Kernel.Pdf; @@ -40,7 +41,7 @@ namespace Elwig.Documents { Season = await ctx.FetchSeasons().FirstOrDefaultAsync() ?? throw new ArgumentException("Invalid season"); MemberHistory = await ctx.MemberHistory .Where(h => h.FromMgNr == Member.MgNr || h.ToMgNr == Member.MgNr) - .OrderBy(h => h.DateString).ThenBy(h => h.HistNr) + .OrderBy(h => h.DateNoticeString).ThenBy(h => h.HistNr) .ToListAsync(); MemberBuckets = await ctx.GetMemberBuckets(Utils.CurrentYear, Member.MgNr); ActiveAreaCommitments = await Member.ActiveAreaCommitments(ctx) @@ -245,15 +246,16 @@ namespace Elwig.Documents { } protected Table NewMemberBusinessSharesTable(IEnumerable history) { - var tbl = new Table(ColsMM(18, 111, 12, 12, 12), true) + var tbl = new Table(ColsMM(18, 93, App.Client.HasRedWhite ? 18 : 12, App.Client.HasRedWhite ? 12 : 18, 12, 12), true) .SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout() .SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE) .SetBorder(Border.NO_BORDER) .SetFontSize(10); var tw = App.Client.HasRedWhite ? 1 : 2; - tbl.AddHeaderCell(NewTh("Datum", rowspan: 2)) + tbl.AddHeaderCell(NewTh("Meldung am", rowspan: 2)) .AddHeaderCell(NewTh("Text", rowspan: 2, colspan: tw, left: true)) + .AddHeaderCell(NewTh("Wirksam ab", rowspan: 2)) .AddHeaderCell(NewTh("Geschäftsanteile", colspan: App.Client.HasRedWhite ? 3 : 2)); if (App.Client.HasRedWhite) { tbl.AddHeaderCell(NewTh("rot")) @@ -264,26 +266,29 @@ namespace Elwig.Documents { .AddHeaderCell(NewTh("ruhend")); } - if (!history.Any() || Member.EntryDate <= history.First().Date) { + if (!history.Any() || Member.EntryDate <= history.First().DateNotice) { tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) .AddCell(NewTd("Eintritt", colspan: tw, bold: true)) + .AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) .AddCells(NewBusinessSharesCells(new())); } var recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => new( - s.Shares + (h.ToMgNr == Member.MgNr && h.ToType == 1 ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == 1 ? h.Shares : 0), - s.SharesRed + (h.ToMgNr == Member.MgNr && h.ToType == 2 ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == 2 ? h.Shares : 0), - s.SharesWhite + (h.ToMgNr == Member.MgNr && h.ToType == 3 ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == 3 ? h.Shares : 0), - s.SharesDormant + (h.ToMgNr == Member.MgNr && h.ToType == 9 ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == 9 ? h.Shares : 0))); + s.Shares + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Normal ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Normal ? h.Shares : 0), + s.SharesRed + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Red ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Red ? h.Shares : 0), + s.SharesWhite + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.White ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.White ? h.Shares : 0), + s.SharesDormant + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Dormant ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Dormant ? h.Shares : 0))); var cur = new MemberHistoryPoint(Member.Shares - recorded.Shares, Member.SharesRed - recorded.SharesRed, Member.SharesWhite - recorded.SharesWhite, Member.SharesDormant - recorded.SharesDormant); if (cur.Shares != 0 || cur.SharesRed != 0 || cur.SharesWhite != 0 || cur.SharesDormant != 0) { tbl.AddCell(NewTd()) - .AddCell(NewTd("Nicht erfasst", colspan: tw, italic: true)) + .AddCell(NewTd("Nicht in Software erfasst", colspan: tw, italic: true)) + .AddCell(NewTd()) .AddCells(NewBusinessSharesCells(cur)); if (history.Any()) { tbl.AddCell(NewTd(borderTop: true)) - .AddCell(NewTd($"Bis Lese {history.First().Date.Year - 1}", borderTop: true, colspan: tw, bold: true)) + .AddCell(NewTd($"Bis Lese {history.First().DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true)) + .AddCell(NewTd(borderTop: true)) .AddCells(NewBusinessSharesCells(cur, isDiff: false)); } } @@ -293,62 +298,65 @@ namespace Elwig.Documents { int s = 0, r = 0, w = 0, d = 0; if (row.FromMgNr == Member.MgNr) { switch (row.FromType) { - case 1: s -= row.Shares; break; - case 2: r -= row.Shares; break; - case 3: w -= row.Shares; break; - case 9: d -= row.Shares; break; + case BusinessShareType.Normal: s -= row.Shares; break; + case BusinessShareType.Red: r -= row.Shares; break; + case BusinessShareType.White: w -= row.Shares; break; + case BusinessShareType.Dormant: d -= row.Shares; break; } } if (row.ToMgNr == Member.MgNr) { switch (row.ToType) { - case 1: s += row.Shares; break; - case 2: r += row.Shares; break; - case 3: w += row.Shares; break; - case 9: d += row.Shares; break; + case BusinessShareType.Normal: s += row.Shares; break; + case BusinessShareType.Red: r += row.Shares; break; + case BusinessShareType.White: w += row.Shares; break; + case BusinessShareType.Dormant: d += row.Shares; break; } } - if (lastDate < Member.EntryDate && row.Date <= Member.EntryDate) { + + if (lastDate < Member.EntryDate && row.DateNotice <= Member.EntryDate) { tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) .AddCell(NewTd("Eintritt", colspan: tw, bold: true)) + .AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) .AddCells(NewBusinessSharesCells(new())); } - if (lastDate != null && row.Date.Year != lastDate?.Year) { + if (lastDate != null && row.DateNotice.Year != lastDate?.Year) { tbl.AddCell(NewTd(borderTop: true)) - .AddCell(NewTd(lastDate?.Year == row.Date.Year - 1 ? $"Lese {lastDate?.Year}" : $"Lese {lastDate?.Year}–{row.Date.Year - 1}", borderTop: true, colspan: tw, bold: true)) + .AddCell(NewTd(lastDate?.Year == row.DateNotice.Year - 1 ? $"Lese {lastDate?.Year}" : $"Lese {lastDate?.Year}–{row.DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true)) + .AddCell(NewTd(borderTop: true)) .AddCells(NewBusinessSharesCells(cur, isDiff: false)); } - if (lastDate < Member.ExitDate && row.Date <= Member.ExitDate) { + if (lastDate < Member.ExitDate && row.DateNotice <= Member.ExitDate) { tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}")) .AddCell(NewTd("Austritt", colspan: tw, bold: true)) + .AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME .AddCells(NewBusinessSharesCells(new())); } cur = new(cur.Shares + s, cur.SharesRed + r, cur.SharesWhite + w, cur.SharesDormant + d); - var reason = row.Reason; - if (reason == "auto") { - reason = "Automatische Nachzeichnung"; - } else if (reason == "transfer") { - if (row.ToMgNr == Member.MgNr) { - reason = $"Übertragen von {row.FromMember.AdministrativeName} (MgNr. {row.FromMgNr})"; - } else { - reason = $"Übertragen an {row.ToMember.AdministrativeName} (MgNr. {row.ToMgNr})"; - } - } - - tbl.AddCell(NewTd($"{row.Date:dd.MM.yyyy}")) - .AddCell(NewTd(reason, colspan: tw)) + tbl.AddCell(NewTd($"{row.DateNotice:dd.MM.yyyy}")) + .AddCell(NewTd(row.FormatReasonPhrase(Member.MgNr), colspan: tw)) + .AddCell(NewTd($"{row.DateEffective:dd.MM.yyyy}")) .AddCells(NewBusinessSharesCells(new(s, r, w, d))); - lastDate = row.Date; + lastDate = row.DateNotice; } - if (Member.ExitDate != null && (!history.Any() || Member.ExitDate > history.Last().Date)) { + if (Member.EntryDate != null && history.Any() && Member.EntryDate > lastDate) { + tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) + .AddCell(NewTd("Eintritt", colspan: tw, bold: true)) + .AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}")) + .AddCells(NewBusinessSharesCells(new())); + } + + if (Member.ExitDate != null && (!history.Any() || Member.ExitDate > lastDate)) { tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}")) .AddCell(NewTd("Austritt", colspan: tw, bold: true)) + .AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME .AddCells(NewBusinessSharesCells(new())); } tbl.AddCell(NewTd(borderTop: true)) .AddCell(NewTd(lastDate == null ? "Aktuell" : $"Ab Lese {lastDate?.Year}", borderTop: true, colspan: tw, bold: true)) + .AddCell(NewTd(borderTop: true)) .AddCells(NewBusinessSharesCells(cur, isDiff: false)); return tbl; diff --git a/Elwig/Documents/MemberList.cs b/Elwig/Documents/MemberList.cs index ce92382..d20288e 100644 --- a/Elwig/Documents/MemberList.cs +++ b/Elwig/Documents/MemberList.cs @@ -86,7 +86,7 @@ namespace Elwig.Documents { .AddCell(NewTd($"{m.Plz}", 8)) .AddCell(NewTd(m.Locality, 6)) .AddCell(NewTd(m.LfbisNr ?? "", 8)) - .AddCell(NewTd($"{m.SharesTotal:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0)) + .AddCell(NewTd($"{m.SharesActive:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0)) .AddCell(NewTd(m.DefaultKg ?? "", 6)); if (AreaComFilters.Length > 0) { foreach (var v in AreaComFilters) { diff --git a/Elwig/Helpers/AppDbContext.cs b/Elwig/Helpers/AppDbContext.cs index 228032d..4c76fa9 100644 --- a/Elwig/Helpers/AppDbContext.cs +++ b/Elwig/Helpers/AppDbContext.cs @@ -315,6 +315,10 @@ namespace Elwig.Helpers { return (await DeliverySchedules.Where(s => s.Year == year).Select(v => (int?)v.DsNr).MaxAsync() ?? 0) + 1; } + public async Task NextHistNr() { + return (await MemberHistory.Select(c => (int?)c.HistNr).MaxAsync() ?? 0) + 1; + } + public IAsyncEnumerable FetchBranches(string? zwstid = null, bool includeWithoutMembers = true) { return _compiledQueryBranches.Invoke(this, zwstid, includeWithoutMembers); } diff --git a/Elwig/Helpers/Billing/Billing.cs b/Elwig/Helpers/Billing/Billing.cs index d65ad61..82cc891 100644 --- a/Elwig/Helpers/Billing/Billing.cs +++ b/Elwig/Helpers/Billing/Billing.cs @@ -54,14 +54,14 @@ namespace Elwig.Helpers.Billing { if (addMinShares < 1) addMinShares = 1; using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch($""" - DELETE FROM member_history WHERE source = 'elwig' AND reason = 'auto' AND deduct_year = {Year}; - INSERT INTO member_history (histnr, from_mgnr, from_type, to_mgnr, to_type, date, reason, source, deduct_year, shares, value_per_share, currency) + DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year}; + INSERT INTO member_history (histnr, from_mgnr, from_type, to_mgnr, to_type, date_notice, date_effective, reason, source, deduct_year, shares, value_per_share, currency, comment) SELECT COALESCE((SELECT MAX(histnr) AS histnr FROM member_history), 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr), - NULL, NULL, u.mgnr, 1, '{date:yyyy-MM-dd}', 'auto', 'elwig', s.year, + NULL, NULL, u.mgnr, 1, '{date:yyyy-MM-dd}', '{date:yyyy-MM-dd}', '{MemberHistory.REASON_PRESCRIPTION}', 'elwig', s.year, CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerShare}.0 * u.shares) / s.max_kg_per_share - {allowanceShares.ToString(CultureInfo.InvariantCulture)} - {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.shares) AS adjust_shares, - s.share_value / POW(10, s.precision - 2), s.currency + s.share_value / POW(10, s.precision - 2), s.currency, 'durch automatische Nachzeichnung' FROM v_total_under_delivery u JOIN season s ON s.year = u.year JOIN member m ON m.mgnr = u.mgnr @@ -72,7 +72,7 @@ namespace Elwig.Helpers.Billing { public async Task UnAdjustBusinessShares() { using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch($""" - DELETE FROM member_history WHERE source = 'elwig' AND reason = 'auto' AND deduct_year = {Year}; + DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year}; """); } diff --git a/Elwig/Helpers/Export/ElwigData.cs b/Elwig/Helpers/Export/ElwigData.cs index 925ece4..f8ab3d8 100644 --- a/Elwig/Helpers/Export/ElwigData.cs +++ b/Elwig/Helpers/Export/ElwigData.cs @@ -1,3 +1,4 @@ +using Elwig.Models; using Elwig.Models.Entities; using Elwig.Services; using Microsoft.EntityFrameworkCore; @@ -9,6 +10,7 @@ using System.IO.Compression; using System.Linq; using System.Text.Json.Nodes; using System.Threading.Tasks; +using System.Windows; namespace Elwig.Helpers.Export { public static class ElwigData { @@ -239,6 +241,8 @@ namespace Elwig.Helpers.Export { var device = meta.Device; using var ctx = new AppDbContext(); + var tx = await ctx.Database.BeginTransactionAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); var kgnrs = wbKgs.Select(k => k.KgNr).ToList(); var duplicateKgNrs = await ctx.WbKgs @@ -402,9 +406,9 @@ namespace Elwig.Helpers.Export { importedDeliveries.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, deliveries.Count - n - o, meta.DeliveryFilters)); } - await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); await ctx.SaveChangesAsync(); await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); + await tx.CommitAsync(); var primaryKeys = new Dictionary() { ["member"] = "mgnr, 0, 0", @@ -748,15 +752,18 @@ namespace Elwig.Helpers.Export { return new JsonObject { ["histnr"] = h.HistNr, ["from_mgnr"] = h.FromMgNr, - ["from_type"] = h.FromType, + ["from_type"] = h.FromType?.ToString().ToLower(), ["to_mgnr"] = h.ToMgNr, - ["to_type"] = h.ToType, - ["date"] = h.DateString, + ["to_type"] = h.ToType?.ToString().ToLower(), + ["date_notice"] = h.DateNoticeString, + ["date_effective"] = h.DateEffectiveString, ["reason"] = h.Reason, ["source"] = h.Source, ["shares"] = h.Shares, + ["signed"] = h.MemberHasSigned, ["value_per_share"] = h.ValuePerShare, ["currency"] = h.CurrencyCode, + ["deduct_year"] = h.DeductYear, ["comment"] = h.Comment, }; } @@ -765,15 +772,18 @@ namespace Elwig.Helpers.Export { return new MemberHistory { HistNr = json["histnr"]!.AsValue().GetValue(), FromMgNr = json["from_mgnr"]?.AsValue().GetValue(), - FromType = json["from_type"]?.AsValue().GetValue(), + FromType = Enum.TryParse(json["from_type"]?.AsValue().GetValue(), true, out var from) ? from : null, ToMgNr = json["to_mgnr"]?.AsValue().GetValue(), - ToType = json["to_type"]?.AsValue().GetValue(), - DateString = json["date"]!.AsValue().GetValue(), + ToType = Enum.TryParse(json["to_type"]?.AsValue().GetValue(), true, out var to) ? to : null, + DateNoticeString = json["date_notice"]!.AsValue().GetValue(), + DateEffectiveString = json["date_effective"]!.AsValue().GetValue(), Reason = json["reason"]!.AsValue().GetValue(), Source = json["source"]!.AsValue().GetValue(), Shares = json["shares"]!.AsValue().GetValue(), + MemberHasSigned = json["signed"]?.AsValue().GetValue() ?? false, ValuePerShare = json["value_per_share"]?.AsValue().GetValue(), CurrencyCode = json["currency"]?.AsValue().GetValue(), + DeductYear = json["deduct_year"]?.AsValue().GetValue(), Comment = json["comment"]?.AsValue().GetValue(), }; } diff --git a/Elwig/Helpers/Validator.cs b/Elwig/Helpers/Validator.cs index 72a16b5..33084de 100644 --- a/Elwig/Helpers/Validator.cs +++ b/Elwig/Helpers/Validator.cs @@ -136,8 +136,7 @@ namespace Elwig.Helpers { return new(false, "PLZ zu kurz"); } using var ctx = new AppDbContext(); - int plz = int.Parse(input.Text); - if (ctx.Postleitzahlen.Find(plz) == null) { + if (!int.TryParse(input.Text, out var plz) || ctx.Postleitzahlen.Find(plz) == null) { return new(false, "Ungültige PLZ"); } return new(true, null); @@ -423,8 +422,7 @@ namespace Elwig.Helpers { } using var ctx = new AppDbContext(); - int nr = int.Parse(input.Text); - if (!ctx.MgNrExists(nr).GetAwaiter().GetResult()) { + if (!int.TryParse(input.Text, out var nr) || !ctx.MgNrExists(nr).GetAwaiter().GetResult()) { return new(false, "Ungültige Mitgliedsnummer"); } @@ -440,8 +438,7 @@ namespace Elwig.Helpers { } using var ctx = new AppDbContext(); - int nr = int.Parse(input.Text); - if (nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) { + if (!int.TryParse(input.Text, out var nr) || nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) { return new(false, "Mitgliedsnummer wird bereits verwendet"); } @@ -476,7 +473,7 @@ namespace Elwig.Helpers { return res; } else if (!required && input.Text.Length == 0) { return new(true, null); - } else if (!ctx.MgNrExists(int.Parse(input.Text)).GetAwaiter().GetResult()) { + } else if (!int.TryParse(input.Text, out var nr) ||!ctx.MgNrExists(nr).GetAwaiter().GetResult()) { return new(false, "Ein Mitglied mit dieser Mitgliedsnummer existiert nicht"); } @@ -537,7 +534,7 @@ namespace Elwig.Helpers { return (parts[0]?.Length == 4) ? new(true, null) : new(false, "Datum ist ungültig"); } else if (p == 1) { // only month and year provided - int m = parts[0] != null && parts[0] != "" ? int.Parse(parts[0] ?? "0") : 0; + int m = parts[0] != null && parts[0] != "" && int.TryParse(parts[0], out var v) ? v : 0; if (parts[1]?.Length != 4 || parts[0]?.Length != 2 || m < 1 || m > 12) return new(false, "Datum ist ungültig"); return new(true, null); @@ -589,8 +586,7 @@ namespace Elwig.Helpers { } using var ctx = new AppDbContext(); - int nr = int.Parse(input.Text); - if (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult()) { + if (!int.TryParse(input.Text, out var nr) || (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult())) { return new(false, "Flächenbindungsnummer wird bereits verwendet"); } @@ -605,8 +601,7 @@ namespace Elwig.Helpers { return new(true, null); } - var oe = double.Parse(input.Text); - if (oe < 10 || oe >= 300) { + if (!double.TryParse(input.Text, out var oe) || oe < 10 || oe >= 300) { return new(false, "Ungültiger Oechsle-Wert"); } @@ -621,8 +616,7 @@ namespace Elwig.Helpers { return new(true, null); } - var kmw = double.Parse(input.Text); - if (kmw < 5 || kmw >= 50) { + if (!double.TryParse(input.Text, out var kmw) || kmw < 5 || kmw >= 50) { return new(false, "Ungültiger KMW-Wert"); } diff --git a/Elwig/Models/BusinessShareType.cs b/Elwig/Models/BusinessShareType.cs new file mode 100644 index 0000000..9f2f70d --- /dev/null +++ b/Elwig/Models/BusinessShareType.cs @@ -0,0 +1,5 @@ +namespace Elwig.Models { + public enum BusinessShareType { + Normal = 1, Red = 2, White = 3, Dormant = 9, + } +} diff --git a/Elwig/Models/Dtos/MemberListData.cs b/Elwig/Models/Dtos/MemberListData.cs index fa7b0cc..32b5496 100644 --- a/Elwig/Models/Dtos/MemberListData.cs +++ b/Elwig/Models/Dtos/MemberListData.cs @@ -19,7 +19,7 @@ namespace Elwig.Models.Dtos { ("Locality", "Ort", null, 60), ("DefaultKg", "Stammgemeinde", null, 60), ("Branch", "Zweigstelle", null, 40), - ("SharesTotal", "GA", null, 10), + ("SharesActive", "GA", null, 10), ("BillingName", "Rechnungsname", null, 60), ("BillingAddress", "Rechnungsadresse", null, 60), ("BillingPlz", "PLZ", null, 10), @@ -69,8 +69,7 @@ namespace Elwig.Models.Dtos { public int Shares; public int SharesRed; public int SharesWhite; - public int SharesDormant; - public int SharesTotal; + public int SharesActive; public string Address; public int Plz; public string Locality; @@ -105,8 +104,7 @@ namespace Elwig.Models.Dtos { Shares = m.Shares; SharesRed = m.SharesRed; SharesWhite = m.SharesWhite; - SharesDormant = m.SharesDormant; - SharesTotal = Shares + SharesRed + SharesWhite + SharesDormant; + SharesActive = Shares + SharesRed + SharesWhite; Address = m.Address; Plz = m.PostalDest.AtPlz!.Plz; Locality = m.PostalDest.AtPlz!.Ort.Name; diff --git a/Elwig/Models/Entities/Member.cs b/Elwig/Models/Entities/Member.cs index 6a9d796..2294d38 100644 --- a/Elwig/Models/Entities/Member.cs +++ b/Elwig/Models/Entities/Member.cs @@ -42,6 +42,8 @@ namespace Elwig.Models.Entities { [NotMapped] public string ShortName => (!string.IsNullOrWhiteSpace(GivenName) ? $"{GivenName} " : "") + Name; [NotMapped] + public string FullAdministrativeName => $"{AdministrativeName} ({MgNr})"; + [NotMapped] public string AdministrativeName => AdministrativeName1 + (!string.IsNullOrWhiteSpace(AdministrativeName2) ? $" {AdministrativeName2}" : ""); [NotMapped] public string AdministrativeName1 => IsJuridicalPerson ? Name : Name.Replace('ß', 'ẞ').ToUpper(); @@ -79,8 +81,6 @@ namespace Elwig.Models.Entities { [Column("shares_dormant")] public int SharesDormant { get; set; } - [NotMapped] - public int SharesTotal => Shares + SharesRed + SharesWhite + SharesDormant; [NotMapped] public int SharesActive => Shares + SharesRed + SharesWhite; diff --git a/Elwig/Models/Entities/MemberHistory.cs b/Elwig/Models/Entities/MemberHistory.cs index 2e10dae..f5a5041 100644 --- a/Elwig/Models/Entities/MemberHistory.cs +++ b/Elwig/Models/Entities/MemberHistory.cs @@ -6,29 +6,69 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Elwig.Models.Entities { [Table("member_history"), PrimaryKey("HistNr")] public class MemberHistory { + + public const string REASON_PURCHASE = "purchase"; // Kauf + public const string REASON_CONVERT = "convert"; // Änderung + public const string REASON_TRANSFER = "transfer"; // Übertragung + public const string REASON_PRESCRIPTION = "prescription"; // Vorschreibung + public const string REASON_EXCLUSION = "exclusion"; // Ausschluss + public const string REASON_DECEASED = "deceased"; // Tod + public const string REASON_RETIREMENT = "retirement"; // Kündigung/stilllegen + public const string REASON_PAYOUT = "payout"; // Auszahlung + [Column("histnr")] public int HistNr { get; set; } [Column("from_mgnr")] public int? FromMgNr { get; set; } [Column("from_type")] - public int? FromType { get; set; } + public int? FromTypeValue { get; set; } + [NotMapped] + public BusinessShareType? FromType { + get => (BusinessShareType?)FromTypeValue; + set => FromTypeValue = (int?)value; + } [Column("to_mgnr")] public int? ToMgNr { get; set; } [Column("to_type")] - public int? ToType { get; set; } - - [Column("date")] - public required string DateString { get; set; } + public int? ToTypeValue { get; set; } [NotMapped] - public DateOnly Date { - get => DateOnly.ParseExact(DateString, "yyyy-MM-dd"); + public BusinessShareType? ToType { + get => (BusinessShareType?)ToTypeValue; + set => ToTypeValue = (int?)value; + } + + [Column("date_notice")] + public required string DateNoticeString { get; set; } + [NotMapped] + public DateOnly DateNotice { + get => DateOnly.ParseExact(DateNoticeString, "yyyy-MM-dd"); + set => value.ToString("yyyy-MM-dd"); + } + + [Column("date_effective")] + public required string DateEffectiveString { get; set; } + [NotMapped] + public DateOnly DateEffective { + get => DateOnly.ParseExact(DateEffectiveString, "yyyy-MM-dd"); set => value.ToString("yyyy-MM-dd"); } [Column("reason")] public required string Reason { get; set; } + [NotMapped] + public string ReasonPretty => Reason switch { + REASON_PURCHASE => "Kauf", + REASON_CONVERT => "Änderung", + REASON_TRANSFER => "Übertragung", + REASON_PRESCRIPTION => "Vorschreibung", + REASON_EXCLUSION => "Ausschluss", + REASON_DECEASED => "Tod", + REASON_RETIREMENT => "Kündigung", + REASON_PAYOUT => "Auszahlung", + _ => Reason, + }; [Column("source")] public required string Source { get; set; } @@ -36,6 +76,9 @@ namespace Elwig.Models.Entities { [Column("shares")] public int Shares { get; set; } + [Column("signed")] + public bool MemberHasSigned { get; set; } + [Column("value_per_share")] public long? ValuePerShareValue { get; set; } [NotMapped] @@ -56,11 +99,27 @@ namespace Elwig.Models.Entities { [Column("comment")] public string? Comment { get; set; } + public string FormatReasonPhrase(int mgnr) { + if (Reason == REASON_TRANSFER) { + if (FromMgNr == mgnr) { + return $"{ReasonPretty} an {ToMember?.FullAdministrativeName ?? "Nachfolger"}" + (Comment != null ? $", {Comment}" : ""); + } else if (ToMgNr == mgnr) { + return $"{ReasonPretty} von {FromMember?.FullAdministrativeName ?? "Vorgänger"}" + (Comment != null ? $", {Comment}" : ""); + } + } + return ReasonPretty + (Comment != null ? $", {Comment}" : ""); + } + + [NotMapped] + public int? PoVMgNr { get; set; } + [NotMapped] + public string PoVReasonPhrase => FormatReasonPhrase(PoVMgNr ?? 0); + [ForeignKey("FromMgNr")] - public virtual Member FromMember { get; private set; } = null!; + public virtual Member? FromMember { get; private set; } = null!; [ForeignKey("ToMgNr")] - public virtual Member ToMember { get; private set; } = null!; + public virtual Member? ToMember { get; private set; } = null!; [ForeignKey("CurrencyCode")] public virtual Currency Currency { get; private set; } = null!; diff --git a/Elwig/Resources/Sql/39-40.sql b/Elwig/Resources/Sql/39-40.sql index 244270d..7fca09b 100644 --- a/Elwig/Resources/Sql/39-40.sql +++ b/Elwig/Resources/Sql/39-40.sql @@ -3,9 +3,12 @@ PRAGMA writable_schema = ON; ALTER TABLE member RENAME COLUMN business_shares TO shares; -ALTER TABLE member ADD COLUMN shares_red INTEGER NOT NULL DEFAULT 0; -ALTER TABLE member ADD COLUMN shares_white INTEGER NOT NULL DEFAULT 0; -ALTER TABLE member ADD COLUMN shares_dormant INTEGER NOT NULL DEFAULT 0; +ALTER TABLE member ADD COLUMN shares_red INTEGER NOT NULL DEFAULT 0 CHECK (shares_red >= 0); +ALTER TABLE member ADD COLUMN shares_white INTEGER NOT NULL DEFAULT 0 CHECK (shares_white >= 0); +ALTER TABLE member ADD COLUMN shares_dormant INTEGER NOT NULL DEFAULT 0 CHECK (shares_dormant >= 0); + +UPDATE sqlite_schema SET sql = REPLACE(sql, 'shares INTEGER NOT NULL DEFAULT 0', 'shares INTEGER NOT NULL DEFAULT 0 CHECK (shares >= 0)') +WHERE type = 'table' AND name = 'member'; UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS'; UPDATE member @@ -20,11 +23,13 @@ CREATE TABLE member_history_new ( from_type INTEGER, to_mgnr INTEGER, to_type INTEGER, - date TEXT NOT NULL CHECK (date REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$') DEFAULT CURRENT_DATE, - reason TEXT NOT NULL CHECK (reason REGEXP '^[a-z_]+$'), - source TEXT NOT NULL CHECK (source REGEXP '^[a-z_]+$'), + date_notice TEXT NOT NULL CHECK (date_notice REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$') DEFAULT CURRENT_DATE, + date_effective TEXT NOT NULL CHECK (date_effective REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$') DEFAULT CURRENT_DATE, + reason TEXT NOT NULL CHECK (reason REGEXP '^[a-z_]+$'), + source TEXT NOT NULL CHECK (source REGEXP '^[a-z_]+$'), - shares INTEGER NOT NULL, + shares INTEGER NOT NULL CHECK (shares > 0), + signed INTEGER NOT NULL CHECK (signed IN (TRUE, FALSE)) DEFAULT FALSE, value_per_share INTEGER DEFAULT NULL, currency TEXT DEFAULT NULL, deduct_year INTEGER DEFAULT NULL, @@ -45,8 +50,10 @@ CREATE TABLE member_history_new ( ((to_mgnr IS NULL AND to_type IS NULL) OR (to_mgnr IS NOT NULL AND to_type IS NOT NULL))) ) STRICT; -INSERT INTO member_history_new (histnr, from_mgnr, from_type, to_mgnr, to_type, date, reason, source, shares, value_per_share, currency, deduct_year, comment) -SELECT ROW_NUMBER() OVER(ORDER BY h.date, h.mgnr), NULL, NULL, h.mgnr, 1, h.date, h.type, 'elwig', h.business_shares, s.bs_value / POW(10, s.precision - 2), s.currency, IIF(h.type = 'auto', s.year, NULL), comment +INSERT INTO member_history_new (histnr, from_mgnr, from_type, to_mgnr, to_type, date_notice, date_effective, reason, source, shares, value_per_share, currency, deduct_year, comment) +SELECT ROW_NUMBER() OVER(ORDER BY h.date, h.mgnr), NULL, NULL, h.mgnr, 1, h.date, h.date, + IIF(h.type = 'auto', 'prescription', h.type), 'elwig', h.business_shares, s.bs_value / POW(10, s.precision - 2), s.currency, + IIF(h.type = 'auto', s.year, NULL), 'automatische Nachzeichnung' FROM member_history h JOIN season s ON s.year = SUBSTR(h.date, 1, 4); @@ -70,7 +77,7 @@ BEGIN END; CREATE TRIGGER t_member_history_u_member - AFTER UPDATE ON member_history FOR EACH ROW + AFTER UPDATE OF from_mgnr, from_type, to_mgnr, to_type, shares ON member_history FOR EACH ROW WHEN (SELECT value FROM client_parameter WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS') = 1 BEGIN UPDATE member SET shares = shares + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 1; @@ -247,8 +254,8 @@ SELECT m.mgnr, s.year, m.shares_white + SUM(IIF(h1.from_type = 3, h1.shares, 0)) - SUM(IIF(h2.to_type = 3, h2.shares, 0)) AS shares_white, m.shares_dormant + SUM(IIF(h1.from_type = 9, h1.shares, 0)) - SUM(IIF(h2.to_type = 9, h2.shares, 0)) AS shares_dormant FROM member m, v_virtual_season s - LEFT JOIN member_history h1 ON (SUBSTR(h1.date, 1, 4) + 0) > s.year AND h1.from_mgnr = m.mgnr - LEFT JOIN member_history h2 ON (SUBSTR(h2.date, 1, 4) + 0) > s.year AND h2.to_mgnr = m.mgnr + LEFT JOIN member_history h1 ON (SUBSTR(h1.date_notice, 1, 4) + 0) > s.year AND h1.from_mgnr = m.mgnr + LEFT JOIN member_history h2 ON (SUBSTR(h2.date_notice, 1, 4) + 0) > s.year AND h2.to_mgnr = m.mgnr GROUP BY m.mgnr, s.year ORDER BY m.mgnr, s.year; @@ -307,12 +314,12 @@ ORDER BY u.year, u.mgnr; DROP VIEW v_auto_business_shares; CREATE VIEW v_auto_business_shares AS -SELECT (SUBSTR(h.date, 1, 4) + 0) AS year, +SELECT (SUBSTR(h.date_notice, 1, 4) + 0) AS year, h.to_mgnr AS mgnr, SUM(h.shares) AS shares, SUM(h.shares * COALESCE(h.value_per_share, 0)) AS total_amount FROM member_history h -WHERE h.reason = 'auto' AND h.source = 'elwig' +WHERE h.reason = 'prescription' AND h.source = 'elwig' GROUP BY year, h.to_mgnr ORDER BY year, h.to_mgnr; diff --git a/Elwig/Services/AreaComService.cs b/Elwig/Services/AreaComService.cs index 8e00d91..94ede78 100644 --- a/Elwig/Services/AreaComService.cs +++ b/Elwig/Services/AreaComService.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Controls; -using System.Windows; namespace Elwig.Services { public static class AreaComService { @@ -115,7 +114,7 @@ namespace Elwig.Services { using var ctx = new AppDbContext(); var c = new AreaComContract { FbNr = oldFbNr ?? newFbNr, - Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment, + Comment = string.IsNullOrWhiteSpace(vm.Comment) ? null : vm.Comment, KgNr = vm.Kg!.KgNr, RdNr = vm.Rd?.RdNr, }; diff --git a/Elwig/Services/DeliveryAncmtService.cs b/Elwig/Services/DeliveryAncmtService.cs index c5d739e..f7577cd 100644 --- a/Elwig/Services/DeliveryAncmtService.cs +++ b/Elwig/Services/DeliveryAncmtService.cs @@ -8,7 +8,6 @@ using Microsoft.EntityFrameworkCore; using Elwig.Documents; using Elwig.Helpers.Export; using Elwig.Models.Dtos; -using System.Windows; using System; using LinqKit; using System.Windows.Controls; diff --git a/Elwig/Services/DeliveryService.cs b/Elwig/Services/DeliveryService.cs index d635e38..9d20349 100644 --- a/Elwig/Services/DeliveryService.cs +++ b/Elwig/Services/DeliveryService.cs @@ -6,7 +6,6 @@ using Elwig.Models.Entities; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Windows; using System; using Elwig.ViewModels; using LinqKit; @@ -23,15 +22,6 @@ namespace Elwig.Services { FromFilters, FromToday, FromSeason, FromSeasonAndBranch, Selected, }; - public static async Task GetMemberAsync(int mgnr) { - using var ctx = new AppDbContext(); - return await ctx.FetchMembers(mgnr).SingleOrDefaultAsync(); - } - - public static Member? GetMember(int mgnr) { - return GetMemberAsync(mgnr).GetAwaiter().GetResult(); - } - public static void ClearInputs(this DeliveryAdminViewModel vm) { } @@ -486,7 +476,7 @@ namespace Elwig.Services { if (partNew && timeIsDefault) { newTimeString = DateTime.Now.ToString("HH:mm:ss"); } else if (partNew || timeHasChanged) { - newTimeString = string.IsNullOrEmpty(vm.Time) ? null : vm.Time + ":00"; + newTimeString = string.IsNullOrWhiteSpace(vm.Time) ? null : vm.Time + ":00"; } var d = new Delivery { @@ -498,7 +488,7 @@ namespace Elwig.Services { ZwstId = vm.Branch!.ZwstId, LsNr = newLsNr ?? vm.LsNr!, MgNr = vm.MgNr!.Value, - Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment, + Comment = string.IsNullOrWhiteSpace(vm.Comment) ? null : vm.Comment, }; p = new DeliveryPart { @@ -521,7 +511,7 @@ namespace Elwig.Services { Unloading = vm.Unloading, Temperature = vm.Temperature, Acid = vm.Acid, - Comment = string.IsNullOrEmpty(vm.PartComment) ? null : vm.PartComment, + Comment = string.IsNullOrWhiteSpace(vm.PartComment) ? null : vm.PartComment, Weight = vm.Weight!.Value, IsManualWeighing = vm.IsManualWeighing, diff --git a/Elwig/Services/InteractionService.cs b/Elwig/Services/InteractionService.cs index 34d9b11..83acbf2 100644 --- a/Elwig/Services/InteractionService.cs +++ b/Elwig/Services/InteractionService.cs @@ -48,6 +48,19 @@ namespace Elwig.Services { } } + public static bool? AskContinueYesNo(string title, string text) { + if (NextContinue != null) { + var r = NextContinue(title, text); + NextContinue = null; + return r; + } else if (Override != null) { + return Override("continue", title, text) != null; + } else { + var r = MessageBox.Show(text, title, MessageBoxButton.YesNoCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel); + return r == MessageBoxResult.Yes ? true : r == MessageBoxResult.No ? false : null; + } + } + public static bool AskConfirmation(string title, string text) { if (NextConfirmation != null) { var r = NextConfirmation(title, text); diff --git a/Elwig/Services/MemberBusinessSharesService.cs b/Elwig/Services/MemberBusinessSharesService.cs new file mode 100644 index 0000000..180f4c4 --- /dev/null +++ b/Elwig/Services/MemberBusinessSharesService.cs @@ -0,0 +1,98 @@ +using Elwig.Helpers; +using Elwig.Models; +using Elwig.Models.Entities; +using Elwig.ViewModels; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading.Tasks; + +namespace Elwig.Services { + public static class MemberBusinessSharesService { + + public static async Task InitInputs(this MemberBusinessSharesViewModel vm) { + using var ctx = new AppDbContext(); + vm.DateNoticeString = $"{Utils.Today:dd.MM.yyyy}"; + vm.ValuePerShare = (await ctx.Seasons.OrderBy(s => s.Year).LastOrDefaultAsync())?.BusinessShareValue; + } + + public static void ClearInputs(this MemberBusinessSharesViewModel vm) { + + } + + public static void FillInputs(this MemberBusinessSharesViewModel vm, MemberHistory h) { + var mgnr = vm.FilterMember.MgNr; + vm.DateNotice = h.DateNotice; + vm.DateEffective = h.DateEffective; + vm.Reason = h.Reason + (h.Reason == MemberHistory.REASON_TRANSFER ? $"_{(h.ToMgNr == mgnr ? "from" : "to")}" : ""); + vm.Shares = h.Shares; + vm.MemberHasSigned = h.MemberHasSigned; + vm.ValuePerShare = h.ValuePerShare; + vm.OtherMgNr = h.FromMgNr != null && h.FromMgNr != mgnr ? h.FromMgNr : h.ToMgNr != null && h.ToMgNr != mgnr ? h.ToMgNr : null; + vm.DeductYear = h.DeductYear ?? h.DateNotice.Year; + vm.Deduct = h.DeductYear != null; + vm.Comment = h.Comment; + } + + public static async Task UpdateMemberHistory(this MemberBusinessSharesViewModel vm, int? oldHistNr, bool enableTriggers) { + return await Task.Run(async () => { + using var ctx = new AppDbContext(); + var tx = await ctx.Database.BeginTransactionAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = {(enableTriggers ? "1" : "0")} WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); + + var h = new MemberHistory { + HistNr = oldHistNr ?? await ctx.NextHistNr(), + FromMgNr = + vm.Reason == $"{MemberHistory.REASON_TRANSFER}_from" ? vm.OtherMgNr : + vm.Reason == MemberHistory.REASON_PURCHASE || vm.Reason == MemberHistory.REASON_PRESCRIPTION ? null : + vm.FilterMember.MgNr, + FromType = + vm.Reason == MemberHistory.REASON_PAYOUT ? BusinessShareType.Dormant : + vm.Reason == MemberHistory.REASON_PURCHASE || vm.Reason == MemberHistory.REASON_PRESCRIPTION ? null : + BusinessShareType.Normal, + ToMgNr = + vm.Reason == MemberHistory.REASON_PAYOUT || vm.Reason == MemberHistory.REASON_EXCLUSION ? null : + vm.Reason == $"{MemberHistory.REASON_TRANSFER}_to" ? vm.OtherMgNr : + vm.FilterMember.MgNr, + ToType = + vm.Reason == MemberHistory.REASON_PAYOUT || vm.Reason == MemberHistory.REASON_EXCLUSION ? null : + vm.Reason == MemberHistory.REASON_RETIREMENT || vm.Reason == MemberHistory.REASON_DECEASED ? BusinessShareType.Dormant : + BusinessShareType.Normal, + DateNoticeString = $"{vm.DateNotice:yyyy-MM-dd}", + DateEffectiveString = $"{vm.DateEffective:yyyy-MM-dd}", + Reason = vm.Reason?.Split("_")[0] ?? MemberHistory.REASON_CONVERT, + Source = "manual", + Shares = vm.Shares ?? 0, + MemberHasSigned = vm.MemberHasSigned, + ValuePerShare = vm.ValuePerShare, + CurrencyCode = vm.ValuePerShare != null ? "EUR" : null, + DeductYear = vm.Deduct ? vm.DeductYear : null, + Comment = string.IsNullOrWhiteSpace(vm.Comment) ? null : vm.Comment.Trim(), + }; + + if (oldHistNr != null) { + ctx.Update(h); + } else { + ctx.Add(h); + } + + await ctx.SaveChangesAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); + await tx.CommitAsync(); + + return h.HistNr; + }); + } + + + public static async Task DeleteMemberHistory(int histnr, bool enableTriggers) { + await Task.Run(async () => { + using var ctx = new AppDbContext(); + var tx = await ctx.Database.BeginTransactionAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = {(enableTriggers ? "1" : "0")} WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); + await ctx.MemberHistory.Where(h => h.HistNr == histnr).ExecuteDeleteAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); + await tx.CommitAsync(); + }); + } + } +} diff --git a/Elwig/Services/MemberService.cs b/Elwig/Services/MemberService.cs index 4275151..9fbd9ec 100644 --- a/Elwig/Services/MemberService.cs +++ b/Elwig/Services/MemberService.cs @@ -501,7 +501,10 @@ namespace Elwig.Services { var history2 = await query.IgnoreAutoIncludes() .SelectMany(m => m.HistoryTo) .ToListAsync(); - var history = history1.Union(history2).DistinctBy(h => h.HistNr).OrderBy(h => h.HistNr).ToList(); + var history = history1.Union(history2) + .DistinctBy(h => h.HistNr) + .OrderBy(h => h.DateNotice).ThenBy(h => h.HistNr) + .ToList(); var areaComs = await query .SelectMany(m => m.AreaCommitments) .Select(c => c.Contract).Distinct() @@ -541,33 +544,33 @@ namespace Elwig.Services { Name = vm.Name!, Suffix = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.Suffix) ? null : vm.Suffix, ForTheAttentionOf = !vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.ForTheAttentionOf) ? null : vm.ForTheAttentionOf, - Birthday = string.IsNullOrEmpty(vm.Birthday) ? null : string.Join("-", vm.Birthday!.Split(".").Reverse()), + Birthday = string.IsNullOrWhiteSpace(vm.Birthday) ? null : string.Join("-", vm.Birthday!.Split(".").Reverse()), IsDeceased = vm.IsDeceased, CountryNum = 40, // Austria AT AUT PostalDestId = vm.Ort!.Id, Address = vm.Address!, - Iban = string.IsNullOrEmpty(vm.Iban) ? null : vm.Iban?.Replace(" ", ""), - Bic = string.IsNullOrEmpty(vm.Bic) ? null : vm.Bic, + Iban = string.IsNullOrWhiteSpace(vm.Iban) ? null : vm.Iban?.Replace(" ", ""), + Bic = string.IsNullOrWhiteSpace(vm.Bic) ? null : vm.Bic, - UstIdNr = string.IsNullOrEmpty(vm.UstIdNr) ? null : vm.UstIdNr, - LfbisNr = string.IsNullOrEmpty(vm.LfbisNr) ? null : vm.LfbisNr, + UstIdNr = string.IsNullOrWhiteSpace(vm.UstIdNr) ? null : vm.UstIdNr, + LfbisNr = string.IsNullOrWhiteSpace(vm.LfbisNr) ? null : vm.LfbisNr, IsBuchführend = vm.IsBuchführend, IsOrganic = vm.IsOrganic, - EntryDateString = string.IsNullOrEmpty(vm.EntryDate) ? null : string.Join("-", vm.EntryDate.Split(".").Reverse()), - ExitDateString = string.IsNullOrEmpty(vm.ExitDate) ? null : string.Join("-", vm.ExitDate.Split(".").Reverse()), + EntryDateString = string.IsNullOrWhiteSpace(vm.EntryDate) ? null : string.Join("-", vm.EntryDate.Split(".").Reverse()), + ExitDateString = string.IsNullOrWhiteSpace(vm.ExitDate) ? null : string.Join("-", vm.ExitDate.Split(".").Reverse()), Shares = App.Client.HasRedWhite ? 0 : vm.BusinessShares1 ?? 0, SharesRed = App.Client.HasRedWhite ? vm.BusinessShares1 ?? 0 : 0, SharesWhite = App.Client.HasRedWhite ? vm.BusinessShares2 ?? 0 : 0, SharesDormant = App.Client.HasRedWhite ? vm.BusinessShares3 ?? 0 : vm.BusinessShares2 ?? 0, - AccountingNr = string.IsNullOrEmpty(vm.AccountingNr) ? null : vm.AccountingNr, + AccountingNr = string.IsNullOrWhiteSpace(vm.AccountingNr) ? null : vm.AccountingNr, IsActive = vm.IsActive, IsVollLieferant = vm.IsVollLieferant, IsFunktionär = vm.IsFunktionär, ZwstId = vm.Branch?.ZwstId, DefaultKgNr = vm.DefaultKg?.KgNr, - Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment, + Comment = string.IsNullOrWhiteSpace(vm.Comment) ? null : vm.Comment, ContactViaPost = vm.ContactViaPost, ContactViaEmail = vm.ContactViaEmail, }; diff --git a/Elwig/Services/PaymentVariantService.cs b/Elwig/Services/PaymentVariantService.cs index 45d0307..92d6113 100644 --- a/Elwig/Services/PaymentVariantService.cs +++ b/Elwig/Services/PaymentVariantService.cs @@ -300,7 +300,7 @@ namespace Elwig.Services { TransferDate = vm.TransferDate, TestVariant = vm.SelectedPaymentVariant?.TestVariant ?? true, CalcTimeUnix = vm.SelectedPaymentVariant?.CalcTimeUnix, - Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment, + Comment = string.IsNullOrWhiteSpace(vm.Comment) ? null : vm.Comment, Data = JsonSerializer.Serialize(d.Data), }; avnr = v.AvNr; diff --git a/Elwig/Services/SyncService.cs b/Elwig/Services/SyncService.cs index 98d4471..10e8903 100644 --- a/Elwig/Services/SyncService.cs +++ b/Elwig/Services/SyncService.cs @@ -33,7 +33,10 @@ namespace Elwig.Services { var history2 = await query.IgnoreAutoIncludes() .SelectMany(m => m.HistoryTo) .ToListAsync(); - var history = history1.Union(history2).DistinctBy(h => h.HistNr).OrderBy(h => h.HistNr).ToList(); + var history = history1.Union(history2) + .DistinctBy(h => h.HistNr) + .OrderBy(h => h.DateNotice).ThenBy(h => h.HistNr) + .ToList(); var areaComs = await query .SelectMany(m => m.AreaCommitments) .Select(c => c.Contract).Distinct() diff --git a/Elwig/ViewModels/MemberBusinessSharesViewModel.cs b/Elwig/ViewModels/MemberBusinessSharesViewModel.cs new file mode 100644 index 0000000..f4146c5 --- /dev/null +++ b/Elwig/ViewModels/MemberBusinessSharesViewModel.cs @@ -0,0 +1,76 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Elwig.Models.Entities; +using System; +using System.Collections.Generic; +using System.Windows; + +namespace Elwig.ViewModels { + public partial class MemberBusinessSharesViewModel : ObservableObject { + + [ObservableProperty] + private string _title = "Geschäftsanteilbewegungen - Elwig"; + + public required Member FilterMember { get; set; } + + [ObservableProperty] + private MemberHistory? _selectedHistoryEntry; + [ObservableProperty] + private IEnumerable _history = []; + + [ObservableProperty] + private string? _dateNoticeString; + public DateOnly? DateNotice { + get => DateOnly.TryParseExact(DateNoticeString, "dd.MM.yyyy", out var d) ? d : null; + set => DateNoticeString = $"{value:dd.MM.yyyy}"; + } + [ObservableProperty] + private string? _dateEffectiveString; + public DateOnly? DateEffective { + get => DateOnly.TryParseExact(DateEffectiveString, "dd.MM.yyyy", out var d) ? d : null; + set => DateEffectiveString = $"{value:dd.MM.yyyy}"; + } + [ObservableProperty] + private string? _reason; + [ObservableProperty] + private string? _sharesString; + public int? Shares { + get => int.TryParse(SharesString, out var shares) ? shares : null; + set => SharesString = $"{value}"; + } + [ObservableProperty] + private string? _otherMgNrString; + public int? OtherMgNr { + get => int.TryParse(OtherMgNrString, out var mgnr) ? mgnr : null; + set => OtherMgNrString = $"{value}"; + } + [ObservableProperty] + private string? _deductYearString; + public int? DeductYear { + get => int.TryParse(DeductYearString, out var year) ? year : null; + set => DeductYearString = $"{value}"; + } + [ObservableProperty] + private bool _deduct; + [ObservableProperty] + private bool _memberHasSigned; + [ObservableProperty] + private string? _valuePerShareString; + public decimal? ValuePerShare { + get => decimal.TryParse(ValuePerShareString, out var v) ? v : null; + set => ValuePerShareString = $"{value:0.00}"; + } + [ObservableProperty] + private string? _totalAmount; + [ObservableProperty] + private string? _comment; + + [ObservableProperty] + private Visibility _controlButtonsVisibility = Visibility.Visible; + [ObservableProperty] + private Visibility _editingButtonsVisibility = Visibility.Hidden; + [ObservableProperty] + private Visibility _personalNameVisibility = Visibility.Visible; + [ObservableProperty] + private Visibility _juridicalNameVisibility = Visibility.Hidden; + } +} diff --git a/Elwig/Windows/AdministrationWindow.cs b/Elwig/Windows/AdministrationWindow.cs index 3f0e8aa..d1f6d4d 100644 --- a/Elwig/Windows/AdministrationWindow.cs +++ b/Elwig/Windows/AdministrationWindow.cs @@ -164,10 +164,10 @@ namespace Elwig.Windows { } else if (input is TextBox tb && tb.Text.Length == 0) { ControlUtils.SetInputInvalid(input); Valid[input] = false; - } else if (input is ComboBox cb && cb.SelectedItem == null && cb.ItemsSource != null && cb.ItemsSource.Cast().Any()) { + } else if (input is ComboBox cb && cb.SelectedItem == null && cb.HasItems) { ControlUtils.SetInputInvalid(input); Valid[input] = false; - } else if (input is ListBox lb && lb.SelectedItem == null && lb.ItemsSource != null && lb.ItemsSource.Cast().Any()) { + } else if (input is ListBox lb && lb.SelectedItem == null && lb.HasItems) { ControlUtils.SetInputInvalid(input); Valid[input] = false; } else if (input is CheckBox ckb && ((ckb.IsThreeState && ckb.IsChecked == null) || (!ckb.IsThreeState && ckb.IsChecked != true))) { @@ -285,8 +285,13 @@ namespace Elwig.Windows { var binding = cb.GetBindingExpression(ComboBox.SelectedItemProperty); binding?.UpdateSource(); } - foreach (var lb in ListBoxInputs) - lb.SelectedItems.Clear(); + foreach (var lb in ListBoxInputs) { + if (lb.SelectionMode == SelectionMode.Single) { + lb.SelectedItem = null; + } else { + lb.SelectedItems.Clear(); + } + } foreach (var cb in CheckBoxInputs) { cb.IsChecked = cb.IsThreeState ? null : false; var binding = cb.GetBindingExpression(CheckBox.IsCheckedProperty); diff --git a/Elwig/Windows/BaseDataWindow.xaml.cs b/Elwig/Windows/BaseDataWindow.xaml.cs index 0a3c61e..d39b316 100644 --- a/Elwig/Windows/BaseDataWindow.xaml.cs +++ b/Elwig/Windows/BaseDataWindow.xaml.cs @@ -361,29 +361,29 @@ namespace Elwig.Windows { private async Task UpdateClientParameters(ClientParameters p) { p.Name = ClientNameInput.Text; - p.NameSuffix = string.IsNullOrEmpty(ClientNameSuffixInput.Text) ? null : ClientNameSuffixInput.Text; + p.NameSuffix = string.IsNullOrWhiteSpace(ClientNameSuffixInput.Text) ? null : ClientNameSuffixInput.Text; p.NameType = ClientNameTypeInput.Text; p.NameToken = ClientNameTokenInput.Text; p.NameShort = ClientNameShortInput.Text; p.Address = ClientAddressInput.Text; p.Plz = int.Parse(ClientPlzInput.Text); p.Ort = ClientOrtInput.Text; - p.Iban = string.IsNullOrEmpty(ClientIbanInput.Text) ? null : ClientIbanInput.Text; - p.Bic = string.IsNullOrEmpty(ClientBicInput.Text) ? null : ClientBicInput.Text; - p.UstIdNr = string.IsNullOrEmpty(ClientUstIdNrInput.Text) ? null : ClientUstIdNrInput.Text; - p.LfbisNr = string.IsNullOrEmpty(ClientLfbisNrInput.Text) ? null : ClientLfbisNrInput.Text; - p.OrganicAuthority = string.IsNullOrEmpty(ClientOrganicAuthorityInput.Text) ? null : ClientOrganicAuthorityInput.Text; - p.PhoneNr = string.IsNullOrEmpty(ClientPhoneNrInput.Text) ? null : ClientPhoneNrInput.Text; - p.FaxNr = string.IsNullOrEmpty(ClientFaxNrInput.Text) ? null : ClientFaxNrInput.Text; - p.EmailAddress = string.IsNullOrEmpty(ClientEmailAddressInput.Text) ? null : ClientEmailAddressInput.Text; - p.Website = string.IsNullOrEmpty(ClientWebsiteInput.Text) ? null : ClientWebsiteInput.Text; + p.Iban = string.IsNullOrWhiteSpace(ClientIbanInput.Text) ? null : ClientIbanInput.Text; + p.Bic = string.IsNullOrWhiteSpace(ClientBicInput.Text) ? null : ClientBicInput.Text; + p.UstIdNr = string.IsNullOrWhiteSpace(ClientUstIdNrInput.Text) ? null : ClientUstIdNrInput.Text; + p.LfbisNr = string.IsNullOrWhiteSpace(ClientLfbisNrInput.Text) ? null : ClientLfbisNrInput.Text; + p.OrganicAuthority = string.IsNullOrWhiteSpace(ClientOrganicAuthorityInput.Text) ? null : ClientOrganicAuthorityInput.Text; + p.PhoneNr = string.IsNullOrWhiteSpace(ClientPhoneNrInput.Text) ? null : ClientPhoneNrInput.Text; + p.FaxNr = string.IsNullOrWhiteSpace(ClientFaxNrInput.Text) ? null : ClientFaxNrInput.Text; + p.EmailAddress = string.IsNullOrWhiteSpace(ClientEmailAddressInput.Text) ? null : ClientEmailAddressInput.Text; + p.Website = string.IsNullOrWhiteSpace(ClientWebsiteInput.Text) ? null : ClientWebsiteInput.Text; p.ModeBusinessShares = ParameterBusinessShareModeInput.SelectedIndex; p.EnableMemberHistory = ParameterBusinessShareHistoryInput.IsChecked ?? false; - p.TextDeliveryNote = string.IsNullOrEmpty(TextElementDeliveryNote.Text) ? null : TextElementDeliveryNote.Text; + p.TextDeliveryNote = string.IsNullOrWhiteSpace(TextElementDeliveryNote.Text) ? null : TextElementDeliveryNote.Text; p.ModeDeliveryNoteStats = (ModeDeliveryNoteNone.IsChecked == true) ? 0 : (ModeDeliveryNoteGaOnly.IsChecked == true) ? 1 : (ModeDeliveryNoteShort.IsChecked == true) ? 2 : (ModeDeliveryNoteFull.IsChecked == true) ? 3 : 2; - p.TextDeliveryConfirmation = string.IsNullOrEmpty(TextElementDeliveryConfirmation.Text) ? null : TextElementDeliveryConfirmation.Text; - p.TextCreditNote = string.IsNullOrEmpty(TextElementCreditNote.Text) ? null : TextElementCreditNote.Text; + p.TextDeliveryConfirmation = string.IsNullOrWhiteSpace(TextElementDeliveryConfirmation.Text) ? null : TextElementDeliveryConfirmation.Text; + p.TextCreditNote = string.IsNullOrWhiteSpace(TextElementCreditNote.Text) ? null : TextElementCreditNote.Text; p.ExportEbicsVersion = ParameterExportEbicsVersion.SelectedIndex + 3; p.ExportEbicsAddress = ParameterExportEbicsAddress.SelectedIndex; diff --git a/Elwig/Windows/DeliveryAdminWindow.xaml.cs b/Elwig/Windows/DeliveryAdminWindow.xaml.cs index 92f4194..a714b2d 100644 --- a/Elwig/Windows/DeliveryAdminWindow.xaml.cs +++ b/Elwig/Windows/DeliveryAdminWindow.xaml.cs @@ -124,7 +124,8 @@ namespace Elwig.Windows { public DeliveryAdminWindow(int mgnr) : this() { - ViewModel.FilterMember = DeliveryService.GetMember(mgnr) ?? throw new ArgumentException("MgNr argument has invalid value"); + using var ctx = new AppDbContext(); + ViewModel.FilterMember = ctx.Members.Find(mgnr) ?? throw new ArgumentException("MgNr argument has invalid value"); ViewModel.Title = $"Lieferungen - {ViewModel.FilterMember.AdministrativeName} - Elwig"; ViewModel.EnableAllSeasons = true; } @@ -482,7 +483,7 @@ namespace Elwig.Windows { await base.OnRenewContext(ctx); if (ViewModel.FilterMember != null) { - if (await DeliveryService.GetMemberAsync(ViewModel.FilterMember.MgNr) is not Member m) { + if (await ctx.FetchMembers(ViewModel.FilterMember.MgNr).SingleOrDefaultAsync() is not Member m) { Close(); return; } @@ -1200,7 +1201,7 @@ namespace Elwig.Windows { } private async Task UpdateLsNr() { - if (string.IsNullOrEmpty(ViewModel.Date) || ViewModel.Branch == null) { + if (string.IsNullOrWhiteSpace(ViewModel.Date) || ViewModel.Branch == null) { ViewModel.LsNr = ""; } else { try { diff --git a/Elwig/Windows/MemberAdminWindow.xaml b/Elwig/Windows/MemberAdminWindow.xaml index 2779083..840e985 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml +++ b/Elwig/Windows/MemberAdminWindow.xaml @@ -233,7 +233,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +