diff --git a/Elwig/Documents/Document.cs b/Elwig/Documents/Document.cs index d80334f..340df28 100644 --- a/Elwig/Documents/Document.cs +++ b/Elwig/Documents/Document.cs @@ -86,6 +86,8 @@ namespace Elwig.Documents { name = "MemberDataSheet"; } else if (this is MemberList) { name = "MemberList"; + } else if (this is WineQualityStatistics) { + name = "WineQualityStatistics"; } else { throw new InvalidOperationException("Invalid document object"); } diff --git a/Elwig/Documents/WineQualityStatistics.cs b/Elwig/Documents/WineQualityStatistics.cs new file mode 100644 index 0000000..cf18723 --- /dev/null +++ b/Elwig/Documents/WineQualityStatistics.cs @@ -0,0 +1,26 @@ +using Elwig.Models.Dtos; +using System.Collections.Generic; + +namespace Elwig.Documents { + public class WineQualityStatistics : Document { + + public new static string Name => "Qualitätsstatistik"; + + public readonly string[][] QualIds = [["WEI"], ["RSW", "LDW"], ["QUW"], ["KAB"]]; + public readonly Dictionary QualityLevels = new() { + ["WEI"] = "Wein", + ["RSW"] = "Rebsortenwein", + ["LDW"] = "Landwein", + ["QUW"] = "Qualitätswein", + ["KAB"] = "Kabinett", + }; + + public string Filter; + public WineQualityStatisticsData Data; + + public WineQualityStatistics(string filter, WineQualityStatisticsData data) : base($"{Name} {filter}") { + Filter = filter; + Data = data; + } + } +} diff --git a/Elwig/Documents/WineQualityStatistics.cshtml b/Elwig/Documents/WineQualityStatistics.cshtml new file mode 100644 index 0000000..9ec508d --- /dev/null +++ b/Elwig/Documents/WineQualityStatistics.cshtml @@ -0,0 +1,74 @@ +@using RazorLight +@inherits TemplatePage +@model Elwig.Documents.WineQualityStatistics +@{ Layout = "Document"; } + +
+

Qualitätsstatistik

+

@Model.Filter

+ @foreach (var sec in Model.Data.Sections) { + + + + + + + + + + + + + + + @foreach (var qualIds in Model.QualIds) { + + } + + + @foreach (var qualIds in Model.QualIds) { + var quals = qualIds.Select(q => sec.Data.GetValueOrDefault(q, Array.Empty<(int, int)>())); + var weight = quals.Sum(q => q.Sum(kv => kv.Item2)); + var oe = quals.Sum(q => q.Sum(kv => (double)kv.Item1 * kv.Item2)) / weight; + + } + + + + + @{ + var totalWeight = sec.Data.Values.Sum(q => q.Sum(kv => kv.Weight)); + var totalOe = sec.Data.Values.Sum(q => q.Sum(kv => (double)kv.Oe * kv.Weight)) / totalWeight; + } + + + +
+

@sec.Name

+
+
+ [°Oe] + [kg] +
+ @foreach (var qualId in qualIds) { +

@(Model.QualityLevels.GetValueOrDefault(qualId, qualId))

+ @foreach (var (oe, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(int, int)>())) { +
+ @oe + @($"{weight:N0}") +
+ } + } +
+
+ @(weight == 0 ? "" : $"{oe:N0}") + @($"{weight:N0}") +
+
+ } +
diff --git a/Elwig/Documents/WineQualityStatistics.css b/Elwig/Documents/WineQualityStatistics.css new file mode 100644 index 0000000..1fe5451 --- /dev/null +++ b/Elwig/Documents/WineQualityStatistics.css @@ -0,0 +1,93 @@ + +h1 { + text-align: center; + font-size: 24pt; + margin-top: 10mm; + margin-bottom: 2mm; +} + +h2 { + text-align: center; + font-size: 14pt; + margin-top: 2mm; +} + +h3 { + font-weight: bold; + font-size: 14pt; + margin: 0; + text-align: left; +} + +h4 { + font-weight: bold; + font-style: italic; + font-size: 12pt; + margin: 0; + text-align: center; + margin: 2mm 0 2mm 0; +} + +.row:first-child { margin-top: 0.5mm; } +.row:last-child { margin-bottom: 0.5mm; } + +.bold { + font-weight: bold; +} + +table { + margin-top: 10mm; + break-inside: avoid; +} + +table th, +table td { + border: 0.5pt solid black; + vertical-align: top !important; +} + +table .header { + padding: 1mm 2mm; +} + +table .header, +table .footer { + background-color: #E0E0E0; +} + +table .header.red, +table .footer.red { + background-color: #FFC0C0; +} + +table .header.green, +table .footer.green { + background-color: #C0FFC0; +} + +.row { + display: flex; + width: 100%; + font-size: 10pt; +} + +.row span { + flex: 10mm 1 1; + display: block; + padding: 0 2mm; +} + +.row .units { + text-align: center; + font-size: 8pt; + font-style: italic; + padding: 1mm; +} + +.oe { + text-align: center; +} + +.weight { + text-align: right; +} diff --git a/Elwig/Models/Dtos/WineQualityStatisticsData.cs b/Elwig/Models/Dtos/WineQualityStatisticsData.cs new file mode 100644 index 0000000..b76b19b --- /dev/null +++ b/Elwig/Models/Dtos/WineQualityStatisticsData.cs @@ -0,0 +1,105 @@ +using Elwig.Models.Entities; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Elwig.Models.Dtos { + public class WineQualityStatisticsData { + + public record struct QualityRow(string? Variety, string? Attribute, string? Cultivation, string? Type, string QualId, int Oe, int Weight); + public record struct QualitySection(string Name, string? Type, Dictionary Data); + + public QualitySection[] Sections; + + public WineQualityStatisticsData(QualitySection[] sections) { + Sections = sections; + } + + private static QualitySection[] GetQualitySections(IEnumerable rows) { + var data = new List(); + var currentQual = new Dictionary(); + var current = new Dictionary(); + string? lastSection = null; + string? lastType = null; + string? lastQual = null; + foreach (var row in rows) { + var sec = $"{row.Variety ?? (row.Type == "R" ? "Rotweinsorten" : row.Type == "W" ? "Weißweinsorten" : "Gesamt")}" + + $"{(row.Attribute != null ? " / " : "")}{row.Attribute}" + + $"{(row.Cultivation != null ? " / " : "")}{row.Cultivation}"; + if (lastQual != null && lastQual != row.QualId) { + current[lastQual] = currentQual.Select(kv => (kv.Key, kv.Value)).ToArray(); + currentQual.Clear(); + } + if (lastSection != null && lastSection != sec) { + if (!current.ContainsKey(lastQual!)) { + current[lastQual!] = currentQual.Select(kv => (kv.Key, kv.Value)).ToArray(); + currentQual.Clear(); + } + data.Add(new(lastSection, lastType, current)); + current = []; + currentQual.Clear(); + } + currentQual[row.Oe] = row.Weight; + lastSection = sec; + lastType = row.Type; + lastQual = row.QualId; + } + if (lastQual != null) { + current[lastQual] = currentQual.Select(kv => (kv.Key, kv.Value)).ToArray(); + currentQual.Clear(); + } + if (lastSection != null) { + data.Add(new(lastSection, lastType, current)); + current = []; + currentQual.Clear(); + } + return [.. data]; + } + + public static async Task FromQuery(IQueryable query) { + var rows = (await query + .GroupBy(p => new { + p.Variety.Type, + Variety = p.Variety.Name, + Attribute = p.Attribute!.Name, + Cultivation = p.Cultivation!.Name, + p.QualId, + Oe = (int)Math.Round(p.Kmw * (4.54 + 0.022 * p.Kmw), 0), + }, (k, g) => new { Key = k, Weight = g.Sum(p => p.Weight) }) + .OrderBy(g => g.Key.Variety) + .ThenBy(g => g.Key.Attribute) + .ThenBy(g => g.Key.Cultivation) + .ThenBy(g => g.Key.QualId) + .ThenBy(g => g.Key.Oe) + .ToListAsync()) + .Select(r => new QualityRow(r.Key.Variety, r.Key.Attribute, r.Key.Cultivation, r.Key.Type, r.Key.QualId, r.Key.Oe, r.Weight)) + .ToList(); + + var data = GetQualitySections(rows); + if (data.Length <= 1) + return new(data); + + var typeRows = rows + .GroupBy(s => new { s.Type, s.QualId, s.Oe }, (k, g) => new QualityRow(null, null, null, k.Type, k.QualId, k.Oe, g.Sum(p => p.Weight))) + .OrderBy(g => g.Type) + .ThenBy(g => g.QualId) + .ThenBy(g => g.Oe) + .ToList(); + var typeData = GetQualitySections(typeRows); + if (typeData.Length <= 1) + return new([.. typeData, .. data]); + + var totalRows = rows + .GroupBy(s => new { s.QualId, s.Oe }, (k, g) => new QualityRow(null, null, null, null, k.QualId, k.Oe, g.Sum(p => p.Weight))) + .OrderBy(g => g.QualId) + .ThenBy(g => g.Oe) + .ToList(); + var totalData = GetQualitySections(totalRows); + return new([.. totalData, .. typeData, .. data]); + } + } +} diff --git a/Elwig/Windows/DeliveryAdminWindow.xaml b/Elwig/Windows/DeliveryAdminWindow.xaml index c564e38..54573bc 100644 --- a/Elwig/Windows/DeliveryAdminWindow.xaml +++ b/Elwig/Windows/DeliveryAdminWindow.xaml @@ -76,16 +76,12 @@ Click="Menu_DeliveryJournal_PrintToday_Click" InputGestureText="Strg+J"/> - - - - diff --git a/Elwig/Windows/DeliveryAdminWindow.xaml.cs b/Elwig/Windows/DeliveryAdminWindow.xaml.cs index 8c96b29..0293833 100644 --- a/Elwig/Windows/DeliveryAdminWindow.xaml.cs +++ b/Elwig/Windows/DeliveryAdminWindow.xaml.cs @@ -282,10 +282,6 @@ namespace Elwig.Windows { } } - private async void Menu_WineQualityStatistics_SaveToday_Click(object sender, RoutedEventArgs evt) { - await GenerateWineQualityStatistics(1, 0); - } - private async void Menu_WineQualityStatistics_ShowToday_Click(object sender, RoutedEventArgs evt) { await GenerateWineQualityStatistics(1, 1); } @@ -294,10 +290,6 @@ namespace Elwig.Windows { await GenerateWineQualityStatistics(1, 2); } - private async void Menu_WineQualityStatistics_SaveFilters_Click(object sender, RoutedEventArgs evt) { - await GenerateWineQualityStatistics(0, 0); - } - private async void Menu_WineQualityStatistics_ShowFilters_Click(object sender, RoutedEventArgs evt) { await GenerateWineQualityStatistics(0, 1); } @@ -307,7 +299,34 @@ namespace Elwig.Windows { } private async Task GenerateWineQualityStatistics(int modeWho, int modeWhat) { - // TODO + using var ctx = new AppDbContext(); + IQueryable query; + List filterNames = []; + if (modeWho == 0) { + var (f, _, q, _, _) = await GetFilters(ctx); + query = q; + filterNames.AddRange(f); + } else { + var date = $"{Utils.Today:yyyy-MM-dd}"; + query = ctx.DeliveryParts + .Where(p => p.Delivery.DateString == date); + filterNames.Add($"{Utils.Today:dd.MM.yyyy}"); + } + + Mouse.OverrideCursor = Cursors.AppStarting; + try { + var data = await WineQualityStatisticsData.FromQuery(query); + using var doc = new WineQualityStatistics(string.Join(" / ", filterNames), data); + await doc.Generate(); + if (modeWhat == 2 && !App.Config.Debug) { + await doc.Print(); + } else { + doc.Show(); + } + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; } private void Menu_Settings_EnableFreeEditing_Checked(object sender, RoutedEventArgs evt) {