PR U-12: report primitives + print CSS

Extract ReportHeader, ReportDivider, ReportSectionHeader, and
ReportFooter under Components/Pages/Reports/ and adopt them across
BalanceReport, BalanceSheetReport, ProfitLossReport, and DetailReport.
Row shapes are unchanged; the primitives just fold the divider hr
rows (30+ occurrences), section headers (8), and header/footer (4
each). ProfitLossReport / DetailReport also drop leftover
`m.Class == 3/4` magic numbers for ClassIds. Print CSS gains
`table.report tr { page-break-inside: avoid; }` so section headers
don't land at the bottom of a page alone. Net -88 lines across the
four report razor files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-07-07 08:15:00 +02:00
parent acc8b14c06
commit 1c5227ab9a
9 changed files with 210 additions and 236 deletions

View file

@ -1,4 +1,4 @@
@page "/BalanceReport" @page "/BalanceReport"
@if (balance is null) @if (balance is null)
{ {
@ -6,11 +6,8 @@
return; return;
} }
<h3>@header</h3> <ReportHeader Title="@header"/>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report"> <table class="report">
<thead> <thead>
<tr> <tr>
@ -20,16 +17,11 @@
<th class="right">Passiven</th> <th class="right">Passiven</th>
</tr> </tr>
<tr> <tr>
<th colspan="4"><hr /></th> <th colspan="4"><hr/></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <ReportSectionHeader Title="Aktiven" Colspan="4"/>
<td colspan="4" class="header">Aktiven</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach (var account in balance.ActivaAccountList.OrderBy(m => m.Number)) @foreach (var account in balance.ActivaAccountList.OrderBy(m => m.Number))
{ {
@if (account.BalanceCHF == 0) { continue; } @if (account.BalanceCHF == 0) { continue; }
@ -41,23 +33,14 @@
<td></td> <td></td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total">Total Aktiven</td> <td colspan="2" class="total">Total Aktiven</td>
<td class="right total">CHF @balance.TotalActivaCHF.ToFormattedString()</td> <td class="right total">CHF @balance.TotalActivaCHF.ToFormattedString()</td>
<td></td> <td></td>
</tr> </tr>
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td> <ReportSectionHeader Title="Passiven" Colspan="4"/>
</tr>
<tr>
<td colspan="4" class="header">Passiven</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach (var account in balance.PassivaAccountList.OrderBy(m => m.Number)) @foreach (var account in balance.PassivaAccountList.OrderBy(m => m.Number))
{ {
@if (account.BalanceCHF == 0) { continue; } @if (account.BalanceCHF == 0) { continue; }
@ -69,17 +52,13 @@
<td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td> <td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total">Total Passiven</td> <td colspan="2" class="total">Total Passiven</td>
<td></td> <td></td>
<td class="right total">@balance.TotalPassivaCHF.ToFormattedString()</td> <td class="right total">@balance.TotalPassivaCHF.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total">Total</td> <td colspan="2" class="total">Total</td>
<td class="right">@balance.TotalActivaCHF.ToFormattedString()</td> <td class="right">@balance.TotalActivaCHF.ToFormattedString()</td>
@ -95,11 +74,6 @@
<td class="right total sum">CHF @balance.TotalActivaCHF.ToFormattedString()</td> <td class="right total sum">CHF @balance.TotalActivaCHF.ToFormattedString()</td>
<td class="right total sum">CHF @((balance.TotalPassivaCHF + win).ToFormattedString())</td> <td class="right total sum">CHF @((balance.TotalPassivaCHF + win).ToFormattedString())</td>
</tr> </tr>
<tr> <ReportFooter Footer="@footer" Colspan="4"/>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,4 +1,4 @@
@page "/BalanceSheetReport" @page "/BalanceSheetReport"
@if (balanceSheet is null) @if (balanceSheet is null)
{ {
@ -6,18 +6,11 @@
return; return;
} }
<h3>@header</h3> <ReportHeader Title="@header"/>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report"> <table class="report">
<tbody> <tbody>
<tr> <ReportSectionHeader Title="Einnahmen" Colspan="3"/>
<td colspan="3" class="header">Einnahmen</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach (var account in balanceSheet.ActivaAccountList.OrderBy(m => m.Number)) @foreach (var account in balanceSheet.ActivaAccountList.OrderBy(m => m.Number))
{ {
@if (account.StartBalanceCHF == 0) { continue; } @if (account.StartBalanceCHF == 0) { continue; }
@ -38,15 +31,8 @@
<td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td> <td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="3"/>
<td colspan="3"><hr /></td> <ReportSectionHeader Title="Ausgaben" Colspan="3"/>
</tr>
<tr>
<td colspan="3" class="header">Ausgaben</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach (var account in balanceSheet.ExpensesAccountList.OrderBy(m => m.Number)) @foreach (var account in balanceSheet.ExpensesAccountList.OrderBy(m => m.Number))
{ {
@if (account.BalanceCHF == 0) { continue; } @if (account.BalanceCHF == 0) { continue; }
@ -57,9 +43,7 @@
<td class="right">@account.BalanceCHF.ToFormattedString()</td> <td class="right">@account.BalanceCHF.ToFormattedString()</td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="3"/>
<td colspan="3"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2">Total Einnahmen</td> <td colspan="2">Total Einnahmen</td>
<td class="right">@balanceSheet.ProfitCHF.ToFormattedString()</td> <td class="right">@balanceSheet.ProfitCHF.ToFormattedString()</td>
@ -72,11 +56,6 @@
<td colspan="2" class="total">Übertrag @(balanceSheet.Year + 1)</td> <td colspan="2" class="total">Übertrag @(balanceSheet.Year + 1)</td>
<td class="right total sum">CHF @win.ToFormattedString()</td> <td class="right total sum">CHF @win.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportFooter Footer="@footer"/>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,4 +1,4 @@
@page "/DetailReport" @page "/DetailReport"
@if (accounts is null || transactions is null) @if (accounts is null || transactions is null)
{ {
@ -6,11 +6,8 @@
return; return;
} }
<h3>@header</h3> <ReportHeader Title="@header"/>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report"> <table class="report">
<thead> <thead>
<tr> <tr>
@ -19,19 +16,14 @@
<th class="right">Betrag</th> <th class="right">Betrag</th>
</tr> </tr>
<tr> <tr>
<th colspan="3"><hr /></th> <th colspan="3"><hr/></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <ReportSectionHeader Title="Einkünfte" Colspan="3"/>
<td colspan="3" class="header">Einkünfte</td> @foreach (var account in accounts.Where(m => m.Class == ClassIds.Income).OrderBy(m => m.Number))
</tr> {
<tr> @if (account.Balance == 0) { continue; }
<td colspan="3"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 3).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr> <tr>
<td class="header">@account.Number</td> <td class="header">@account.Number</td>
@ -39,7 +31,7 @@
<td class="header right">@Math.Abs(account.Balance).ToFormattedString()</td> <td class="header right">@Math.Abs(account.Balance).ToFormattedString()</td>
</tr> </tr>
@foreach(var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate)) @foreach (var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate))
{ {
<tr> <tr>
<td>@trx.ValueDate.ToString("dd.MM.yyyy")</td> <td>@trx.ValueDate.ToString("dd.MM.yyyy")</td>
@ -51,31 +43,23 @@
} }
else else
{ {
<span>-</span>@trx.Value.ToFormattedString(); <span>-</span>
@trx.Value.ToFormattedString();
} }
</td> </td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="3"/>
<td colspan="3"><hr /></td> }
</tr>
}
<tr> <tr>
<td colspan="2" class="total right">Total Einkünfte</td> <td colspan="2" class="total right">Total Einkünfte</td>
<td class="right total">@profit.ToFormattedString()</td> <td class="right total">@profit.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportDivider Colspan="3"/>
<td colspan="3"><hr /></td> <ReportSectionHeader Title="Ausgaben" Colspan="3"/>
</tr> @foreach (var account in accounts.Where(m => m.Class == ClassIds.Expenses).OrderBy(m => m.Number))
<tr> {
<td colspan="3" class="header">Ausgaben</td> @if (account.Balance == 0) { continue; }
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 4).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr> <tr>
<td class="header">@account.Number</td> <td class="header">@account.Number</td>
@ -83,7 +67,7 @@
<td class="header right">@account.Balance.ToFormattedString()</td> <td class="header right">@account.Balance.ToFormattedString()</td>
</tr> </tr>
@foreach(var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate)) @foreach (var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate))
{ {
<tr> <tr>
<td>@trx.ValueDate.ToString("dd.MM.yyyy")</td> <td>@trx.ValueDate.ToString("dd.MM.yyyy")</td>
@ -95,24 +79,18 @@
} }
else else
{ {
<span>-</span>@trx.Value.ToFormattedString(); <span>-</span>
@trx.Value.ToFormattedString();
} }
</td> </td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="3"/>
<td colspan="3"><hr /></td> }
</tr>
}
<tr> <tr>
<td colspan="2" class="total right">Total Ausgaben</td> <td colspan="2" class="total right">Total Ausgaben</td>
<td class="right total">@loss.ToFormattedString()</td> <td class="right total">@loss.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportFooter Footer="@footer"/>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,4 +1,4 @@
@page "/ProfitLossReport" @page "/ProfitLossReport"
@if (accounts is null) @if (accounts is null)
{ {
@ -6,11 +6,8 @@
return; return;
} }
<h3>@header</h3> <ReportHeader Title="@header"/>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report"> <table class="report">
<thead> <thead>
<tr> <tr>
@ -20,19 +17,14 @@
<th class="right">Ertrag</th> <th class="right">Ertrag</th>
</tr> </tr>
<tr> <tr>
<th colspan="4"><hr /></th> <th colspan="4"><hr/></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <ReportSectionHeader Title="Einkünfte" Colspan="4"/>
<td colspan="4" class="header">Einkünfte</td> @foreach (var account in accounts.Where(m => m.Class == ClassIds.Income).OrderBy(m => m.Number))
</tr> {
<tr> @if (account.Balance == 0) { continue; }
<td colspan="4"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 3).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr> <tr>
<td>@account.Number</td> <td>@account.Number</td>
@ -40,27 +32,18 @@
<td></td> <td></td>
<td class="right">@Math.Abs(account.Balance).ToFormattedString()</td> <td class="right">@Math.Abs(account.Balance).ToFormattedString()</td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total">Total Einkünfte</td> <td colspan="2" class="total">Total Einkünfte</td>
<td></td> <td></td>
<td class="right total">@profit.ToFormattedString()</td> <td class="right total">@profit.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td> <ReportSectionHeader Title="Ausgaben" Colspan="4"/>
</tr> @foreach (var account in accounts.Where(m => m.Class == ClassIds.Expenses).OrderBy(m => m.Number))
<tr> {
<td colspan="4" class="header">Ausgaben</td> @if (account.Balance == 0) { continue; }
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 4).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr> <tr>
<td>@account.Number</td> <td>@account.Number</td>
@ -68,18 +51,14 @@
<td class="right">@Math.Abs(account.Balance).ToFormattedString()</td> <td class="right">@Math.Abs(account.Balance).ToFormattedString()</td>
<td></td> <td></td>
</tr> </tr>
} }
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total">Total Ausgaben</td> <td colspan="2" class="total">Total Ausgaben</td>
<td class="right total">@loss.ToFormattedString()</td> <td class="right total">@loss.ToFormattedString()</td>
<td></td> <td></td>
</tr> </tr>
<tr> <ReportDivider Colspan="4"/>
<td colspan="4"><hr /></td>
</tr>
<tr> <tr>
<td colspan="2" class="total"></td> <td colspan="2" class="total"></td>
<td class="right total">@loss.ToFormattedString()</td> <td class="right total">@loss.ToFormattedString()</td>
@ -95,11 +74,6 @@
<td class="right total sum">@profit.ToFormattedString()</td> <td class="right total sum">@profit.ToFormattedString()</td>
<td class="right total sum">@profit.ToFormattedString()</td> <td class="right total sum">@profit.ToFormattedString()</td>
</tr> </tr>
<tr> <ReportFooter Footer="@footer" Colspan="4"/>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -0,0 +1,11 @@
@* Full-width table divider — <tr><td colspan="N"><hr/></td></tr>.
Callers pass their table's column count. *@
<tr>
<td colspan="@Colspan"><hr/></td>
</tr>
@code {
[Parameter, EditorRequired]
public int Colspan { get; set; }
}

View file

@ -0,0 +1,19 @@
@* Report tail — spacer row + italic footer line ("Stand: dd.MM.yyyy").
Note: the existing code used colspan="4" here even in reports whose
table only has 3 columns; preserved as-is so the visual is unchanged.
Callers can override via the Colspan parameter. *@
<tr>
<td colspan="@Colspan" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="@Colspan" class="footer">@Footer</td>
</tr>
@code {
[Parameter, EditorRequired]
public string Footer { get; set; } = "";
[Parameter]
public int Colspan { get; set; } = 4;
}

View file

@ -0,0 +1,14 @@
@* Shared banner used at the top of every printable report.
H3 + browser print trigger + spacer. The Print link is hidden in the
print stylesheet via .hidePrint. *@
<h3>@Title</h3>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br/>
<br/>
@code {
[Parameter, EditorRequired]
public string Title { get; set; } = "";
}

View file

@ -0,0 +1,18 @@
@* Section header + divider pair — "Aktiven", "Passiven", "Einkünfte",
"Ausgaben", "Einnahmen" etc. Wraps two <tr>s so the caller drops it
into <tbody> as a single element. *@
<tr>
<td colspan="@Colspan" class="header">@Title</td>
</tr>
<tr>
<td colspan="@Colspan"><hr/></td>
</tr>
@code {
[Parameter, EditorRequired]
public string Title { get; set; } = "";
[Parameter, EditorRequired]
public int Colspan { get; set; }
}

View file

@ -63,6 +63,13 @@ h1:focus {
.hidePrint { .hidePrint {
display: none !important; display: none !important;
} }
/* Keep report rows intact across page breaks. Without this a row could
be split by the printer between an amount and its label, or between a
section header and its first data row. */
table.report tr {
page-break-inside: avoid;
}
} }
table.report table.report