[#30] Documents: Add WineQualityStatistics
This commit is contained in:
@ -86,6 +86,8 @@ namespace Elwig.Documents {
|
|||||||
name = "MemberDataSheet";
|
name = "MemberDataSheet";
|
||||||
} else if (this is MemberList) {
|
} else if (this is MemberList) {
|
||||||
name = "MemberList";
|
name = "MemberList";
|
||||||
|
} else if (this is WineQualityStatistics) {
|
||||||
|
name = "WineQualityStatistics";
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationException("Invalid document object");
|
throw new InvalidOperationException("Invalid document object");
|
||||||
}
|
}
|
||||||
|
26
Elwig/Documents/WineQualityStatistics.cs
Normal file
26
Elwig/Documents/WineQualityStatistics.cs
Normal file
@ -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<string, string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
Elwig/Documents/WineQualityStatistics.cshtml
Normal file
74
Elwig/Documents/WineQualityStatistics.cshtml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
@using RazorLight
|
||||||
|
@inherits TemplatePage<Elwig.Documents.WineQualityStatistics>
|
||||||
|
@model Elwig.Documents.WineQualityStatistics
|
||||||
|
@{ Layout = "Document"; }
|
||||||
|
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\WineQualityStatistics.css"/>
|
||||||
|
<main>
|
||||||
|
<h1>Qualitätsstatistik</h1>
|
||||||
|
<h2>@Model.Filter</h2>
|
||||||
|
@foreach (var sec in Model.Data.Sections) {
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 25%;"/>
|
||||||
|
<col style="width: 25%;"/>
|
||||||
|
<col style="width: 25%;"/>
|
||||||
|
<col style="width: 25%;"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="4" class="header @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
|
||||||
|
<h3>@sec.Name</h3>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
@foreach (var qualIds in Model.QualIds) {
|
||||||
|
<td class="container">
|
||||||
|
<div class="row">
|
||||||
|
<span class="units">[°Oe]</span>
|
||||||
|
<span class="units">[kg]</span>
|
||||||
|
</div>
|
||||||
|
@foreach (var qualId in qualIds) {
|
||||||
|
<h4>@(Model.QualityLevels.GetValueOrDefault(qualId, qualId))</h4>
|
||||||
|
@foreach (var (oe, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(int, int)>())) {
|
||||||
|
<div class="row">
|
||||||
|
<span class="oe">@oe</span>
|
||||||
|
<span class="weight">@($"{weight:N0}")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
@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;
|
||||||
|
<td class="container bold">
|
||||||
|
<div class="row">
|
||||||
|
<span class="oe">@(weight == 0 ? "" : $"{oe:N0}")</span>
|
||||||
|
<span class="weight">@($"{weight:N0}")</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
@{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
<td colspan="4" class="container bold footer @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
|
||||||
|
<div class="row" style="width: 24%; margin-left: 76%;">
|
||||||
|
<span class="oe">@(totalWeight == 0 ? "" : $"{totalOe:N0}")</span>
|
||||||
|
<span class="weight">@($"{totalWeight:N0}")</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</main>
|
93
Elwig/Documents/WineQualityStatistics.css
Normal file
93
Elwig/Documents/WineQualityStatistics.css
Normal file
@ -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;
|
||||||
|
}
|
105
Elwig/Models/Dtos/WineQualityStatisticsData.cs
Normal file
105
Elwig/Models/Dtos/WineQualityStatisticsData.cs
Normal file
@ -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<string, (int Oe, int Weight)[]> Data);
|
||||||
|
|
||||||
|
public QualitySection[] Sections;
|
||||||
|
|
||||||
|
public WineQualityStatisticsData(QualitySection[] sections) {
|
||||||
|
Sections = sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QualitySection[] GetQualitySections(IEnumerable<QualityRow> rows) {
|
||||||
|
var data = new List<QualitySection>();
|
||||||
|
var currentQual = new Dictionary<int, int>();
|
||||||
|
var current = new Dictionary<string, (int, int)[]>();
|
||||||
|
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<WineQualityStatisticsData> FromQuery(IQueryable<DeliveryPart> 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -76,16 +76,12 @@
|
|||||||
Click="Menu_DeliveryJournal_PrintToday_Click" InputGestureText="Strg+J"/>
|
Click="Menu_DeliveryJournal_PrintToday_Click" InputGestureText="Strg+J"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="Qualitätsstatistik">
|
<MenuItem Header="Qualitätsstatistik">
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_SaveFilters" Header="...aus Filtern speichern (Excel)"
|
<MenuItem x:Name="Menu_WineQualityStatistics_ShowFilters" Header="...aus Filtern anzeigen"
|
||||||
Click="Menu_WineQualityStatistics_SaveFilters_Click"/>
|
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_ShowFilters" Header="...aus Filtern anzeigen (PDF)"
|
|
||||||
Click="Menu_WineQualityStatistics_ShowFilters_Click"/>
|
Click="Menu_WineQualityStatistics_ShowFilters_Click"/>
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_PrintFilters" Header="...aus Filtern drucken"
|
<MenuItem x:Name="Menu_WineQualityStatistics_PrintFilters" Header="...aus Filtern drucken"
|
||||||
Click="Menu_WineQualityStatistics_PrintFilters_Click"/>
|
Click="Menu_WineQualityStatistics_PrintFilters_Click"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_SaveToday" Header="...von heute speichern (Excel)"
|
<MenuItem x:Name="Menu_WineQualityStatistics_ShowToday" Header="...von heute anzeigen"
|
||||||
Click="Menu_WineQualityStatistics_SaveToday_Click"/>
|
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_ShowToday" Header="...von heute anzeigen (PDF)"
|
|
||||||
Click="Menu_WineQualityStatistics_ShowToday_Click"/>
|
Click="Menu_WineQualityStatistics_ShowToday_Click"/>
|
||||||
<MenuItem x:Name="Menu_WineQualityStatistics_PrintToday" Header="...von heute drucken"
|
<MenuItem x:Name="Menu_WineQualityStatistics_PrintToday" Header="...von heute drucken"
|
||||||
Click="Menu_WineQualityStatistics_PrintToday_Click" InputGestureText="Strg+Q"/>
|
Click="Menu_WineQualityStatistics_PrintToday_Click" InputGestureText="Strg+Q"/>
|
||||||
|
@ -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) {
|
private async void Menu_WineQualityStatistics_ShowToday_Click(object sender, RoutedEventArgs evt) {
|
||||||
await GenerateWineQualityStatistics(1, 1);
|
await GenerateWineQualityStatistics(1, 1);
|
||||||
}
|
}
|
||||||
@ -294,10 +290,6 @@ namespace Elwig.Windows {
|
|||||||
await GenerateWineQualityStatistics(1, 2);
|
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) {
|
private async void Menu_WineQualityStatistics_ShowFilters_Click(object sender, RoutedEventArgs evt) {
|
||||||
await GenerateWineQualityStatistics(0, 1);
|
await GenerateWineQualityStatistics(0, 1);
|
||||||
}
|
}
|
||||||
@ -307,7 +299,34 @@ namespace Elwig.Windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenerateWineQualityStatistics(int modeWho, int modeWhat) {
|
private async Task GenerateWineQualityStatistics(int modeWho, int modeWhat) {
|
||||||
// TODO
|
using var ctx = new AppDbContext();
|
||||||
|
IQueryable<DeliveryPart> query;
|
||||||
|
List<string> 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) {
|
private void Menu_Settings_EnableFreeEditing_Checked(object sender, RoutedEventArgs evt) {
|
||||||
|
Reference in New Issue
Block a user