Reporting: UI improvements (#5432)

This commit is contained in:
d11n 2023-11-09 10:26:00 +01:00 committed by GitHub
parent 7708084331
commit c15f02ddbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 93 additions and 46 deletions

View file

@ -2881,7 +2881,7 @@ namespace BTCPayServer.Tests
Assert.Single(paymentTypes["On-Chain"]);
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
report = await GetReport(acc, new() { ViewName = "On-Chain Wallets" });
report = await GetReport(acc, new() { ViewName = "Wallets" });
var txIdIndex = report.GetIndex("TransactionId");
var balanceIndex = report.GetIndex("BalanceChange");
Assert.Equal(2, report.Data.Count);
@ -2889,7 +2889,7 @@ namespace BTCPayServer.Tests
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
// Items sold
report = await GetReport(acc, new() { ViewName = "Products sold" });
report = await GetReport(acc, new() { ViewName = "Sales" });
var itemIndex = report.GetIndex("Product");
var countIndex = report.GetIndex("Quantity");
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())

View file

@ -9,7 +9,15 @@
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
@if (Model.Elastic)
{
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
}
else
{
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
<span>…</span>
}
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
</span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
@ -33,7 +41,7 @@
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}

View file

@ -27,7 +27,7 @@ public class OnChainWalletReportProvider : ReportProvider
private StoreRepository StoreRepository { get; }
private BTCPayNetworkProvider NetworkProvider { get; }
private WalletRepository WalletRepository { get; }
public override string Name => "On-Chain Wallets";
public override string Name => "Wallets";
ViewDefinition CreateViewDefinition()
{
return new()

View file

@ -58,7 +58,7 @@ public class PaymentsReportProvider : ReportProvider
},
new ()
{
Name = "Aggregated currency amount",
Name = "Aggregated amount",
Groups = { "Currency" },
Totals = { "Currency" },
HasGrandTotal = false,
@ -66,7 +66,7 @@ public class PaymentsReportProvider : ReportProvider
},
new ()
{
Name = "Group by Lightning Address (Currency amount)",
Name = "Group by Lightning Address",
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
Groups = { "LightningAddress", "Currency" },
Aggregates = { "CurrencyAmount" },
@ -74,7 +74,7 @@ public class PaymentsReportProvider : ReportProvider
},
new ()
{
Name = "Group by Lightning Address (Crypto amount)",
Name = "Group by Lightning Address (Crypto)",
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
Groups = { "LightningAddress", "Crypto" },
Aggregates = { "CryptoAmount" },

View file

@ -22,7 +22,7 @@ public class ProductsReportProvider : ReportProvider
private InvoiceRepository InvoiceRepository { get; }
private AppService Apps { get; }
public override string Name => "Products sold";
public override string Name => "Sales";
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
{
@ -114,7 +114,7 @@ public class ProductsReportProvider : ReportProvider
{
new ()
{
Name = "Summary by products",
Name = "Summary",
Groups = { "AppId", "Currency", "State", "Product" },
Aggregates = { "Quantity", "CurrencyAmount" },
Totals = { "State" }

View file

@ -16,7 +16,11 @@
{
@* Set a height for the responsive table container to make it work with the fixed table headers.
Details described here: thttps://uxdesign.cc/position-stuck-96c9f55d9526 *@
<style>#app .table-responsive { max-height: 80vh; }</style>
<style>
#app .table-responsive { max-height: 80vh; }
#app #charts { gap: var(--btcpay-space-l) var(--btcpay-space-xxl); }
#app #charts article { flex: 1 1 450px; }
</style>
}
<div class="sticky-header">
@ -29,9 +33,7 @@
</h2>
<div class="d-flex flex-wrap gap-3">
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake date</a>
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">
Export CSV
</button>
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">Export</button>
</div>
</div>
</div>
@ -64,35 +66,47 @@
</div>
<div id="app" v-cloak class="w-100-fixed">
<article v-for="chart in srv.charts" class="mb-5">
<h3>{{ chart.name }}</h3>
<div class="table-responsive" v-if="chart.rows.length || chart.hasGrandTotal">
<table class="table table-hover w-auto">
<thead class="bg-body">
<tr>
<th v-for="group in chart.groups">{{ titleCase(group) }}</th>
<th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg) }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in chart.rows">
<td v-for="group in row.groups" :rowspan="group.rowCount">{{ group.name }}</td>
<td v-if="row.isTotal" :colspan="row.rLevel">Total</td>
<td v-for="value in row.values" class="text-end">{{ displayValue(value) }}</td>
</tr>
<div v-if="srv.charts && srv.charts.some(hasChartData)" id="charts" class="d-flex flex-wrap mb-5">
<article v-for="chart in srv.charts" v-if="hasChartData(chart)">
<h3>{{ chart.name }}</h3>
<div class="table-responsive">
<table class="table table-hover w-auto">
<thead class="bg-body">
<tr>
<th v-for="group in chart.groups">{{ titleCase(group, true) }}</th>
<th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg, true) }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in chart.rows">
<td v-for="(group, groupIndex) in row.groups" :rowspan="group.rowCount">
<template v-if="group.name === true"><span class="text-success"><vc:icon symbol="checkmark" /></span></template>
<template v-else-if="group.name === false"><span class="text-danger"><vc:icon symbol="cross" /></span></template>
<template v-else-if="['Settled', 'Processing', 'Invalid', 'Expired', 'New', 'Pending'].includes(group.name)">
<span class="badge" :class="`badge-${group.name.toLowerCase()}`">{{ displayValue(group.name) }}</span>
</template>
<template v-else>{{ displayValue(group.name) }}</template>
</td>
<td v-if="row.isTotal" :colspan="row.rLevel">Total</td>
<td v-for="(value, columnIndex) in row.values" class="text-end">
<template v-if="chart.aggregates[columnIndex] === 'BalanceChange' && (value >= 0 || typeof value === 'object' && value.d >= 0)"><span class="text-success">{{ displayValue(value) }}</span></template>
<template v-else-if="chart.aggregates[columnIndex] === 'BalanceChange' && (value < 0 || typeof value === 'object' && value.d < 0)"><span class="text-danger">{{ displayValue(value) }}</span></template>
<template v-else>{{ displayValue(value) }}</template>
</td>
</tr>
<tr v-if="chart.hasGrandTotal">
<td :colspan="chart.groups.length">Grand Total</td>
<td v-for="value in chart.grandTotalValues" class="text-end">{{ displayValue(value) }}</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="mt-3 mb-5 text-secondary">No data</p>
</article>
</tbody>
</table>
</div>
</article>
</div>
<article v-if="srv.result.data">
<h3 id="raw-data">Raw data</h3>
<div class="table-responsive" v-if="srv.result.data.length">
<table class="table table-hover w-auto">
<table class="table table-hover">
<thead class="sticky-top bg-body">
<tr>
<th v-for="field in srv.result.fields" :class="{ 'text-end': ['integer', 'decimal', 'amount'].includes(field.type) }">
@ -109,14 +123,25 @@
</thead>
<tbody>
<tr v-for="(row, index) in srv.result.data" :key="index">
<td class="text-nowrap" v-for="(value, columnIndex) in row" :key="columnIndex" :class="{ 'text-end': ['integer', 'decimal', 'amount'].includes(srv.result.fields[columnIndex].type) }">
<td v-for="(value, columnIndex) in row" :key="columnIndex" :class="{
'text-end': ['integer', 'decimal', 'amount'].includes(srv.result.fields[columnIndex].type),
'text-center': ['boolean'].includes(srv.result.fields[columnIndex].type),
'text-nowrap': ['datetime'].includes(srv.result.fields[columnIndex].type) }">
<a :href="getInvoiceUrl(value)"
target="_blank"
v-if="srv.result.fields[columnIndex].type === 'invoice_id'">{{ displayValue(value) }}</a>
<a :href="getExplorerUrl(value, row[columnIndex-1])"
target="_blank"
rel="noreferrer noopener"
v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">{{ displayValue(value) }}</a>
<template v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" link="getExplorerUrl(value, row[columnIndex-1])" />
</template>
<template v-else-if="['Address', 'PaymentId'].includes(srv.result.fields[columnIndex].name)">
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" />
</template>
<template v-else-if="srv.result.fields[columnIndex].type === 'datetime'">{{ displayDate(value) }}</template>
<span v-else-if="srv.result.fields[columnIndex].type === 'boolean' && value === true" class="text-success"><vc:icon symbol="checkmark" /></span>
<span v-else-if="srv.result.fields[columnIndex].type === 'boolean' && value === false" class="text-danger"><vc:icon symbol="cross" /></span>
<span v-else-if="['BalanceChange'].includes(srv.result.fields[columnIndex].name) && (value >= 0 || typeof value === 'object' && value.d >= 0)" class="text-success">{{ displayValue(value) }}</span>
<span v-else-if="['BalanceChange'].includes(srv.result.fields[columnIndex].name) && (value < 0 || typeof value === 'object' && value.d < 0)" class="text-danger">{{ displayValue(value) }}</span>
<span v-else-if="['State'].includes(srv.result.fields[columnIndex].name)" class="badge" :class="`badge-${value.toLowerCase()}`">{{ displayValue(value) }}</span>
<template v-else>{{ displayValue(value) }}</template>
</td>
</tr>

View file

@ -9,6 +9,7 @@
<symbol id="checkmark" viewBox="0 0 16 16" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.7808 4.21934C13.9213 4.35997 14.0002 4.55059 14.0002 4.74934C14.0002 4.94809 13.9213 5.13871 13.7808 5.27934L6.53082 12.5293C6.3902 12.6698 6.19957 12.7487 6.00082 12.7487C5.80207 12.7487 5.61145 12.6698 5.47082 12.5293L2.22082 9.27934C2.08834 9.13717 2.01622 8.94912 2.01965 8.75482C2.02308 8.56052 2.10179 8.37513 2.2392 8.23772C2.37661 8.10031 2.562 8.02159 2.7563 8.01816C2.9506 8.01474 3.13865 8.08686 3.28082 8.21934L6.00082 10.9393L12.7208 4.21934C12.8614 4.07889 13.0521 4 13.2508 4C13.4496 4 13.6402 4.07889 13.7808 4.21934Z" fill="currentColor"/></symbol>
<symbol id="close" viewBox="0 0 16 16" fill="none"><path d="M9.38526 8.08753L15.5498 1.85558C15.9653 1.43545 15.9653 0.805252 15.5498 0.385121C15.1342 -0.0350102 14.5108 -0.0350102 14.0952 0.385121L7.93072 6.61707L1.76623 0.315098C1.35065 -0.105033 0.727273 -0.105033 0.311688 0.315098C-0.103896 0.73523 -0.103896 1.36543 0.311688 1.78556L6.47618 8.0175L0.311688 14.2495C-0.103896 14.6696 -0.103896 15.2998 0.311688 15.7199C0.519481 15.93 0.796499 16 1.07355 16C1.35061 16 1.62769 15.93 1.83548 15.7199L7.99997 9.48797L14.1645 15.7199C14.3722 15.93 14.6493 16 14.9264 16C15.2034 16 15.4805 15.93 15.6883 15.7199C16.1039 15.2998 16.1039 14.6696 15.6883 14.2495L9.38526 8.08753Z" fill="currentColor"/></symbol>
<symbol id="copy" viewBox="0 0 16 16" fill="none"><path d="M13.3333 6H7.33333C6.59695 6 6 6.59695 6 7.33333V13.3333C6 14.0697 6.59695 14.6667 7.33333 14.6667H13.3333C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333V7.33333C14.6667 6.59695 14.0697 6 13.3333 6Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.33203 10.0007H2.66536C2.31174 10.0007 1.9726 9.86018 1.72256 9.61013C1.47251 9.36008 1.33203 9.02094 1.33203 8.66732V2.66732C1.33203 2.3137 1.47251 1.97456 1.72256 1.72451C1.9726 1.47446 2.31174 1.33398 2.66536 1.33398H8.66536C9.01899 1.33398 9.35813 1.47446 9.60817 1.72451C9.85822 1.97456 9.9987 2.3137 9.9987 2.66732V3.33398" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="cross" viewBox="0 0 16 16" fill="none"><path d="m 12.130988,3.8368041 c 0.1405,0.14063 0.2194,0.33125 0.2194,0.53 0,0.19875 -0.0789,0.38937 -0.2194,0.53 0,0 -7.1796704,7.1797099 -7.2499804,7.2499599 -0.4782845,0.337104 -0.6862778,0.290768 -1.06,0 -0.3737222,-0.290768 -0.019811,-1.025771 -0.019811,-1.025771 L 11.070988,3.8368041 c 0.1406,-0.14045 0.3313,-0.21934 0.53,-0.21934 0.1988,0 0.3894,0.07889 0.53,0.21934 z" fill="currentColor"/><path d="m 12.130987,12.163196 c 0.1405,-0.14063 0.2194,-0.33125 0.2194,-0.53 0,-0.19875 -0.0789,-0.38937 -0.2194,-0.53 0,0 -7.1796692,-7.1797105 -7.2499792,-7.2499605 -0.4782845,-0.337104 -0.6862778,-0.290768 -1.06,0 -0.3737222,0.290768 -0.019811,1.025771 -0.019811,1.025771 l 7.2697902,7.2841895 c 0.1406,0.14045 0.3313,0.21934 0.53,0.21934 0.1988,0 0.3894,-0.07889 0.53,-0.21934 z" fill="currentColor"/></symbol>
<symbol id="crowdfund" viewBox="0 0 24 24" fill="none"><path d="M9.1638 12.4922C11.339 12.4922 13.1023 10.7288 13.1023 8.5537C13.1023 6.37854 11.339 4.61523 9.1638 4.61523C6.98865 4.61523 5.22534 6.37854 5.22534 8.5537C5.22534 10.7288 6.98865 12.4922 9.1638 12.4922Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.9331 18.0307C17.4965 18.0307 18.7638 16.7633 18.7638 15.1999C18.7638 13.6365 17.4965 12.3691 15.9331 12.3691C14.3697 12.3691 13.1023 13.6365 13.1023 15.1999C13.1023 16.7633 14.3697 18.0307 15.9331 18.0307Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.24067 19.3839C9.49818 19.3839 10.5176 18.3645 10.5176 17.107C10.5176 15.8495 9.49818 14.8301 8.24067 14.8301C6.98316 14.8301 5.96375 15.8495 5.96375 17.107C5.96375 18.3645 6.98316 19.3839 8.24067 19.3839Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="docs" viewBox="0 0 16 16" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 16A8 8 0 1 0 7.998-.002 8 8 0 0 0 8 16Zm-.336-8.032.773-.616c.258-.203.437-.414.546-.624a1.43 1.43 0 0 0 .172-.71c0-.32-.094-.578-.297-.758-.202-.18-.483-.273-.85-.273s-.664.094-.882.289c-.203.195-.312.46-.312.804 0 .255-.194.489-.449.47l-.728-.053a.458.458 0 0 1-.435-.4 2.521 2.521 0 0 1-.012-.244c0-.655.258-1.178.765-1.568.523-.383 1.21-.578 2.076-.578.913 0 1.616.187 2.115.57.492.374.742.905.742 1.592 0 .765-.367 1.452-1.093 2.068l-.679.57a1.375 1.375 0 0 0-.28.312.738.738 0 0 0-.071.351.36.36 0 0 1-.36.36h-.78a.5.5 0 0 1-.5-.5v-.023c0-.235.048-.43.133-.586a1.82 1.82 0 0 1 .406-.453Zm-.406 4.036a.97.97 0 0 0 .726.288c.305 0 .547-.093.734-.288a.988.988 0 0 0 .289-.734c0-.304-.094-.546-.289-.742-.187-.195-.43-.288-.734-.288a.97.97 0 0 0-.726.288 1.019 1.019 0 0 0-.28.742c0 .296.093.539.28.734Z" fill="currentColor"/></symbol>
<symbol id="donate" viewBox="0 0 16 16" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.65 14.91a.75.75 0 0 0 .7 0L8 14.26l.35.66h.02c1.33-.74 2.59-1.6 3.75-2.6C13.96 10.74 16 8.36 16 5.5 16 2.84 13.91 1 11.75 1 10.2 1 8.85 1.8 8 3.02A4.57 4.57 0 0 0 4.25 1 4.38 4.38 0 0 0 0 5.5c0 2.85 2.04 5.23 3.88 6.82a22.08 22.08 0 0 0 3.75 2.58l.02.01Z" fill="currentColor"/></symbol>

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -131,16 +131,28 @@ document.addEventListener("DOMContentLoaded", () => {
el: '#app',
data() { return { srv } },
methods: {
titleCase(str) {
const result = str.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1);
hasChartData(chart) {
return chart.rows.length || chart.hasGrandTotal;
},
displayValue
titleCase(str, shorten) {
const result = str.replace(/([A-Z])/g, " $1");
const title = result.charAt(0).toUpperCase() + result.slice(1)
return shorten && title.endsWith(' Amount') ? 'Amount' : title;
},
displayValue,
displayDate
}
});
fetchStoreReports();
});
const dtFormatter = new Intl.DateTimeFormat('default', { dateStyle: 'short', timeStyle: 'short' });
function displayDate(val) {
const date = new Date(val);
return dtFormatter.format(date);
}
function displayValue(val) {
return val && typeof val === "object" && typeof val.d === "number" ? new Decimal(val.v).toFixed(val.d) : val;
}

View file

@ -12161,6 +12161,7 @@ fieldset:disabled .btn {
*::-webkit-scrollbar {
width: var(--btcpay-scrollbar-width);
height: var(--btcpay-scrollbar-width);
}
*::-webkit-scrollbar-track {

View file

@ -687,7 +687,7 @@
#mainContent > section .w-100-fixed {
/* constrains the content to respect the maximum width and enable responsive tables */
width: calc(100vw - var(--sidebar-width) - var(--content-padding-horizontal) * 2);
width: calc(100vw - var(--sidebar-width) - var(--btcpay-scrollbar-width) - var(--content-padding-horizontal) * 2);
}
#SectionNav .nav {