MemberAdminWindow: Add pending member movement export

This commit is contained in:
2026-10-02 23:47:56 +02:00
parent 0e586cc438
commit abb9e5a577
14 changed files with 335 additions and 103 deletions
+6 -6
View File
@@ -274,11 +274,11 @@ namespace Elwig.Documents {
.AddCells(NewBusinessSharesCells(new())); .AddCells(NewBusinessSharesCells(new()));
} }
var recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => 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.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.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Red ? 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.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.White ? 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.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Dormant ? 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); 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) { if (cur.Shares != 0 || cur.SharesRed != 0 || cur.SharesWhite != 0 || cur.SharesDormant != 0) {
@@ -297,7 +297,7 @@ namespace Elwig.Documents {
DateOnly? lastDate = null; DateOnly? lastDate = null;
foreach (var row in history) { foreach (var row in history) {
int s = 0, r = 0, w = 0, d = 0; 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) { switch (row.FromType) {
case BusinessShareType.Normal: s -= row.Shares; break; case BusinessShareType.Normal: s -= row.Shares; break;
case BusinessShareType.Red: r -= row.Shares; break; case BusinessShareType.Red: r -= row.Shares; break;
+39
View File
@@ -1,3 +1,4 @@
using Elwig.Models;
using Elwig.Models.Dtos; using Elwig.Models.Dtos;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Elwig.Services; using Elwig.Services;
@@ -531,6 +532,44 @@ namespace Elwig.Helpers {
return _memberHistory[year].GetValueOrDefault(mgnr, new()); return _memberHistory[year].GetValueOrDefault(mgnr, new());
} }
public async Task<Dictionary<int, SortedDictionary<DateOnly, MemberHistoryPoint>>> GetMemberHistory() {
var today = DateOnly.FromDateTime(DateTime.Today).AddDays(1);
var history = new Dictionary<int, SortedDictionary<DateOnly, MemberHistoryPoint>>();
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<Dictionary<string, AreaCmtBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) { public async Task<Dictionary<string, AreaCmtBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberAreaCommitmentBuckets.ContainsKey(year)) if (!_memberAreaCommitmentBuckets.ContainsKey(year))
await FetchMemberAreaCommitmentBuckets(year, cnx); await FetchMemberAreaCommitmentBuckets(year, cnx);
-4
View File
@@ -46,10 +46,6 @@ namespace Elwig.Helpers.Export {
await ExportAsync(rows); await ExportAsync(rows);
} }
public void Export(int year) {
ExportAsync(year).GetAwaiter().GetResult();
}
public override string FormatRow(Row row) { public override string FormatRow(Row row) {
var (member, delivery) = row; var (member, delivery) = row;
var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member; var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member;
-9
View File
@@ -29,11 +29,6 @@ namespace Elwig.Helpers.Export {
_writer.Dispose(); _writer.Dispose();
} }
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
}
public async Task ExportAsync(IEnumerable<T> data, IProgress<double>? progress = null) { public async Task ExportAsync(IEnumerable<T> data, IProgress<double>? progress = null) {
progress?.Report(0.0); progress?.Report(0.0);
int count = data.Count() + 2, i = 0; int count = data.Count() + 2, i = 0;
@@ -50,10 +45,6 @@ namespace Elwig.Helpers.Export {
progress?.Report(100.0); progress?.Report(100.0);
} }
public void Export(IEnumerable<T> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
public string FormatRow(IEnumerable row) { public string FormatRow(IEnumerable row) {
return string.Join(Separator, row); return string.Join(Separator, row);
} }
-9
View File
@@ -28,15 +28,6 @@ namespace Elwig.Helpers.Export {
Writer.Dispose(); Writer.Dispose();
} }
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return Writer.DisposeAsync();
}
public void Export(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
ExportAsync(transactions, progress).GetAwaiter().GetResult();
}
public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) { public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
if (transactions.Any(tx => tx.Amount < 0)) { if (transactions.Any(tx => tx.Amount < 0)) {
throw new ArgumentException("Tranaction amount may not be negative"); throw new ArgumentException("Tranaction amount may not be negative");
+4 -2
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
public interface IExporter<T> : IDisposable, IAsyncDisposable { public interface IExporter<T> : IDisposable {
/// <summary> /// <summary>
/// The default file extension of the exported files to be used (whithout a preceding ".") /// The default file extension of the exported files to be used (whithout a preceding ".")
/// </summary> /// </summary>
@@ -14,7 +14,9 @@ namespace Elwig.Helpers.Export {
/// </summary> /// </summary>
/// <param name="data">The data to be exported</param> /// <param name="data">The data to be exported</param>
/// <param name="progress">The progress object to report to</param> /// <param name="progress">The progress object to report to</param>
void Export(IEnumerable<T> data, IProgress<double>? progress = null); void Export(IEnumerable<T> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
/// <summary> /// <summary>
/// Asynchronosly export the given data to the given file. /// Asynchronosly export the given data to the given file.
+60 -30
View File
@@ -109,8 +109,13 @@ namespace Elwig.Helpers.Export {
<style:paragraph-properties fo:text-align="center"/> <style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/> <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style> </style:style>
<style:style style:name="sectionheader" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/>
<style:paragraph-properties fo:text-align="left"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" fo:font-size="14pt"/>
</style:style>
<style:style style:name="th" style:family="table-cell" style:parent-style-name="default"> <style:style style:name="th" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle"/> <style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle" fo:wrap-option="wrap"/>
<style:paragraph-properties fo:text-align="center"/> <style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/> <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style> </style:style>
@@ -211,35 +216,59 @@ namespace Elwig.Helpers.Export {
} }
} }
await Content.WriteAsync( await Content.WriteAsync($"""
$" <table:table-row>\r\n" + <table:table-row>
FormatCell(table.FullName, colSpan: totalSpan, style: "header") + {FormatCell(table.FullName, colSpan: totalSpan, style: "header")}
$" </table:table-row>\r\n" + </table:table-row>
$" <table:table-row>\r\n" + <table:table-row>
FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader") + {FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader")}
$" </table:table-row>\r\n" + </table:table-row>
$" <table:table-row>\r\n" + <table:table-row>
$" <table:table-cell table:number-columns-repeated=\"{totalSpan}\"/>\r\n" + <table:table-cell table:number-columns-repeated="{totalSpan}"/>
$" </table:table-row>\r\n" + </table:table-row>
$" <table:table-row>\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(" </table:table-row>\r\n <table:table-row>\r\n");
foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) {
if (units.Length == 0) {
await Content.WriteAsync($" <table:covered-table-cell table:number-columns-repeated=\"{span}\"/>\r\n");
continue;
}
foreach (var u in units) {
await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th"));
}
}
await Content.WriteAsync(" </table:table-row>\r\n");
foreach (var row in table.GetData()) { var firstSection = true;
await FormatRow(row, table.ColumnUnits, mergeSubRowCells); foreach (var sec in table.GetData()) {
if (!firstSection) {
await Content.WriteAsync($"""
<table:table-row>
<table:table-cell table:number-columns-repeated="{totalSpan}"/>
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="{totalSpan}"/>
</table:table-row>
""");
}
if (sec.Name != null) {
await Content.WriteAsync($"""
<table:table-row>
{FormatCell(sec.Name, colSpan: totalSpan, style: "sectionheader")}
</table:table-row>
""");
}
await Content.WriteAsync(" <table:table-row>\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(" </table:table-row>\r\n <table:table-row>\r\n");
foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) {
if (units.Length == 0) {
await Content.WriteAsync($" <table:covered-table-cell table:number-columns-repeated=\"{span}\"/>\r\n");
continue;
}
foreach (var u in units) {
await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th"));
}
}
await Content.WriteAsync(" </table:table-row>\r\n");
foreach (var row in sec.Rows) {
await FormatRow(row, table.ColumnUnits, mergeSubRowCells);
}
firstSection = false;
} }
await Content.WriteAsync(" </table:table>\r\n"); await Content.WriteAsync(" </table:table>\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}><text:p>{data}</text:p></{ct}>"; c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}><text:p>{data}</text:p></{ct}>";
} else { } else {
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}><text:p>{SecurityElement.Escape(data.ToString())}</text:p></{ct}>"; var lines = string.Join("", data.ToString()?.Split('\n').Select(l => $"<text:p>{SecurityElement.Escape(l)}</text:p>") ?? []);
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}>{lines}</{ct}>";
} }
return $" {c}\r\n" + (colSpan > 1 ? $" <table:covered-table-cell table:number-columns-repeated=\"{colSpan - 1}\"/>\r\n" : ""); return $" {c}\r\n" + (colSpan > 1 ? $" <table:covered-table-cell table:number-columns-repeated=\"{colSpan - 1}\"/>\r\n" : "");
+3 -10
View File
@@ -13,7 +13,9 @@ namespace Elwig.Helpers.Export {
private readonly StreamWriter _writer; 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) { public VCard(string filename, Encoding encoding) {
_writer = new StreamWriter(filename, false, encoding); _writer = new StreamWriter(filename, false, encoding);
@@ -24,11 +26,6 @@ namespace Elwig.Helpers.Export {
_writer.Dispose(); _writer.Dispose();
} }
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
}
public async Task ExportAsync(IEnumerable<Member> data, IProgress<double>? progress = null) { public async Task ExportAsync(IEnumerable<Member> data, IProgress<double>? progress = null) {
progress?.Report(0.0); progress?.Report(0.0);
int count = data.Count() + 1, i = 0; int count = data.Count() + 1, i = 0;
@@ -58,10 +55,6 @@ namespace Elwig.Helpers.Export {
progress?.Report(100.0); progress?.Report(100.0);
} }
public void Export(IEnumerable<Member> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
private static string? Escape(string? text) { private static string? Escape(string? text) {
return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", ""); return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", "");
} }
+45 -27
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -9,13 +10,14 @@ namespace Elwig.Models.Dtos {
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; } public string FullName { get; set; }
public string? Subtitle { get; set; } public string? Subtitle { get; set; }
public IEnumerable<T> Rows { get; private set; } public IEnumerable<(string? Name, IEnumerable<T> Rows)> Sections { get; private set; }
public IEnumerable<T> Rows => Sections.SelectMany(s => s.Rows);
public int RowNum => Rows.Count(); public int RowNum => Rows.Count();
public int ColNum => ColumnNames.Count(); public int ColNum => ColumnNames.Count();
public IEnumerable<(string, Type?)> ColumnDefs => _map.Select(m => (m.Item1, m.Item2?.PropertyType ?? m.Item3?.FieldType)); public IEnumerable<(string Name, Type? Type)> ColumnDefs => _map.Select(m => (m.Name, m.PInfo?.PropertyType ?? m.FInfo?.FieldType));
public IEnumerable<string> ColumnNames => ColumnDefs.Select(m => m.Item1); public IEnumerable<string> ColumnNames => ColumnDefs.Select(m => m.Name);
public IEnumerable<Type?> ColumnTypes => ColumnDefs.Select(m => m.Item2); public IEnumerable<Type?> ColumnTypes => ColumnDefs.Select(m => m.Type);
public IEnumerable<Type?> ColumnFlatTypes { get; private set; } public IEnumerable<Type?> ColumnFlatTypes { get; private set; }
public IEnumerable<int> ColumnSpans { get; private set; } public IEnumerable<int> ColumnSpans { get; private set; }
public IEnumerable<int?> ColumnWidths { get; private set; } public IEnumerable<int?> ColumnWidths { get; private set; }
@@ -23,21 +25,25 @@ namespace Elwig.Models.Dtos {
private readonly PropertyInfo[] _properties; private readonly PropertyInfo[] _properties;
private readonly FieldInfo[] _fields; 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<T> rows, IEnumerable<(string, string, string?, int?)>? colNames = null) : public DataTable(string name, string fullName, string subtitle, IEnumerable<(string? Name, IEnumerable<T> Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) :
this(name, fullName, rows, colNames) { this(name, fullName, sections, colNames) {
Subtitle = subtitle; Subtitle = subtitle;
} }
public DataTable(string name, string fullName, IEnumerable<T> rows, IEnumerable<(string, string, string?, int?)>? colNames = null) { public DataTable(string name, string fullName, string subtitle, IEnumerable<T> 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<T> Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) {
_fields = typeof(T).GetFields(); _fields = typeof(T).GetFields();
_properties = typeof(T).GetProperties(); _properties = typeof(T).GetProperties();
colNames ??= [.. _properties.Select(p => p.Name).Union(_fields.Select(f => f.Name)).Select(i => (i, i, (string?)null, (int?)null))]; 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; Name = name;
FullName = fullName; FullName = fullName;
Rows = rows; Sections = sections;
ColumnFlatTypes = [.. ColumnTypes.SelectMany(type => { ColumnFlatTypes = [.. ColumnTypes.SelectMany(type => {
var elType = type?.GetElementType(); var elType = type?.GetElementType();
return type != null && type.IsValueType && type.Name.StartsWith("ValueTuple") ? type.GetFields().Select(f => (Type?)f.FieldType) : 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 : type != null && elType != null && type.IsArray && elType.IsValueType && elType.Name.StartsWith("ValueTuple") ? elType.GetFields().Length :
1; 1;
})]; })];
ColumnWidths = [.. colNames.Select(c => c.Item4)]; ColumnWidths = [.. colNames.Select(c => c.Width)];
ColumnUnits = [.. colNames.Select(c => c.Item3?.Split("|").Select(p => p.Length == 0 ? null : p).ToArray() ?? [])]; ColumnUnits = [.. colNames.Select(c => c.Unit?.Split("|").Select(p => p.Length == 0 ? null : p).ToArray() ?? [])];
} }
public DataTable(string name, string fullName, IEnumerable<T> rows, IEnumerable<(string, string, string?)>? colNames = null) : public DataTable(string name, string fullName, IEnumerable<T> rows, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) :
this(name, fullName, rows, colNames?.Select(c => (c.Item1, c.Item2, c.Item3, (int?)null))) { this(name, fullName, [(null, rows)], colNames) {
} }
public DataTable(string name, IEnumerable<T> rows, IEnumerable<(string, string, string?)>? colNames = null) : public DataTable(string name, string fullName, IEnumerable<T> 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<T> 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<T> rows, IEnumerable<(string Property, string Name, string? Unit, int? Width)>? colNames = null) :
this(name, name, rows, colNames) { this(name, name, rows, colNames) {
} }
public DataTable(string name, IEnumerable<T> rows, IEnumerable<(string, string, string?, int?)>? colNames = null) : public DataTable(string name, IEnumerable<T> rows, IEnumerable<(string Property, string Name, string? Unit)>? colNames = null) :
this(name, name, rows, colNames) { this(name, name, rows, colNames) {
} }
public DataTable(string name, IEnumerable<T> rows, IEnumerable<(string, string, string?, int)>? colNames = null) : public DataTable(string name, IEnumerable<T> rows, IEnumerable<(string Property, string Name, string? Unit, int Width)>? colNames = null) :
this(name, name, rows, colNames) { this(name, name, rows, colNames) {
} }
public DataTable(string name, string fullName, IEnumerable<T> rows, IEnumerable<(string, string, string?, int)>? colNames = null) : public DataTable(string name, string fullName, IEnumerable<(string? Name, IEnumerable<T> Rows)> sections, IEnumerable<(string Property, string Name, string? Unit)>? colNames = null) :
this(name, fullName, rows, colNames?.Select(c => (c.Item1, c.Item2, c.Item3, (int?)c.Item4))) { this(name, fullName, sections, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)null))) {
} }
protected IEnumerable<(string, object?)> GetNamedRowData(T row) { public DataTable(string name, string fullName, IEnumerable<(string? Name, IEnumerable<T> Rows)> sections, IEnumerable<(string Property, string Name, string? Unit, int Width)>? colNames = null) :
return _map.Select(i => (i.Item1, i.Item2?.GetValue(row) ?? i.Item3?.GetValue(row))); this(name, fullName, sections, colNames?.Select(c => (c.Property, c.Name, c.Unit, (int?)c.Width))) {
} }
protected IEnumerable<object?> GetRowData(T row) { protected IEnumerable<(string Name, object? Value)> GetNamedRowValues(T row) {
return GetNamedRowData(row).Select(i => i.Item2); return _map.Select(i => (i.Name, i.PInfo?.GetValue(row) ?? i.FInfo?.GetValue(row)));
} }
public IEnumerable<IEnumerable<object?>> GetData() { protected IEnumerable<object?> GetRowValues(T row) {
return Rows.Select(GetRowData); return GetNamedRowValues(row).Select(i => i.Value);
} }
public IEnumerable<IEnumerable<(string, object?)>> GetNamedData() { public IEnumerable<(string? Name, IEnumerable<IEnumerable<object?>> Rows)> GetData() {
return Rows.Select(GetNamedRowData); return Sections.Select(s => (s.Name, s.Rows.Select(GetRowValues)));
}
public IEnumerable<(string? Name, IEnumerable<IEnumerable<(string Name, object? Value)>> Rows)> GetNamedData() {
return Sections.Select(s => (s.Name, s.Rows.Select(GetNamedRowValues)));
} }
} }
} }
+142
View File
@@ -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<MemberMovementDataRow> {
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<MemberMovementDataRow> Rows)> sections) :
base("Mitgl.-Bewegungen", name, sections, FieldNames) {
}
public static MemberMovementData FromMemberHistory(string name, IEnumerable<MemberHistory> rows, Dictionary<int, SortedDictionary<DateOnly, MemberHistoryPoint>> history) {
(string? Name, List<MemberMovementDataRow> 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<MemberMovementDataRow>)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 = [];
}
}
+3 -3
View File
@@ -104,10 +104,10 @@ namespace Elwig.Models.Entities {
public string FormatReasonPhrase(int mgnr) { public string FormatReasonPhrase(int mgnr) {
if (Reason == REASON_TRANSFER) { if (Reason == REASON_TRANSFER) {
if (FromMgNr == mgnr) { if (ToMgNr == 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} 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}" : ""); return ReasonPretty + (Comment != null ? $", {Comment}" : "");
@@ -1,5 +1,7 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Helpers.Export;
using Elwig.Models; using Elwig.Models;
using Elwig.Models.Dtos;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Elwig.ViewModels; using Elwig.ViewModels;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -10,6 +12,10 @@ using System.Threading.Tasks;
namespace Elwig.Services { namespace Elwig.Services {
public static class MemberBusinessSharesService { public static class MemberBusinessSharesService {
public enum ExportSubject {
Pending,
};
public static async Task InitInputs(this MemberBusinessSharesViewModel vm) { public static async Task InitInputs(this MemberBusinessSharesViewModel vm) {
using var ctx = new AppDbContext(); using var ctx = new AppDbContext();
vm.DateNoticeString = $"{DateTime.Today:dd.MM.yyyy}"; vm.DateNoticeString = $"{DateTime.Today:dd.MM.yyyy}";
@@ -97,5 +103,19 @@ namespace Elwig.Services {
await tx.CommitAsync(); 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);
});
}
} }
} }
+7 -3
View File
@@ -110,7 +110,7 @@
Click="Menu_CreditNote_Email_Click"/> Click="Menu_CreditNote_Email_Click"/>
</MenuItem> </MenuItem>
<MenuItem Header="Mitgliederliste" x:Name="Menu_List"> <MenuItem Header="Mitgliederliste" x:Name="Menu_List">
<MenuItem x:Name="Menu_List_SaveActive" Header="...mit Aktiven speichern (Excel)" <MenuItem x:Name="Menu_List_SaveActive" Header="...mit Aktiven speichern... (Excel)"
Click="Menu_List_SaveActive_Click" InputGestureText="Strg+L"/> Click="Menu_List_SaveActive_Click" InputGestureText="Strg+L"/>
<MenuItem x:Name="Menu_List_ShowActive" Header="...mit Aktiven anzeigen (PDF)" <MenuItem x:Name="Menu_List_ShowActive" Header="...mit Aktiven anzeigen (PDF)"
Click="Menu_List_ShowActive_Click" InputGestureText="Strg+O"/> Click="Menu_List_ShowActive_Click" InputGestureText="Strg+O"/>
@@ -119,7 +119,7 @@
<MenuItem x:Name="Menu_List_PrintActive" Header="...mit Aktiven drucken" <MenuItem x:Name="Menu_List_PrintActive" Header="...mit Aktiven drucken"
Click="Menu_List_PrintActive_Click" InputGestureText="Strg+Shift+O"/> Click="Menu_List_PrintActive_Click" InputGestureText="Strg+Shift+O"/>
<Separator/> <Separator/>
<MenuItem x:Name="Menu_List_SaveFilters" Header="...aus Filtern speichern (Excel)" <MenuItem x:Name="Menu_List_SaveFilters" Header="...aus Filtern speichern... (Excel)"
Click="Menu_List_SaveFilters_Click"/> Click="Menu_List_SaveFilters_Click"/>
<MenuItem x:Name="Menu_List_ShowFilters" Header="...aus Filtern anzeigen (PDF)" <MenuItem x:Name="Menu_List_ShowFilters" Header="...aus Filtern anzeigen (PDF)"
Click="Menu_List_ShowFilters_Click"/> Click="Menu_List_ShowFilters_Click"/>
@@ -128,7 +128,7 @@
<MenuItem x:Name="Menu_List_PrintFilters" Header="...aus Filtern drucken" <MenuItem x:Name="Menu_List_PrintFilters" Header="...aus Filtern drucken"
Click="Menu_List_PrintFilters_Click"/> Click="Menu_List_PrintFilters_Click"/>
<Separator/> <Separator/>
<MenuItem x:Name="Menu_List_SaveAll" Header="...mit allen speichern (Excel)" <MenuItem x:Name="Menu_List_SaveAll" Header="...mit allen speichern... (Excel)"
Click="Menu_List_SaveAll_Click"/> Click="Menu_List_SaveAll_Click"/>
<MenuItem x:Name="Menu_List_ShowAll" Header="...mit allen anzeigen (PDF)" <MenuItem x:Name="Menu_List_ShowAll" Header="...mit allen anzeigen (PDF)"
Click="Menu_List_ShowAll_Click"/> Click="Menu_List_ShowAll_Click"/>
@@ -147,6 +147,10 @@
IsCheckable="True" IsChecked="{Binding MemberListOrderByOrt, Mode=TwoWay}" IsCheckable="True" IsChecked="{Binding MemberListOrderByOrt, Mode=TwoWay}"
Click="Menu_List_Order_Click"/> Click="Menu_List_Order_Click"/>
</MenuItem> </MenuItem>
<MenuItem Header="Geschäftsanteile" x:Name="Menu_BusinessShares">
<MenuItem x:Name="Menu_BusinessShares_SavePending" Header="Ausstehende Mitlg.-Bewegungen speichern... (Excel)"
Click="Menu_BusinessShares_SavePending_Click"/>
</MenuItem>
<MenuItem Header="Export"> <MenuItem Header="Export">
<MenuItem x:Name="Menu_Export_ExportSelected" Header="...von ausgewähltem Mitglied speichern..." IsEnabled="False" <MenuItem x:Name="Menu_Export_ExportSelected" Header="...von ausgewähltem Mitglied speichern..." IsEnabled="False"
Click="Menu_Export_ExportSelected_Click"/> Click="Menu_Export_ExportSelected_Click"/>
+6
View File
@@ -1,6 +1,7 @@
using Elwig.Dialogs; using Elwig.Dialogs;
using Elwig.Documents; using Elwig.Documents;
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Helpers.Export;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Elwig.Services; using Elwig.Services;
using Elwig.ViewModels; using Elwig.ViewModels;
@@ -114,6 +115,7 @@ namespace Elwig.Windows {
} }
if (!App.Client.EnableMemberHistory) { if (!App.Client.EnableMemberHistory) {
Menu_BusinessShares.Visibility = Visibility.Collapsed;
BusinessSharesButton.Visibility = Visibility.Hidden; BusinessSharesButton.Visibility = Visibility.Hidden;
} }
@@ -809,6 +811,10 @@ namespace Elwig.Windows {
await App.Client.UpdateValues(); 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) { private void FocusSearchInput(object sender, RoutedEventArgs evt) {
if (!IsEditing && !IsCreating) { if (!IsEditing && !IsCreating) {
SearchInput.Focus(); SearchInput.Focus();