[#36] MemberAdminWindow: Add MemberList

This commit is contained in:
2024-03-26 12:53:31 +01:00
parent 5795c5e8ba
commit f4ef75ac40
10 changed files with 341 additions and 31 deletions

View File

@ -116,6 +116,27 @@ main table tr.sectionheading th {
border-top: 0.5pt solid black;
}
main table tr.header {
border: 0.5pt solid black;
background-color: #E0E0E0;
}
main table tr.header th {
font-weight: bold;
font-style: normal;
font-size: 16pt;
padding: 1mm 2mm;
}
main table tr.spacing td,
main table tr.spacing th {
height: 5mm;
}
main table tr.spacing ~ tr.header {
break-before: avoid;
}
main table.number thead tr:first-child th:first-child {
text-align: left;
}

View File

@ -84,6 +84,8 @@ namespace Elwig.Documents {
name = "DeliveryConfirmation";
} else if (this is MemberDataSheet) {
name = "MemberDataSheet";
} else if (this is MemberList) {
name = "MemberList";
} else {
throw new InvalidOperationException("Invalid document object");
}

View File

@ -0,0 +1,21 @@
using Elwig.Models.Dtos;
using System.Collections.Generic;
namespace Elwig.Documents {
public class MemberList : Document {
public new static string Name => "Mitgliederliste";
public string Filter;
public IEnumerable<MemberListRow> Members;
public MemberList(string filter, IEnumerable<MemberListRow> members) : base(Name) {
Filter = filter;
Members = members;
}
public MemberList(string filter, MemberListData data) :
this(filter, data.Rows) {
}
}
}

View File

@ -0,0 +1,72 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.MemberList>
@model Elwig.Documents.MemberList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberList.css"/>
<main>
<h1>Mitgliederliste</h1>
<h2>@Model.Filter</h2>
<table class="members">
<colgroup>
<col style="width: 8mm;"/>
<col style="width: 42mm;"/>
<col style="width: 40mm;"/>
<col style="width: 8mm;"/>
<col style="width: 20mm;"/>
<col style="width: 12mm;"/>
<col style="width: 5mm;" />
<col style="width: 18mm;"/>
<col style="width: 12mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2">Nr.</th>
<th rowspan="2" style="text-align: left;">Name</th>
<th rowspan="2" style="text-align: left;">Adresse</th>
<th rowspan="2">PLZ</th>
<th rowspan="2" style="text-align: left;">Ort</th>
<th rowspan="2">Betr.-Nr.</th>
<th rowspan="2">GA</th>
<th rowspan="2" style="text-align: left;">Stamm-KG</th>
<th>Geb. Fl.</th>
</tr>
<tr>
<th class="unit">[m²]</th>
</tr>
</thead>
<tbody class="small">
@{
string? lastBranch = Model.Members.Select(m => m.Branch).Distinct().Count() == 1 ? null : "";
}
@foreach (var m in Model.Members) {
if (lastBranch != null && m.Branch != lastBranch) {
<tr class="spacing"><td colspan="9"></td></tr>
<tr class="header">
<th colspan="9">@m.Branch</th>
</tr>
lastBranch = m.Branch;
}
<tr>
<td class="number" rowspan="@(m.BillingName != null ? 2 : 1)">@m.MgNr</td>
<td>@m.Name.Replace('ß', 'ẞ').ToUpper() @m.GivenName</td>
<td>@m.Address</td>
<td>@m.Plz</td>
<td class="tiny">@m.Locality</td>
<td>@m.LfbisNr</td>
<td class="number">@m.BusinessShares</td>
<td class="tiny">@m.DefaultKg</td>
<td class="number">@($"{m.AreaCommitment:N0}")</td>
</tr>
if (m.BillingName != null) {
<tr>
<td>@m.BillingName</td>
<td>@m.BillingAddress</td>
<td>@m.BillingPlz</td>
<td class="tiny">@m.BillingLocality</td>
<td colspan="4"></td>
</tr>
}
}
</tbody>
</table>
</main>

View File

@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}

View File

@ -24,6 +24,8 @@ using System.Collections;
using Elwig.Documents;
using MimeKit;
using System.Windows.Input;
using LinqKit;
using System.Linq.Expressions;
namespace Elwig.Helpers {
public static partial class Utils {
@ -489,14 +491,16 @@ namespace Elwig.Helpers {
}
}
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments(int yearTo) =>
c => (c.YearFrom <= yearTo) && (c.YearTo == null || c.YearTo >= yearTo);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int yearTo) {
return query.Where(c => (c.YearFrom <= CurrentYear) && (c.YearTo == null || c.YearTo >= yearTo));
}
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int yearTo) =>
query.Where(ActiveAreaCommitments(yearTo));
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int yearTo) {
return query.Where(c => (c.YearFrom <= CurrentYear) && (c.YearTo == null || c.YearTo >= yearTo));
}
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int yearTo) =>
query.Where(c => ActiveAreaCommitments(yearTo).Invoke(c));
}
}

View File

@ -25,6 +25,11 @@ namespace Elwig.Models.Dtos {
private readonly FieldInfo[] _fields;
private readonly (string, PropertyInfo?, FieldInfo?)[] _map;
public DataTable(string name, string fullName, string subtitle, IEnumerable<T> rows, IEnumerable<(string, string, string?, int?)>? colNames = null) :
this(name, fullName, rows, colNames) {
Subtitle = subtitle;
}
public DataTable(string name, string fullName, IEnumerable<T> rows, IEnumerable<(string, string, string?, int?)>? colNames = null) {
_fields = typeof(T).GetFields();
_properties = typeof(T).GetProperties();

View File

@ -0,0 +1,110 @@
using Elwig.Documents;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Elwig.Helpers;
using System;
namespace Elwig.Models.Dtos {
public class MemberListData : DataTable<MemberListRow> {
private static readonly (string, string, string?, int?)[] FieldNames = [
("MgNr", "MgNr.", null, 12),
("Name", "Name", null, 40),
("GivenName", "Vorname", null, 40),
("Address", "Adresse", null, 60),
("Plz", "PLZ", null, 10),
("Locality", "Ort", null, 60),
("DefaultKg", "Stammgemeinde", null, 60),
("Branch", "Zweigstelle", null, 40),
("BusinessShares", "GA", null, 10),
("BillingName", "Rechnungsname", null, 60),
("BillingAddress", "Rechnungsadresse", null, 60),
("BillingPlz", "PLZ", null, 10),
("BillingLocality", "Ort", null, 60),
("LfbisNr", "Betr.-Nr.", null, 20),
("IsBuchführend", "buchf.", null, 15),
("IsOrganic", "Bio", null, 15),
("IsActive", "aktiv", null, 15),
("EntryDate", "Eintritt", null, 20),
("ExitDate", "Austritt", null, 20),
("AreaCommitment", "geb. Fläche", "m²", 20),
("UstIdNr", "UID", null, 25),
("Iban", "IBAN", null, 45),
("Bic", "BIC", null, 30),
("Comment", "Anmerkung", null, 60),
];
public MemberListData(IEnumerable<MemberListRow> rows, List<string> filterNames) :
base(MemberList.Name, MemberList.Name, string.Join(" / ", filterNames), rows, FieldNames) {
}
public static async Task<MemberListData> FromQuery(IQueryable<Member> query, List<string> filterNames) {
var areaCom = await query.ToDictionaryAsync(m => m.MgNr, m => Utils.ActiveAreaCommitments(m.AreaCommitments).Sum(c => c.Area));
return new((await query
.Include(m => m.DefaultWbKg!.AtKg)
.Include(m => m.Branch)
.Include(m => m.PostalDest.AtPlz!.Ort)
.Include(m => m.BillingAddress!.PostalDest.AtPlz!.Ort)
.ToListAsync()).Select(m => new MemberListRow(m, areaCom[m.MgNr])), filterNames);
}
}
public class MemberListRow {
public int MgNr;
public string? Name;
public string? GivenName;
public string? DefaultKg;
public string? Branch;
public int BusinessShares;
public string Address;
public int Plz;
public string Locality;
public string? BillingName;
public string? BillingAddress;
public int? BillingPlz;
public string? BillingLocality;
public string? LfbisNr;
public string? UstIdNr;
public string? Iban;
public string? Bic;
public int? AreaCommitment;
public bool IsBuchführend;
public bool IsOrganic;
public bool IsActive;
public DateOnly? EntryDate;
public DateOnly? ExitDate;
public string? Comment;
public MemberListRow(Member m, int? areaCom = null) {
MgNr = m.MgNr;
Name = m.FamilyName;
GivenName = m.GivenName;
DefaultKg = m.DefaultKg?.Name;
Branch = m.Branch?.Name;
BusinessShares = m.BusinessShares;
Address = m.Address;
Plz = m.PostalDest.AtPlz!.Plz;
Locality = m.PostalDest.AtPlz!.Ort.Name;
if (m.BillingAddress is BillingAddr a) {
BillingName = a.Name;
BillingAddress = a.Address;
BillingPlz = a.PostalDest.AtPlz!.Plz;
BillingLocality = a.PostalDest.AtPlz!.Ort.Name;
}
LfbisNr = m.LfbisNr;
UstIdNr = m.UstIdNr;
Iban = m.Iban != null ? Utils.FormatIban(m.Iban) : null;
Bic = m.Bic;
IsBuchführend = m.IsBuchführend;
IsOrganic = m.IsOrganic;
IsActive = m.IsActive;
EntryDate = m.EntryDate;
ExitDate = m.ExitDate;
Comment = m.Comment;
AreaCommitment = areaCom == 0 ? null : areaCom;
}
}
}

View File

@ -157,7 +157,7 @@ namespace Elwig.Models.Entities {
public virtual ICollection<AreaCom> AreaCommitments { get; private set; } = null!;
public IQueryable<AreaCom> ActiveAreaCommitments(AppDbContext ctx) {
return Utils.ActiveAreaCommitments(ctx.AreaCommitments.Where(c => c.MgNr == MgNr));
return ctx.AreaCommitments.Where(c => c.MgNr == MgNr).Where(Utils.ActiveAreaCommitments());
}
[InverseProperty(nameof(BillingAddr.Member))]

View File

@ -12,6 +12,8 @@ using System.Collections.ObjectModel;
using Elwig.Documents;
using System.Diagnostics;
using Elwig.Models.Dtos;
using Elwig.Helpers.Export;
using Microsoft.Win32;
namespace Elwig.Windows {
public partial class MemberAdminWindow : AdministrationWindow {
@ -117,10 +119,13 @@ namespace Elwig.Windows {
await RefreshMemberListQuery();
}
private async Task RefreshMemberListQuery(bool updateSort = false) {
using var ctx = new AppDbContext();
private async Task<(List<string>, IQueryable<Member>, List<string>)> GetFilters(AppDbContext ctx) {
List<string> filterNames = [];
IQueryable<Member> memberQuery = ctx.Members;
if (ActiveMemberInput.IsChecked == true) memberQuery = memberQuery.Where(m => m.IsActive);
if (ActiveMemberInput.IsChecked == true) {
memberQuery = memberQuery.Where(m => m.IsActive);
filterNames.Add("aktive Mitglieder");
}
var filterMgNr = new List<int>();
var filterZwst = new List<string>();
@ -142,56 +147,74 @@ namespace Elwig.Windows {
if (e.Length >= 5 && e.Length <= 10 && "funktionär".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsFunktionär);
filter.RemoveAt(i--);
filterNames.Add("Funktionäre");
} else if (e.Length >= 6 && e.Length <= 11 && e[0] == '!' && "funktionär".StartsWith(e[1..], StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsFunktionär);
filter.RemoveAt(i--);
filterNames.Add("Nicht-Funktionäre");
} else if (e.Equals("bio", StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsOrganic);
filter.RemoveAt(i--);
filterNames.Add("Bio-Betriebe");
} else if (e.Equals("!bio", StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsOrganic);
filter.RemoveAt(i--);
filterNames.Add("Nicht-Bio-Betriebe");
} else if (e.Length >= 4 && e.Length <= 13 && "volllieferant".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsVollLieferant);
filter.RemoveAt(i--);
filterNames.Add("Volllieferanten");
} else if (e.Length >= 5 && e.Length <= 14 && e[0] == '!' && "volllieferant".StartsWith(e[1..], StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsVollLieferant);
filter.RemoveAt(i--);
filterNames.Add("Nicht-Vollieferanten");
} else if (e.Length >= 5 && e.Length <= 11 && "buchführend".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsBuchführend);
filter.RemoveAt(i--);
filterNames.Add("buchführend");
} else if (e.Length >= 6 && e.Length <= 12 && e[0] == '!' && "buchführend".StartsWith(e[1..], StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsBuchführend);
filter.RemoveAt(i--);
filterNames.Add("pauschaliert");
} else if (e.Length >= 8 && e.Length <= 12 && "pauschaliert".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsBuchführend);
filter.RemoveAt(i--);
filterNames.Add("pauschaliert");
} else if (e.Length >= 9 && e.Length <= 13 && e[0] == '!' && "pauschaliert".StartsWith(e[1..], StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsBuchführend);
filter.RemoveAt(i--);
filterNames.Add("buchführend");
} else if (e.All(char.IsAsciiDigit) && mgnr.ContainsKey(e)) {
filterMgNr.Add(int.Parse(e));
filter.RemoveAt(i--);
filterNames.Add($"MgNr. {e}");
} else if (kgs.TryGetValue(e, out var kg)) {
filterKgNr.Add(kg.KgNr);
filter.RemoveAt(i--);
filterNames.Add($"Stammgemeinde {kg.Name}");
} else if (e.StartsWith("zwst:")) {
try {
filterZwst.Add(branches.Where(b => b.Name.StartsWith(e[5..], StringComparison.CurrentCultureIgnoreCase)).Single().ZwstId);
var branch = branches.Where(b => b.Name.StartsWith(e[5..], StringComparison.CurrentCultureIgnoreCase)).Single();
filterZwst.Add(branch.ZwstId);
filter.RemoveAt(i--);
filterNames.Add($"Zweigstelle {branch.Name}");
} catch (InvalidOperationException) { }
} else if (e.StartsWith('+') && e[1..].All(char.IsAsciiDigit)) {
memberQuery = memberQuery.Where(m => m.TelephoneNumbers.Any(t => t.Number.Replace(" ", "").StartsWith(e)));
filter.RemoveAt(i--);
filterNames.Add($"Tel.-Nr. {e}");
} else if (areaComs.ContainsKey(e.ToUpper())) {
filterAreaCom.Add(e.ToUpper());
filter.RemoveAt(i--);
filterNames.Add($"Flächenbindung {e.ToUpper()}");
} else if (Validator.CheckLfbisNr(e)) {
filterLfbisNr.Add(e);
filter.RemoveAt(i--);
filterNames.Add($"Betriebsnummer {e}");
} else if (Validator.CheckUstIdNr(e.ToUpper())) {
filterUstIdNr.Add(e.ToUpper());
filter.RemoveAt(i--);
filterNames.Add($"UID {e.ToUpper()}");
} else if (e.Length > 2 && e.StartsWith('"') && e.EndsWith('"')) {
filter[i] = e[1..^1];
} else if (e.Length < 2) {
@ -202,12 +225,18 @@ namespace Elwig.Windows {
if (filterMgNr.Count > 0) memberQuery = memberQuery.Where(m => filterMgNr.Contains(m.MgNr));
if (filterKgNr.Count > 0) memberQuery = memberQuery.Where(m => m.DefaultKgNr != null && filterKgNr.Contains((int)m.DefaultKgNr));
if (filterZwst.Count > 0) memberQuery = memberQuery.Where(m => m.ZwstId != null && filterZwst.Contains(m.ZwstId));
if (filterAreaCom.Count > 0) memberQuery = memberQuery.Where(m => m.AreaCommitments.Where(c => c.YearFrom <= Utils.CurrentYear && (c.YearTo ?? int.MaxValue) >= Utils.CurrentYear).Any(c => filterAreaCom.Contains(c.VtrgId)));
if (filterAreaCom.Count > 0) memberQuery = memberQuery.Where(m => m.AreaCommitments.AsQueryable().Where(Utils.ActiveAreaCommitments()).Any(c => filterAreaCom.Contains(c.VtrgId)));
if (filterLfbisNr.Count > 0) memberQuery = memberQuery.Where(m => m.LfbisNr != null && filterLfbisNr.Contains(m.LfbisNr));
if (filterUstIdNr.Count > 0) memberQuery = memberQuery.Where(m => m.UstIdNr != null && filterUstIdNr.Contains(m.UstIdNr));
}
memberQuery = memberQuery
return (filterNames, memberQuery, filter);
}
private async Task RefreshMemberListQuery(bool updateSort = false) {
using var ctx = new AppDbContext();
var (_, memberQuery, filter) = await GetFilters(ctx);
var members = await memberQuery
.Include(m => m.Branch)
.Include(m => m.DefaultWbKg!.AtKg)
.Include(m => m.EmailAddresses)
@ -215,8 +244,8 @@ namespace Elwig.Windows {
.Include(m => m.PostalDest.AtPlz!.Ort)
.Include(m => m.PostalDest.AtPlz!.Country)
.Include(m => m.BillingAddress!.PostalDest.AtPlz!.Ort)
.Include(m => m.BillingAddress!.PostalDest.AtPlz!.Country);
List<Member> members = await memberQuery.ToListAsync();
.Include(m => m.BillingAddress!.PostalDest.AtPlz!.Country)
.ToListAsync();
if (filter.Count > 0 && members.Count > 0) {
var dict = members.AsParallel()
@ -650,39 +679,72 @@ namespace Elwig.Windows {
private async Task GenerateMemberList(int modeWho, int modeWhat) {
using var ctx = new AppDbContext();
IQueryable<Member> list;
IQueryable<Member> query;
List<string> filterNames = [];
if (modeWho == 0) {
list = ctx.Members.Where(m => m.IsActive);
query = ctx.Members.Where(m => m.IsActive);
filterNames.Add("aktive Mitglieder");
} else if (modeWho == 1) {
list = ctx.Members; // TODO
var (f, q, _) = await GetFilters(ctx);
query = q;
filterNames.AddRange(f);
} else {
list = ctx.Members;
query = ctx.Members;
}
if (Menu_List_OrderMgNr.IsChecked) {
list = list.OrderBy(m => m.MgNr);
query = query
.OrderBy(m => m.Branch!.Name)
.ThenBy(m => m.MgNr);
} else if (Menu_List_OrderName.IsChecked) {
list = list
.OrderBy(m => m.FamilyName)
query = query
.OrderBy(m => m.Branch!.Name)
.ThenBy(m => m.FamilyName)
.ThenBy(m => m.GivenName)
.ThenBy(m => m.MgNr);
} else if (Menu_List_OrderOrt.IsChecked) {
list = list
.OrderBy(m => m.DefaultWbKg!.AtKg.Name)
query = query
.OrderBy(m => m.Branch!.Name)
.ThenBy(m => m.DefaultWbKg!.AtKg.Name)
.ThenBy(m => m.FamilyName)
.ThenBy(m => m.GivenName)
.ThenBy(m => m.MgNr);
}
if (modeWhat == 0) {
} else if (modeWhat == 1) {
} else if (modeWhat == 2) {
var d = new SaveFileDialog() {
FileName = $"{Documents.MemberList.Name}.ods",
DefaultExt = "ods",
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
Title = $"{Documents.MemberList.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await MemberListData.FromQuery(query, filterNames);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = null;
}
} else {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await MemberListData.FromQuery(query, filterNames);
using var doc = new MemberList(string.Join(" / ", filterNames), data);
await doc.Generate();
if (modeWhat == 1 || App.Config.Debug) {
doc.Show();
} else if (modeWhat == 2) {
await doc.Print();
}
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = null;
}
// TODO
}
private void FocusSearchInput(object sender, RoutedEventArgs evt) {