diff --git a/Elwig/Documents/MemberDataSheet.cs b/Elwig/Documents/MemberDataSheet.cs index 7c395db..ee85689 100644 --- a/Elwig/Documents/MemberDataSheet.cs +++ b/Elwig/Documents/MemberDataSheet.cs @@ -274,11 +274,11 @@ namespace Elwig.Documents { .AddCells(NewBusinessSharesCells(new())); } - var recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => new( - 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 recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => new( + s.Shares + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Normal ? h.Shares : 0) - (h.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Normal && h.FromType == BusinessShareType.Normal ? 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.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Red && h.FromType == BusinessShareType.Red ? 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.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.White && h.FromType == BusinessShareType.White ? 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.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Dormant && h.FromType == BusinessShareType.Dormant ? 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) { @@ -297,7 +297,7 @@ namespace Elwig.Documents { DateOnly? lastDate = null; foreach (var row in history) { int s = 0, r = 0, w = 0, d = 0; - if (row.FromMgNr == Member.MgNr) { + if (row.FromMgNr == Member.MgNr && !(row.FromMgNr == row.ToMgNr && row.FromType == row.ToType)) { switch (row.FromType) { case BusinessShareType.Normal: s -= row.Shares; break; case BusinessShareType.Red: r -= row.Shares; break; diff --git a/Elwig/Helpers/AppDbContext.cs b/Elwig/Helpers/AppDbContext.cs index 9a064e4..7f21fa6 100644 --- a/Elwig/Helpers/AppDbContext.cs +++ b/Elwig/Helpers/AppDbContext.cs @@ -1,3 +1,4 @@ +using Elwig.Models; using Elwig.Models.Dtos; using Elwig.Models.Entities; using Elwig.Services; @@ -531,6 +532,44 @@ namespace Elwig.Helpers { return _memberHistory[year].GetValueOrDefault(mgnr, new()); } + public async Task>> GetMemberHistory() { + var today = DateOnly.FromDateTime(DateTime.Today).AddDays(1); + var history = new Dictionary>(); + + var members = await Members.ToListAsync(); + foreach (var m in members) { + history[m.MgNr] = new() { + [today] = new(m.Shares, m.SharesRed, m.SharesWhite, m.SharesDormant), + }; + } + + var hist = await MemberHistory.OrderByDescending(h => h.DateNoticeString).ToListAsync(); + foreach (var h in hist) { + if (h.FromMgNr is int from) { + var latest = history[from].Last().Value; + switch (h.FromType) { + case BusinessShareType.Normal: latest.Shares += h.Shares; break; + case BusinessShareType.Red: latest.SharesRed += h.Shares; break; + case BusinessShareType.White: latest.SharesWhite += h.Shares; break; + case BusinessShareType.Dormant: latest.SharesDormant += h.Shares; break; + } + history[from][h.DateNotice] = latest; + } + if (h.ToMgNr is int to) { + var latest = history[to].Last().Value; + switch (h.ToType) { + case BusinessShareType.Normal: latest.Shares -= h.Shares; break; + case BusinessShareType.Red: latest.SharesRed -= h.Shares; break; + case BusinessShareType.White: latest.SharesWhite -= h.Shares; break; + case BusinessShareType.Dormant: latest.SharesDormant -= h.Shares; break; + } + history[to][h.DateNotice] = latest; + } + } + + return history; + } + public async Task> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) { if (!_memberAreaCommitmentBuckets.ContainsKey(year)) await FetchMemberAreaCommitmentBuckets(year, cnx); diff --git a/Elwig/Helpers/Export/Bki.cs b/Elwig/Helpers/Export/Bki.cs index 50d2d0a..d4fafe6 100644 --- a/Elwig/Helpers/Export/Bki.cs +++ b/Elwig/Helpers/Export/Bki.cs @@ -46,10 +46,6 @@ namespace Elwig.Helpers.Export { await ExportAsync(rows); } - public void Export(int year) { - ExportAsync(year).GetAwaiter().GetResult(); - } - public override string FormatRow(Row row) { var (member, delivery) = row; var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member; diff --git a/Elwig/Helpers/Export/CsvFile.cs b/Elwig/Helpers/Export/CsvFile.cs index 029c390..21422a2 100644 --- a/Elwig/Helpers/Export/CsvFile.cs +++ b/Elwig/Helpers/Export/CsvFile.cs @@ -29,11 +29,6 @@ namespace Elwig.Helpers.Export { _writer.Dispose(); } - public ValueTask DisposeAsync() { - GC.SuppressFinalize(this); - return _writer.DisposeAsync(); - } - public async Task ExportAsync(IEnumerable data, IProgress? progress = null) { progress?.Report(0.0); int count = data.Count() + 2, i = 0; @@ -50,10 +45,6 @@ namespace Elwig.Helpers.Export { progress?.Report(100.0); } - public void Export(IEnumerable data, IProgress? progress = null) { - ExportAsync(data, progress).GetAwaiter().GetResult(); - } - public string FormatRow(IEnumerable row) { return string.Join(Separator, row); } diff --git a/Elwig/Helpers/Export/Ebics.cs b/Elwig/Helpers/Export/Ebics.cs index 4d80549..e17ab6a 100644 --- a/Elwig/Helpers/Export/Ebics.cs +++ b/Elwig/Helpers/Export/Ebics.cs @@ -28,15 +28,6 @@ namespace Elwig.Helpers.Export { Writer.Dispose(); } - public ValueTask DisposeAsync() { - GC.SuppressFinalize(this); - return Writer.DisposeAsync(); - } - - public void Export(IEnumerable transactions, IProgress? progress = null) { - ExportAsync(transactions, progress).GetAwaiter().GetResult(); - } - public async Task ExportAsync(IEnumerable transactions, IProgress? progress = null) { if (transactions.Any(tx => tx.Amount < 0)) { throw new ArgumentException("Tranaction amount may not be negative"); diff --git a/Elwig/Helpers/Export/IExporter.cs b/Elwig/Helpers/Export/IExporter.cs index d7b689d..1529cc8 100644 --- a/Elwig/Helpers/Export/IExporter.cs +++ b/Elwig/Helpers/Export/IExporter.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; namespace Elwig.Helpers.Export { - public interface IExporter : IDisposable, IAsyncDisposable { + public interface IExporter : IDisposable { /// /// The default file extension of the exported files to be used (whithout a preceding ".") /// @@ -14,7 +14,9 @@ namespace Elwig.Helpers.Export { /// /// The data to be exported /// The progress object to report to - void Export(IEnumerable data, IProgress? progress = null); + void Export(IEnumerable data, IProgress? progress = null) { + ExportAsync(data, progress).GetAwaiter().GetResult(); + } /// /// Asynchronosly export the given data to the given file. diff --git a/Elwig/Helpers/Export/OdsFile.cs b/Elwig/Helpers/Export/OdsFile.cs index 4bbd7c1..b1f3096 100644 --- a/Elwig/Helpers/Export/OdsFile.cs +++ b/Elwig/Helpers/Export/OdsFile.cs @@ -109,8 +109,13 @@ namespace Elwig.Helpers.Export { + + + + + - + @@ -211,35 +216,59 @@ namespace Elwig.Helpers.Export { } } - await Content.WriteAsync( - $" \r\n" + - FormatCell(table.FullName, colSpan: totalSpan, style: "header") + - $" \r\n" + - $" \r\n" + - FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader") + - $" \r\n" + - $" \r\n" + - $" \r\n" + - $" \r\n" + - $" \r\n"); - foreach (var (name, span, units) in table.ColumnNames.Zip(table.ColumnSpans, table.ColumnUnits)) { - var hasUnits = units.Length > 0; - await Content.WriteAsync(FormatCell(name, colSpan: span, rowSpan: hasUnits ? 1 : 2, style: "th")); - } - await Content.WriteAsync(" \r\n \r\n"); - foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) { - if (units.Length == 0) { - await Content.WriteAsync($" \r\n"); - continue; - } - foreach (var u in units) { - await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th")); - } - } - await Content.WriteAsync(" \r\n"); + await Content.WriteAsync($""" + + {FormatCell(table.FullName, colSpan: totalSpan, style: "header")} + + + {FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader")} + + + + + """); - foreach (var row in table.GetData()) { - await FormatRow(row, table.ColumnUnits, mergeSubRowCells); + var firstSection = true; + foreach (var sec in table.GetData()) { + if (!firstSection) { + await Content.WriteAsync($""" + + + + + + + """); + } + if (sec.Name != null) { + await Content.WriteAsync($""" + + {FormatCell(sec.Name, colSpan: totalSpan, style: "sectionheader")} + + """); + } + + await Content.WriteAsync(" \r\n"); + foreach (var (name, span, units) in table.ColumnNames.Zip(table.ColumnSpans, table.ColumnUnits)) { + var hasUnits = units.Length > 0; + await Content.WriteAsync(FormatCell(name, colSpan: span, rowSpan: hasUnits ? 1 : 2, style: "th")); + } + await Content.WriteAsync(" \r\n \r\n"); + foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) { + if (units.Length == 0) { + await Content.WriteAsync($" \r\n"); + continue; + } + foreach (var u in units) { + await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th")); + } + } + await Content.WriteAsync(" \r\n"); + + foreach (var row in sec.Rows) { + await FormatRow(row, table.ColumnUnits, mergeSubRowCells); + } + firstSection = false; } await Content.WriteAsync(" \r\n"); @@ -314,7 +343,8 @@ namespace Elwig.Helpers.Export { } c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}>{data}"; } else { - c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}>{SecurityElement.Escape(data.ToString())}"; + var lines = string.Join("", data.ToString()?.Split('\n').Select(l => $"{SecurityElement.Escape(l)}") ?? []); + c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}>{lines}"; } return $" {c}\r\n" + (colSpan > 1 ? $" \r\n" : ""); diff --git a/Elwig/Helpers/Export/VCard.cs b/Elwig/Helpers/Export/VCard.cs index cb84024..008b69a 100644 --- a/Elwig/Helpers/Export/VCard.cs +++ b/Elwig/Helpers/Export/VCard.cs @@ -13,7 +13,9 @@ namespace Elwig.Helpers.Export { private readonly StreamWriter _writer; - public VCard(string filename) : this(filename, Utils.UTF8) { } + public VCard(string filename) : + this(filename, Utils.UTF8) { + } public VCard(string filename, Encoding encoding) { _writer = new StreamWriter(filename, false, encoding); @@ -24,11 +26,6 @@ namespace Elwig.Helpers.Export { _writer.Dispose(); } - public ValueTask DisposeAsync() { - GC.SuppressFinalize(this); - return _writer.DisposeAsync(); - } - public async Task ExportAsync(IEnumerable data, IProgress? progress = null) { progress?.Report(0.0); int count = data.Count() + 1, i = 0; @@ -58,10 +55,6 @@ namespace Elwig.Helpers.Export { progress?.Report(100.0); } - public void Export(IEnumerable data, IProgress? progress = null) { - ExportAsync(data, progress).GetAwaiter().GetResult(); - } - private static string? Escape(string? text) { return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", ""); } diff --git a/Elwig/Models/Dtos/DataTable.cs b/Elwig/Models/Dtos/DataTable.cs index ac46298..4e1d67d 100644 --- a/Elwig/Models/Dtos/DataTable.cs +++ b/Elwig/Models/Dtos/DataTable.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Reflection; @@ -9,13 +10,14 @@ namespace Elwig.Models.Dtos { public string Name { get; set; } public string FullName { get; set; } public string? Subtitle { get; set; } - public IEnumerable Rows { get; private set; } + public IEnumerable<(string? Name, IEnumerable Rows)> Sections { get; private set; } + public IEnumerable Rows => Sections.SelectMany(s => s.Rows); public int RowNum => Rows.Count(); public int ColNum => ColumnNames.Count(); - public IEnumerable<(string, Type?)> ColumnDefs => _map.Select(m => (m.Item1, m.Item2?.PropertyType ?? m.Item3?.FieldType)); - public IEnumerable ColumnNames => ColumnDefs.Select(m => m.Item1); - public IEnumerable ColumnTypes => ColumnDefs.Select(m => m.Item2); + public IEnumerable<(string Name, Type? Type)> ColumnDefs => _map.Select(m => (m.Name, m.PInfo?.PropertyType ?? m.FInfo?.FieldType)); + public IEnumerable ColumnNames => ColumnDefs.Select(m => m.Name); + public IEnumerable ColumnTypes => ColumnDefs.Select(m => m.Type); public IEnumerable ColumnFlatTypes { get; private set; } public IEnumerable ColumnSpans { get; private set; } public IEnumerable ColumnWidths { get; private set; } @@ -23,21 +25,25 @@ namespace Elwig.Models.Dtos { private readonly PropertyInfo[] _properties; private readonly FieldInfo[] _fields; - private readonly (string, PropertyInfo?, FieldInfo?)[] _map; + private readonly (string Name, PropertyInfo? PInfo, FieldInfo? FInfo)[] _map; - public DataTable(string name, string fullName, string subtitle, IEnumerable rows, IEnumerable<(string, string, string?, int?)>? colNames = null) : - this(name, fullName, rows, colNames) { + public DataTable(string name, string fullName, string subtitle, IEnumerable<(string? Name, IEnumerable Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) : + this(name, fullName, sections, colNames) { Subtitle = subtitle; } - public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string, string, string?, int?)>? colNames = null) { + public DataTable(string name, string fullName, string subtitle, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) : + this(name, fullName, subtitle, [(null, rows)], colNames) { + } + + public DataTable(string name, string fullName, IEnumerable<(string? Name, IEnumerable Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) { _fields = typeof(T).GetFields(); _properties = typeof(T).GetProperties(); colNames ??= [.. _properties.Select(p => p.Name).Union(_fields.Select(f => f.Name)).Select(i => (i, i, (string?)null, (int?)null))]; - _map = [.. colNames.Select(n => (n.Item2, _properties.FirstOrDefault(p => p?.Name == n.Item1, null), _fields.FirstOrDefault(f => f?.Name == n.Item1, null)))]; + _map = [.. colNames.Select(n => (n.Name, _properties.FirstOrDefault(p => p?.Name == n.Property, null), _fields.FirstOrDefault(f => f?.Name == n.Property, null)))]; Name = name; FullName = fullName; - Rows = rows; + Sections = sections; ColumnFlatTypes = [.. ColumnTypes.SelectMany(type => { var elType = type?.GetElementType(); return type != null && type.IsValueType && type.Name.StartsWith("ValueTuple") ? type.GetFields().Select(f => (Type?)f.FieldType) : @@ -50,44 +56,56 @@ namespace Elwig.Models.Dtos { type != null && elType != null && type.IsArray && elType.IsValueType && elType.Name.StartsWith("ValueTuple") ? elType.GetFields().Length : 1; })]; - ColumnWidths = [.. colNames.Select(c => c.Item4)]; - ColumnUnits = [.. colNames.Select(c => c.Item3?.Split("|").Select(p => p.Length == 0 ? null : p).ToArray() ?? [])]; + ColumnWidths = [.. colNames.Select(c => c.Width)]; + ColumnUnits = [.. colNames.Select(c => c.Unit?.Split("|").Select(p => p.Length == 0 ? null : p).ToArray() ?? [])]; } - public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string, string, string?)>? colNames = null) : - this(name, fullName, rows, colNames?.Select(c => (c.Item1, c.Item2, c.Item3, (int?)null))) { + public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) : + this(name, fullName, [(null, rows)], colNames) { } - public DataTable(string name, IEnumerable rows, IEnumerable<(string, string, string?)>? colNames = null) : + public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit)>? colNames = null) : + this(name, fullName, rows, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)null))) { + } + + public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit, int Width)>? colNames = null) : + this(name, fullName, rows, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)c.Width))) { + } + + public DataTable(string name, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) : this(name, name, rows, colNames) { } - public DataTable(string name, IEnumerable rows, IEnumerable<(string, string, string?, int?)>? colNames = null) : + public DataTable(string name, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit)>? colNames = null) : this(name, name, rows, colNames) { } - public DataTable(string name, IEnumerable rows, IEnumerable<(string, string, string?, int)>? colNames = null) : + public DataTable(string name, IEnumerable rows, IEnumerable<(string Property, string Name, string? Unit, int Width)>? colNames = null) : this(name, name, rows, colNames) { } - public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string, string, string?, int)>? colNames = null) : - this(name, fullName, rows, colNames?.Select(c => (c.Item1, c.Item2, c.Item3, (int?)c.Item4))) { + public DataTable(string name, string fullName, IEnumerable<(string? Name, IEnumerable Rows)> sections, IEnumerable<(string Property, string Name, string? Unit)>? colNames = null) : + this(name, fullName, sections, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)null))) { } - protected IEnumerable<(string, object?)> GetNamedRowData(T row) { - return _map.Select(i => (i.Item1, i.Item2?.GetValue(row) ?? i.Item3?.GetValue(row))); + public DataTable(string name, string fullName, IEnumerable<(string? Name, IEnumerable Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int Width)>? colNames = null) : + this(name, fullName, sections, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)c.Width))) { } - protected IEnumerable GetRowData(T row) { - return GetNamedRowData(row).Select(i => i.Item2); + protected IEnumerable<(string Name, object? Value)> GetNamedRowValues(T row) { + return _map.Select(i => (i.Name, i.PInfo?.GetValue(row) ?? i.FInfo?.GetValue(row))); } - public IEnumerable> GetData() { - return Rows.Select(GetRowData); + protected IEnumerable GetRowValues(T row) { + return GetNamedRowValues(row).Select(i => i.Value); } - public IEnumerable> GetNamedData() { - return Rows.Select(GetNamedRowData); + public IEnumerable<(string? Name, IEnumerable> Rows)> GetData() { + return Sections.Select(s => (s.Name, s.Rows.Select(GetRowValues))); + } + + public IEnumerable<(string? Name, IEnumerable> Rows)> GetNamedData() { + return Sections.Select(s => (s.Name, s.Rows.Select(GetNamedRowValues))); } } } diff --git a/Elwig/Models/Dtos/MemberMovementData.cs b/Elwig/Models/Dtos/MemberMovementData.cs new file mode 100644 index 0000000..a1217ce --- /dev/null +++ b/Elwig/Models/Dtos/MemberMovementData.cs @@ -0,0 +1,142 @@ +using Elwig.Helpers; +using Elwig.Models.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Elwig.Models.Dtos { + public class MemberMovementData : DataTable { + + private static readonly (string, string, string?, int)[] FieldNames = [ + ("Type", "\u2026 durch", null, 30), + ("MgNr", "MgNr.", null, 12), + ("Name1", "Name", null, 40), + ("Name2", "Vorname", null, 25), + ("Address", "Adresse", null, 50), + ("Plz", "PLZ", null, 10), + ("Locality", "Ort", null, 50), + ("Date", "Datum", null, 20), + ("SharesOld", "GA\nalt", null, 10), + ("SharesDiff", "GA\nDiff.", null, 10), + ("SharesNew", "GA\nneu", null, 10), + ("Costs", "Kosten", "€", 15), + ]; + + private MemberMovementData(string name, IEnumerable<(string? Name, IEnumerable Rows)> sections) : + base("Mitgl.-Bewegungen", name, sections, FieldNames) { + } + + public static MemberMovementData FromMemberHistory(string name, IEnumerable rows, Dictionary> history) { + (string? Name, List Rows)[] sections = [ + ("Beitritt", []), + ("Nachzeichnung (GA)", []), + ("Überschreibung", []), + ("Kündigung (GA)", []), + ("Austritt", []), + ]; + foreach (var h in rows.OrderBy(r => r.DateNotice).ThenBy(r => r.ToMgNr).ThenBy(r => r.FromMgNr)) { + + int cat; + if (h.ToMgNr == h.FromMgNr) { + // Umwandlung + if (h.ToType == BusinessShareType.Dormant) { + // Kündigung (GA) + cat = 3; + } else if (h.FromType == h.ToType) { + // Überschreibung + cat = 2; + } else { + throw new NotImplementedException(); + } + } else if (h.DateNotice <= h.ToMember?.EntryDate && h.DateNotice >= h.FromMember?.ExitDate) { + // Überschreibung + cat = 2; + } else if (h.DateNotice <= h.ToMember?.EntryDate) { + // Beitritt + cat = 0; + } else if (h.DateNotice >= h.FromMember?.ExitDate) { + // Austritt + cat = 4; + } else if (h.ToMember == null) { + // Auszahlung + continue; + } else { + // Nachzeichnung (GA) + cat = 1; + } + + Member a; + Member? b = null; + if (h.FromMember != null && h.ToMember != null) { + if (cat <= 1) { + a = h.ToMember; + b = h.FromMember; + } else { + a = h.FromMember; + b = h.ToMember; + } + } else if (h.FromMember != null) { + a = h.FromMember; + } else if (h.ToMember != null) { + a = h.ToMember; + } else { + throw new ArgumentException(); + } + + var oldA = history[a.MgNr].GetValueOrDefault(h.DateNotice); + var d = new MemberMovementDataRow { + Type = [h.ReasonPretty], + + MgNr = [a.MgNr], + Name1 = [a.Name], + Name2 = [a.AdministrativeName2], + Address = [a.Address], + Plz = [a.PostalDest.AtPlz!.Plz], + Locality = [a.PostalDest.AtPlz.Ort.Name], + + Date = h.DateNotice, + SharesOld = [oldA.Shares + oldA.SharesRed + oldA.SharesWhite], + SharesDiff = [cat != 3 && a == h.ToMember ? h.Shares : -h.Shares], + Costs = [h.FromMember == null ? h.TotalValue : null], + }; + d.SharesNew = [d.SharesOld[^1] + (h.FromMgNr == h.ToMgNr ? 0 : d.SharesDiff[^1])]; + + if (b != null) { + var oldB = history[b.MgNr].GetValueOrDefault(h.DateNotice); + d.Type = [..d.Type, "\u2026 " + (cat <= 1 ? "von" : "an")]; + + d.MgNr = [.. d.MgNr, b.MgNr]; + d.Name1 = [.. d.Name1, b.Name]; + d.Name2 = [.. d.Name2, b.AdministrativeName2]; + d.Address = [.. d.Address, b.Address]; + d.Plz = [.. d.Plz, b.PostalDest.AtPlz!.Plz]; + d.Locality = [.. d.Locality, b.PostalDest.AtPlz.Ort.Name]; + + d.SharesOld = [.. d.SharesOld, oldB.Shares + oldB.SharesRed + oldB.SharesWhite]; + d.SharesDiff = [.. d.SharesDiff, b == h.ToMember ? h.Shares : -h.Shares]; + d.SharesNew = [.. d.SharesNew, d.SharesOld[^1] + d.SharesDiff[^1]]; + } + + sections[cat].Rows.Add(d); + } + return new MemberMovementData($"Mitglieder-Bewegungen {name}", sections.Select(s => (s.Name, (IEnumerable)s.Rows))); + } + } + + public class MemberMovementDataRow { + public string[] Type = []; + + public int[] MgNr = []; + public string[] Name1 = []; + public string?[] Name2 = []; + public string[] Address = []; + public int[] Plz = []; + public string[] Locality = []; + + public DateOnly Date; + public int?[] SharesOld = []; + public int[] SharesDiff = []; + public int?[] SharesNew = []; + public decimal?[] Costs = []; + } +} diff --git a/Elwig/Models/Entities/MemberHistory.cs b/Elwig/Models/Entities/MemberHistory.cs index 2aa042c..517f65b 100644 --- a/Elwig/Models/Entities/MemberHistory.cs +++ b/Elwig/Models/Entities/MemberHistory.cs @@ -104,10 +104,10 @@ namespace Elwig.Models.Entities { 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) { + if (ToMgNr == mgnr) { return $"{ReasonPretty} von {FromMember?.FullAdministrativeName ?? "Vorgänger"}" + (Comment != null ? $", {Comment}" : ""); + } else if (FromMgNr == mgnr) { + return $"{ReasonPretty} an {ToMember?.FullAdministrativeName ?? "Nachfolger"}" + (Comment != null ? $", {Comment}" : ""); } } return ReasonPretty + (Comment != null ? $", {Comment}" : ""); diff --git a/Elwig/Services/MemberBusinessSharesService.cs b/Elwig/Services/MemberBusinessSharesService.cs index 4ec2e46..669b739 100644 --- a/Elwig/Services/MemberBusinessSharesService.cs +++ b/Elwig/Services/MemberBusinessSharesService.cs @@ -1,5 +1,7 @@ using Elwig.Helpers; +using Elwig.Helpers.Export; using Elwig.Models; +using Elwig.Models.Dtos; using Elwig.Models.Entities; using Elwig.ViewModels; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,10 @@ using System.Threading.Tasks; namespace Elwig.Services { public static class MemberBusinessSharesService { + public enum ExportSubject { + Pending, + }; + public static async Task InitInputs(this MemberBusinessSharesViewModel vm) { using var ctx = new AppDbContext(); vm.DateNoticeString = $"{DateTime.Today:dd.MM.yyyy}"; @@ -97,5 +103,19 @@ namespace Elwig.Services { await tx.CommitAsync(); }); } + + public static async Task GenerateMemberMovements(ExportSubject subject, ExportMode mode) { + var today = DateTime.Today; + var filename = InteractionService.SaveFile($"Ausstehende Mitgl.-Bewegungen {today:dd.MM.yyyy}", $"MitglBewegungen_{today:yyyy-MM-dd}", "ods"); + if (filename == null) + return; + await Utils.RunForeground(async () => { + using var ctx = new AppDbContext(); + var hist = await ctx.MemberHistory.Where(h => !h.CompanyHasSigned).ToListAsync(); + var data = MemberMovementData.FromMemberHistory($"Ausstehend ({today:dd.MM.yyyy})", hist, await ctx.GetMemberHistory()); + using var ods = new OdsFile(filename); + await ods.AddTable(data); + }); + } } } diff --git a/Elwig/Windows/MemberAdminWindow.xaml b/Elwig/Windows/MemberAdminWindow.xaml index 3114da3..23356c8 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml +++ b/Elwig/Windows/MemberAdminWindow.xaml @@ -110,7 +110,7 @@ Click="Menu_CreditNote_Email_Click"/> - @@ -119,7 +119,7 @@ - @@ -128,7 +128,7 @@ - @@ -147,6 +147,10 @@ IsCheckable="True" IsChecked="{Binding MemberListOrderByOrt, Mode=TwoWay}" Click="Menu_List_Order_Click"/> + + + diff --git a/Elwig/Windows/MemberAdminWindow.xaml.cs b/Elwig/Windows/MemberAdminWindow.xaml.cs index 95bdfe5..30a1dcd 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml.cs +++ b/Elwig/Windows/MemberAdminWindow.xaml.cs @@ -1,6 +1,7 @@ using Elwig.Dialogs; using Elwig.Documents; using Elwig.Helpers; +using Elwig.Helpers.Export; using Elwig.Models.Entities; using Elwig.Services; using Elwig.ViewModels; @@ -114,6 +115,7 @@ namespace Elwig.Windows { } if (!App.Client.EnableMemberHistory) { + Menu_BusinessShares.Visibility = Visibility.Collapsed; BusinessSharesButton.Visibility = Visibility.Hidden; } @@ -809,6 +811,10 @@ namespace Elwig.Windows { await App.Client.UpdateValues(); } + private async void Menu_BusinessShares_SavePending_Click(object sender, RoutedEventArgs evt) { + await MemberBusinessSharesService.GenerateMemberMovements(MemberBusinessSharesService.ExportSubject.Pending, ExportMode.SaveList); + } + private void FocusSearchInput(object sender, RoutedEventArgs evt) { if (!IsEditing && !IsCreating) { SearchInput.Focus();