taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 17dfee5ab0da9d8afcc61b97ec67257f1a039435
parent e55a5551fee3e4853c381525d70dbe458716ef56
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Mon, 24 Nov 2025 17:13:12 +0900

Statistics: Add tables, fixes #10645

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx | 5++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/index.tsx | 17+++++++++++------
3 files changed, 126 insertions(+), 17 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx @@ -51,7 +51,10 @@ export function OrdersChart({ return ( <Fragment> <div> - <div class="column "> + <section class="section is-size-4 hero is-hero-bar"> + {i18n.str`Order statistics`} + </section> + <div class="column "> <div class="buttons is-right"> <div class="field has-addons"> {filterFromDate && ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx @@ -19,7 +19,7 @@ * @author Martin Schanzenbach */ -import { AbsoluteTime } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amount, AmountJson, Amounts } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -29,11 +29,13 @@ import { FormProvider } from "../../../../components/form/FormProvider.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; +import { ChartDataset, Point } from "chart.js"; +import { format } from "date-fns"; export interface RevenueChartProps { currencyString: string, - chartData: any; - chartLabels: any; + chartData?: ChartDataset[]; + chartLabels: number[]; chartOptions: any; revenueRanges: string[]; onShowRange: (rangeCount: number, range?: string) => void; @@ -50,6 +52,7 @@ export function RevenueChart({ activeRange: activeRange, }: RevenueChartProps): VNode { const { i18n } = useTranslationContext(); + const [showTable, setShowTable] = useState<boolean>(false); const translateMap = new Map<string, [string, string]>([ ["hour", [i18n.str`hour`, i18n.str`hours`]], ["day", [i18n.str`day`, i18n.str`days`]], @@ -63,15 +66,31 @@ export function RevenueChart({ return ""; } return (range.rangeCount < 2) ? translateMap.get(range.range!)![0] : translateMap.get(range.range!)![1]; + }; + const formatMap = new Map<string, string>([ + ["hour", "ha"], + ["day", 'MMM d'], + ["week", 'PP'], + ["month", 'MMM yyyy'], + ["quarter", 'qqq - yyyy'], + ["year", 'yyyy'], + ]); + function getRevenueForLabelAndDataset(label: number, ds: ChartDataset) : string { + for (let d of (ds.data as (Point & AmountJson)[])) { + if (d.x === label) { + console.log(Amounts.toPretty(d)); + return Amounts.toPretty(d); + } + } + return "-"; } - const form = activeRange; return ( <Fragment> + <section class="section is-size-4 hero is-hero-bar"> + {i18n.str`Revenue statistics over the past ${activeRange.rangeCount} ${translateRange(activeRange)} for currency '${currencyString}'`} + </section> <div class="columns"> - <div class="column is-two-thirds"> - {i18n.str`Revenue statistics over the past ${activeRange.rangeCount} ${translateRange(activeRange)} for currency '${currencyString}'`} - </div> <div class="column "> <FormProvider object={form} @@ -101,11 +120,93 @@ export function RevenueChart({ </FormProvider> </div> </div> - + <div class="tabs" style={{ overflow: "inherit" }}> + <ul> + <li class={(!showTable) ? "is-active" : ""}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Show chart`} + > + <a onClick={() => {setShowTable(false) }}> + <i18n.Translate>Chart</i18n.Translate> + </a> + </div> + </li> + <li class={(showTable) ? "is-active" : ""}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Show table`} + > + <a onClick={() => {setShowTable(true) }}> + <i18n.Translate>Table</i18n.Translate> + </a> + </div> + </li> + </ul> + </div> {(chartData && chartLabels.length > 0) ? ( - <Bar data={{labels: chartLabels, datasets: chartData}} options={chartOptions}/> + (!showTable) ? ( + <Bar data={{labels: chartLabels, datasets: chartData as any}} options={chartOptions}/> + ) : ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <i18n.Translate>Table</i18n.Translate> + </p> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Start time</i18n.Translate> + </th> + {chartData!.map((d) => { + return ( + <Fragment key={d.label}> + <th> + {d.label} + </th> + </Fragment> + )})} + </tr> + </thead> + <tbody> + {chartLabels.map((l) => { + return ( + <Fragment key={l}> + <tr key="info"> + <td> + {format(new Date(l), formatMap.get(activeRange.range!)!)} + </td> + {chartData?.map((d) => { + return ( + <Fragment key={d.label}> + <td> + {getRevenueForLabelAndDataset(l, d)} + </td> + </Fragment> + );})} + </tr> + </Fragment> + ); + })} + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + ) ) : ( - <i>{i18n.str`No revenue statistics yet.`}</i> + <i>{i18n.str`No revenue statistics yet.`}</i> )} </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/index.tsx @@ -27,6 +27,8 @@ import { TalerError, assertUnreachable, StatisticsCounter, + Amount, + AmountJson, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -693,20 +695,25 @@ export default function Statistics({ if (!datasetForCurrency.has(a.currency)) { datasetForCurrency.set(a.currency, { label: stat.buckets_description, - data: Array<Point>(), + data: Array<Point & AmountJson>(), backgroundColor: datasetColor, hoverOffset: 4, }) } const amount = a.value + (a.fraction / Math.pow(10, 8)); - datasetForCurrency.get(a.currency)!.data.push({x: b.start_time.t_s * 1000, y: amount}); + const timeRangeStart = b.start_time.t_s * 1000; + datasetForCurrency.get(a.currency)!.data.push({x: timeRangeStart, y: amount, ...a}); if (currRangeFilter.rangeCount < datasetForCurrency.get(a.currency)!.data.length) { datasetForCurrency.get(a.currency)?.data.shift(); // Remove oldest entry + revenueChartLabels.get(a.currency)?.shift(); // Remove oldest entry } if (!revenueChartLabels.has(a.currency)) { revenueChartLabels.set(a.currency, []); } - revenueChartLabels.get(a.currency)?.push(b.start_time.t_s * 1000); + const labels = revenueChartLabels.get(a.currency); + if (labels?.indexOf(timeRangeStart) === -1) { + labels?.push(timeRangeStart); + } } } datasetForCurrency.forEach((value: ChartDataset, key: string) => { @@ -769,7 +776,6 @@ export default function Statistics({ <section class="section is-main-section"> <NotificationCard notification={notif} /> - <h2>{i18n.str`Revenue statistics`}</h2> <div> { (revenueCurrencies.length > 0) ? ( @@ -779,7 +785,7 @@ export default function Statistics({ tmpOpt.scales.x.time.unit = rangeFilter.get(c)?.range ?? "quarter"; return <RevenueChart currencyString={c} - chartLabels={revenueChartLabels.get(c)} + chartLabels={revenueChartLabels.get(c) ?? []} chartData={revenueChartDatasets.get(c)} chartOptions={tmpOpt as ChartOptions} activeRange={rangeFilter.get(c)!} @@ -792,7 +798,6 @@ export default function Statistics({ } </div> <hr/> - <h2>{i18n.str`Order statistics`}</h2> <OrdersChart chartLabels={orderStatsChartLabels} chartData={orderStatsChartDatasets}