diff --git a/Elwig/Documents/Document.Table.css b/Elwig/Documents/Document.Table.css index b06ceab..53f1d90 100644 --- a/Elwig/Documents/Document.Table.css +++ b/Elwig/Documents/Document.Table.css @@ -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; } diff --git a/Elwig/Documents/Document.cs b/Elwig/Documents/Document.cs index 41ba537..d80334f 100644 --- a/Elwig/Documents/Document.cs +++ b/Elwig/Documents/Document.cs @@ -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"); } diff --git a/Elwig/Documents/MemberList.cs b/Elwig/Documents/MemberList.cs new file mode 100644 index 0000000..a7a83c5 --- /dev/null +++ b/Elwig/Documents/MemberList.cs @@ -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 Members; + + public MemberList(string filter, IEnumerable members) : base(Name) { + Filter = filter; + Members = members; + } + + public MemberList(string filter, MemberListData data) : + this(filter, data.Rows) { + } + } +} diff --git a/Elwig/Documents/MemberList.cshtml b/Elwig/Documents/MemberList.cshtml new file mode 100644 index 0000000..57984a5 --- /dev/null +++ b/Elwig/Documents/MemberList.cshtml @@ -0,0 +1,72 @@ +@using RazorLight +@inherits TemplatePage +@model Elwig.Documents.MemberList +@{ Layout = "Document"; } + +
+

Mitgliederliste

+

@Model.Filter

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @{ + string? lastBranch = Model.Members.Select(m => m.Branch).Distinct().Count() == 1 ? null : ""; + } + @foreach (var m in Model.Members) { + if (lastBranch != null && m.Branch != lastBranch) { + + + + + lastBranch = m.Branch; + } + + + + + + + + + + + + if (m.BillingName != null) { + + + + + + + + } + } + +
Nr.NameAdressePLZOrtBetr.-Nr.GAStamm-KGGeb. Fl.
[m²]
@m.Branch
@m.MgNr@m.Name.Replace('ß', 'ẞ').ToUpper() @m.GivenName@m.Address@m.Plz@m.Locality@m.LfbisNr@m.BusinessShares@m.DefaultKg@($"{m.AreaCommitment:N0}")
@m.BillingName@m.BillingAddress@m.BillingPlz@m.BillingLocality
+
diff --git a/Elwig/Documents/MemberList.css b/Elwig/Documents/MemberList.css new file mode 100644 index 0000000..f0d3b56 --- /dev/null +++ b/Elwig/Documents/MemberList.css @@ -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; +} diff --git a/Elwig/Helpers/Utils.cs b/Elwig/Helpers/Utils.cs index f19059a..2c5fd27 100644 --- a/Elwig/Helpers/Utils.cs +++ b/Elwig/Helpers/Utils.cs @@ -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> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear); + public static Expression> ActiveAreaCommitments(int yearTo) => + c => (c.YearFrom <= yearTo) && (c.YearTo == null || c.YearTo >= yearTo); + public static IQueryable ActiveAreaCommitments(IQueryable query) => ActiveAreaCommitments(query, CurrentYear); - public static IQueryable ActiveAreaCommitments(IQueryable query, int yearTo) { - return query.Where(c => (c.YearFrom <= CurrentYear) && (c.YearTo == null || c.YearTo >= yearTo)); - } + public static IQueryable ActiveAreaCommitments(IQueryable query, int yearTo) => + query.Where(ActiveAreaCommitments(yearTo)); public static IEnumerable ActiveAreaCommitments(IEnumerable query) => ActiveAreaCommitments(query, CurrentYear); - public static IEnumerable ActiveAreaCommitments(IEnumerable query, int yearTo) { - return query.Where(c => (c.YearFrom <= CurrentYear) && (c.YearTo == null || c.YearTo >= yearTo)); - } + public static IEnumerable ActiveAreaCommitments(IEnumerable query, int yearTo) => + query.Where(c => ActiveAreaCommitments(yearTo).Invoke(c)); } } diff --git a/Elwig/Models/Dtos/DataTable.cs b/Elwig/Models/Dtos/DataTable.cs index 62ea339..b4b3285 100644 --- a/Elwig/Models/Dtos/DataTable.cs +++ b/Elwig/Models/Dtos/DataTable.cs @@ -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 rows, IEnumerable<(string, string, string?, int?)>? colNames = null) : + this(name, fullName, rows, colNames) { + Subtitle = subtitle; + } + public DataTable(string name, string fullName, IEnumerable rows, IEnumerable<(string, string, string?, int?)>? colNames = null) { _fields = typeof(T).GetFields(); _properties = typeof(T).GetProperties(); diff --git a/Elwig/Models/Dtos/MemberListData.cs b/Elwig/Models/Dtos/MemberListData.cs new file mode 100644 index 0000000..97522d5 --- /dev/null +++ b/Elwig/Models/Dtos/MemberListData.cs @@ -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 { + + 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 rows, List filterNames) : + base(MemberList.Name, MemberList.Name, string.Join(" / ", filterNames), rows, FieldNames) { + } + + public static async Task FromQuery(IQueryable query, List 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; + } + } +} diff --git a/Elwig/Models/Entities/Member.cs b/Elwig/Models/Entities/Member.cs index 1912742..52b82f8 100644 --- a/Elwig/Models/Entities/Member.cs +++ b/Elwig/Models/Entities/Member.cs @@ -157,7 +157,7 @@ namespace Elwig.Models.Entities { public virtual ICollection AreaCommitments { get; private set; } = null!; public IQueryable 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))] diff --git a/Elwig/Windows/MemberAdminWindow.xaml.cs b/Elwig/Windows/MemberAdminWindow.xaml.cs index c57f09d..b343973 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml.cs +++ b/Elwig/Windows/MemberAdminWindow.xaml.cs @@ -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, IQueryable, List)> GetFilters(AppDbContext ctx) { + List filterNames = []; IQueryable 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(); var filterZwst = new List(); @@ -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 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 list; + IQueryable query; + List 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) {