@dashadmin/dash-reports 1.3.72 → 1.3.74-srcfree.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/ReportFilters.d.ts +9 -0
  2. package/dist/ReportFiltersDrawer.d.ts +12 -0
  3. package/dist/ReportPage.d.ts +9 -0
  4. package/dist/ReportsDashboard.d.ts +7 -0
  5. package/dist/ReportsIndex.d.ts +7 -0
  6. package/dist/ReportsModule.d.ts +7 -0
  7. package/dist/TenantDashboard.d.ts +12 -0
  8. package/dist/adapters/mui/BarWidget.d.ts +3 -0
  9. package/dist/adapters/mui/DoughnutWidget.d.ts +3 -0
  10. package/dist/adapters/mui/FunnelWidget.d.ts +3 -0
  11. package/dist/adapters/mui/KpiWidget.d.ts +3 -0
  12. package/dist/adapters/mui/LineWidget.d.ts +3 -0
  13. package/dist/adapters/mui/PieWidget.d.ts +3 -0
  14. package/dist/adapters/mui/PreviewWidget.d.ts +10 -0
  15. package/dist/adapters/mui/TableWidget.d.ts +16 -0
  16. package/dist/adapters/mui/chartSetup.d.ts +2 -0
  17. package/dist/adapters/mui/chartTheme.d.ts +71 -0
  18. package/dist/adapters/mui/index.d.ts +12 -0
  19. package/dist/adapters/mui/palette.d.ts +9 -0
  20. package/dist/adapters/mui/useChartData.d.ts +14 -0
  21. package/dist/core/formatBucket.d.ts +4 -0
  22. package/dist/core/label.d.ts +3 -0
  23. package/dist/core/queryParams.d.ts +3 -0
  24. package/dist/core/types.d.ts +94 -0
  25. package/dist/core/useDashboard.d.ts +50 -0
  26. package/dist/core/useReportExport.d.ts +7 -0
  27. package/dist/core/useReportList.d.ts +7 -0
  28. package/dist/core/useReportPreview.d.ts +7 -0
  29. package/dist/core/useReportSeries.d.ts +8 -0
  30. package/dist/core/useReportSpec.d.ts +9 -0
  31. package/dist/core/useReportTable.d.ts +10 -0
  32. package/dist/core/visualizationLayout.d.ts +13 -0
  33. package/dist/core/visualizationRegistry.d.ts +14 -0
  34. package/dist/createReportsResource.d.ts +40 -0
  35. package/dist/i18n/en.d.ts +83 -0
  36. package/dist/i18n/es.d.ts +83 -0
  37. package/{src/index.tsx → dist/index.d.ts} +3 -31
  38. package/package.json +13 -10
  39. package/src/ReportFilters.tsx +0 -145
  40. package/src/ReportFiltersDrawer.tsx +0 -73
  41. package/src/ReportPage.tsx +0 -280
  42. package/src/ReportsDashboard.tsx +0 -61
  43. package/src/ReportsIndex.tsx +0 -113
  44. package/src/ReportsModule.tsx +0 -42
  45. package/src/TenantDashboard.tsx +0 -290
  46. package/src/adapters/mui/BarWidget.tsx +0 -66
  47. package/src/adapters/mui/DoughnutWidget.tsx +0 -112
  48. package/src/adapters/mui/FunnelWidget.tsx +0 -108
  49. package/src/adapters/mui/KpiWidget.tsx +0 -123
  50. package/src/adapters/mui/LineWidget.tsx +0 -70
  51. package/src/adapters/mui/PieWidget.tsx +0 -16
  52. package/src/adapters/mui/PreviewWidget.tsx +0 -123
  53. package/src/adapters/mui/TableWidget.tsx +0 -141
  54. package/src/adapters/mui/chartSetup.ts +0 -53
  55. package/src/adapters/mui/chartTheme.ts +0 -84
  56. package/src/adapters/mui/index.ts +0 -29
  57. package/src/adapters/mui/palette.ts +0 -85
  58. package/src/adapters/mui/useChartData.ts +0 -62
  59. package/src/core/formatBucket.ts +0 -73
  60. package/src/core/label.ts +0 -28
  61. package/src/core/queryParams.ts +0 -27
  62. package/src/core/types.ts +0 -157
  63. package/src/core/useDashboard.ts +0 -161
  64. package/src/core/useReportExport.ts +0 -68
  65. package/src/core/useReportList.ts +0 -51
  66. package/src/core/useReportPreview.ts +0 -68
  67. package/src/core/useReportSeries.ts +0 -59
  68. package/src/core/useReportSpec.ts +0 -79
  69. package/src/core/useReportTable.ts +0 -71
  70. package/src/core/visualizationLayout.ts +0 -34
  71. package/src/core/visualizationRegistry.ts +0 -39
  72. package/src/createReportsResource.tsx +0 -81
  73. package/src/i18n/en.ts +0 -95
  74. package/src/i18n/es.ts +0 -84
  75. /package/{src/i18n/index.ts → dist/i18n/index.d.ts} +0 -0
@@ -1,108 +0,0 @@
1
- import { Box, LinearProgress, Stack, Typography, useTheme } from '@mui/material';
2
- import { colorForSlot } from './palette';
3
- import { label as renderLabel } from '../../core/label';
4
- import type { VisualizationProps } from '../../core/visualizationRegistry';
5
-
6
- /**
7
- * Stage-by-stage drop-off.
8
- *
9
- * Rendered as proportional bars rather than the tapering-trapezoid shape a
10
- * "funnel" usually implies: a trapezoid encodes each stage in an area whose
11
- * width AND height both change, which overstates the drop. A bar per stage
12
- * encodes one quantity in one dimension and is simply readable.
13
- *
14
- * The stage order is the METRIC order the backend declared, which is the flow
15
- * order — so this widget renders KitchnTabs' tab lifecycle without knowing
16
- * anything about it. Percentages are shown against the first stage (overall
17
- * conversion) and against the previous one (step conversion), because the two
18
- * answer different questions and reading either off a bar length is guesswork.
19
- */
20
- export function FunnelWidget({ result, spec, translate }: VisualizationProps) {
21
- const theme = useTheme();
22
-
23
- const stages = spec.metrics
24
- .map((metric, index) => ({
25
- key: metric.key,
26
- label: renderLabel(translate, metric.label, metric.labelText),
27
- value: result.totals[metric.key] ?? 0,
28
- slot: index,
29
- }))
30
- .filter((s) => s.value !== null);
31
-
32
- if (stages.length === 0) return null;
33
-
34
- const first = stages[0]?.value || 0;
35
-
36
- return (
37
- <Stack spacing={1.75} sx={{ px: 0.5, py: 1 }}>
38
- {stages.map((stage, index) => {
39
- const previous = index === 0 ? stage.value : stages[index - 1].value;
40
- const ofFirst = first > 0 ? (stage.value / first) * 100 : 0;
41
- const ofPrevious = previous > 0 ? (stage.value / previous) * 100 : 0;
42
- const color = colorForSlot(stage.slot, theme.palette.mode === 'dark');
43
-
44
- return (
45
- <Box key={stage.key}>
46
- <Stack
47
- direction="row"
48
- sx={{ justifyContent: 'space-between', alignItems: 'baseline', gap: 1, mb: 0.5 }}
49
- >
50
- {/* The label is the only part allowed to shrink; the figures stay
51
- on one line. Previously the percentage column wrapped to three
52
- lines ("100%", "etapa", "833.3%") and the row read as noise. */}
53
- <Typography
54
- variant="body2"
55
- sx={{
56
- color: theme.palette.text.primary,
57
- overflow: 'hidden',
58
- textOverflow: 'ellipsis',
59
- whiteSpace: 'nowrap',
60
- minWidth: 0,
61
- }}
62
- >
63
- {stage.label}
64
- </Typography>
65
- <Stack
66
- direction="row"
67
- spacing={1.5}
68
- sx={{ alignItems: 'baseline', flexShrink: 0, whiteSpace: 'nowrap' }}
69
- >
70
- <Typography variant="body2" sx={{ color: theme.palette.text.primary, fontWeight: 500 }}>
71
- {stage.value.toLocaleString()}
72
- </Typography>
73
- <Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
74
- {Math.round(ofFirst * 10) / 10}%
75
- {/* A step above 100% is not a conversion — it means tabs
76
- reached this stage without passing through the previous
77
- one (a tab closed straight from CREATED, say). Printing
78
- "833% conversion" reads as a broken number; naming the
79
- skip states what actually happened. */}
80
- {index > 0 && (
81
- ofPrevious > 100
82
- ? ` · ${translate('dash_reports.funnel.skipped', { _: 'stage skipped' })}`
83
- : ` · ${translate('dash_reports.funnel.step', { _: 'step' })} ${Math.round(ofPrevious * 10) / 10}%`
84
- )}
85
- </Typography>
86
- </Stack>
87
- </Stack>
88
- <LinearProgress
89
- variant="determinate"
90
- value={Math.min(100, ofFirst)}
91
- aria-label={stage.label}
92
- sx={{
93
- height: 10,
94
- borderRadius: 1,
95
- backgroundColor: theme.palette.mode === 'dark'
96
- ? 'rgba(255,255,255,0.08)'
97
- : 'rgba(0,0,0,0.06)',
98
- '& .MuiLinearProgress-bar': { backgroundColor: color, borderRadius: 1 },
99
- }}
100
- />
101
- </Box>
102
- );
103
- })}
104
- </Stack>
105
- );
106
- }
107
-
108
- export default FunnelWidget;
@@ -1,123 +0,0 @@
1
- import { Box, Typography, useTheme } from '@mui/material';
2
- import { label as renderLabel } from '../../core/label';
3
- import type { VisualizationProps } from '../../core/visualizationRegistry';
4
-
5
- /**
6
- * The headline numbers, with no plot.
7
- *
8
- * The window totals come straight from the backend's ungrouped aggregate — not
9
- * a sum of the series — because averages and extrema do not compose across
10
- * buckets: averaging twelve monthly averages is not the yearly average.
11
- *
12
- * Figures use the body face at a normal weight, and deliberately NOT
13
- * `tabular-nums`: equal-width digits make a large standalone number look gappy
14
- * and mechanical. Tabular figures belong in a column of numbers, which is what
15
- * the table view is for.
16
- */
17
- /**
18
- * A figure, in the unit its metric is measured in.
19
- *
20
- * Durations are minutes, and are shown as minutes rather than a bare number —
21
- * "643" beside "Entregado → Cerrado" is ambiguous in a way "643 min" is not.
22
- * Sub-minute averages keep two decimals so a fast hop reads as 0.26 min rather
23
- * than rounding to a flat 0, which would look like "no data".
24
- */
25
- function formatMetricValue(
26
- value: number | null | undefined,
27
- metric: { decimal: boolean; unit?: string | null },
28
- translate: (key: string, options?: Record<string, unknown>) => string,
29
- ): string {
30
- if (value === null || value === undefined) return '—';
31
-
32
- const digits = metric.decimal ? 2 : 0;
33
- const text = value.toLocaleString(undefined, {
34
- minimumFractionDigits: digits,
35
- maximumFractionDigits: digits,
36
- });
37
-
38
- if (metric.unit === 'minutes') {
39
- return formatMinutes(value, translate);
40
- }
41
-
42
- return text;
43
- }
44
-
45
- /**
46
- * A duration in the unit a person would say it in.
47
- *
48
- * "776.40 min" is technically right and practically unreadable — nobody holds
49
- * thirteen hours in their head as a minute count. The scale switches at the
50
- * points where the smaller unit stops being informative: under a minute,
51
- * seconds; under 90 minutes, minutes; beyond that, hours.
52
- */
53
- function formatMinutes(
54
- minutes: number,
55
- translate: (key: string, options?: Record<string, unknown>) => string,
56
- ): string {
57
- const abs = Math.abs(minutes);
58
-
59
- if (abs < 1) {
60
- const seconds = Math.round(minutes * 60);
61
- return `${seconds} ${translate('dash_reports.units.seconds', { _: 's' })}`;
62
- }
63
-
64
- if (abs < 90) {
65
- const shown = abs < 10 ? Math.round(minutes * 10) / 10 : Math.round(minutes);
66
- return `${shown} ${translate('dash_reports.units.minutes', { _: 'min' })}`;
67
- }
68
-
69
- const hours = Math.round((minutes / 60) * 10) / 10;
70
- return `${hours} ${translate('dash_reports.units.hours', { _: 'h' })}`;
71
- }
72
-
73
- export function KpiWidget({ result, spec, translate }: VisualizationProps) {
74
- const theme = useTheme();
75
-
76
- const metrics = spec.metrics.filter((m) => result.totals[m.key] !== undefined);
77
-
78
- if (metrics.length === 0) return null;
79
-
80
- // A grid, not a wrapping flex row: with flex-wrap the figures landed at
81
- // arbitrary x-positions on each line, so nothing lined up vertically and a
82
- // six-metric tile read as scattered rather than tabulated.
83
- return (
84
- <Box
85
- sx={{
86
- display: 'grid',
87
- gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
88
- gap: 2,
89
- px: 0.5,
90
- py: 1,
91
- }}
92
- role="list"
93
- >
94
- {metrics.map((metric) => {
95
- const value = result.totals[metric.key];
96
-
97
- return (
98
- <Box key={metric.key} role="listitem" sx={{ minWidth: 140 }}>
99
- <Typography
100
- variant="body2"
101
- sx={{ color: theme.palette.text.secondary, mb: 0.5 }}
102
- >
103
- {renderLabel(translate, metric.label, metric.labelText)}
104
- </Typography>
105
- <Typography
106
- component="p"
107
- sx={{
108
- fontSize: '1.75rem',
109
- lineHeight: 1.15,
110
- fontWeight: 500,
111
- color: theme.palette.text.primary,
112
- }}
113
- >
114
- {formatMetricValue(value, metric, translate)}
115
- </Typography>
116
- </Box>
117
- );
118
- })}
119
- </Box>
120
- );
121
- }
122
-
123
- export default KpiWidget;
@@ -1,70 +0,0 @@
1
- import { Line } from 'react-chartjs-2';
2
- import { ensureChartsRegistered } from './chartSetup';
3
- import { baseChartOptions } from './chartTheme';
4
- import { useChartData } from './useChartData';
5
- import type { VisualizationProps } from '../../core/visualizationRegistry';
6
-
7
- ensureChartsRegistered();
8
-
9
- /**
10
- * Change over time.
11
- *
12
- * 2px strokes and points that only appear on hover — a marker on every bucket
13
- * of a 90-day series is noise, but the hover target still has to be reachable,
14
- * so hitRadius stays generous while the drawn radius is zero. Lines are not
15
- * filled by default: overlapping translucent areas muddy every colour beneath
16
- * them, and this chart is for comparing shapes.
17
- *
18
- * `spanGaps` is deliberately off. A null is "no data" (an average with nothing
19
- * to average), and bridging it would draw a straight line through a period the
20
- * report is explicitly saying nothing about.
21
- */
22
- export function LineWidget(props: VisualizationProps) {
23
- const { chrome, datasets, labels, longLabels } = useChartData(props);
24
- const base = baseChartOptions(chrome, datasets.length > 1);
25
-
26
- const data = {
27
- labels,
28
- datasets: datasets.map((d) => ({
29
- label: d.label,
30
- data: d.data,
31
- borderColor: d.color,
32
- backgroundColor: d.color,
33
- borderWidth: 2,
34
- tension: 0.25,
35
- pointRadius: 0,
36
- pointHoverRadius: 5,
37
- // A 12px hit radius on an invisible point: the value stays reachable
38
- // without asking anyone to land on a 2px line.
39
- pointHitRadius: 12,
40
- pointBorderWidth: 2,
41
- // A surface-coloured ring keeps an active point legible where series
42
- // overlap, without drawing a border on every mark.
43
- pointHoverBorderColor: chrome.surface,
44
- pointHoverBorderWidth: 2,
45
- spanGaps: false,
46
- fill: false,
47
- })),
48
- };
49
-
50
- return (
51
- <Line
52
- data={data}
53
- height={props.height ?? 320}
54
- options={{
55
- ...base,
56
- plugins: {
57
- ...base.plugins,
58
- tooltip: {
59
- ...base.plugins.tooltip,
60
- callbacks: {
61
- title: (items: Array<{ dataIndex: number }>) => longLabels[items[0]?.dataIndex] ?? '',
62
- },
63
- },
64
- },
65
- }}
66
- />
67
- );
68
- }
69
-
70
- export default LineWidget;
@@ -1,16 +0,0 @@
1
- import { DoughnutWidget } from './DoughnutWidget';
2
- import type { VisualizationProps } from '../../core/visualizationRegistry';
3
-
4
- /**
5
- * A doughnut, not a true pie.
6
- *
7
- * Kept as a distinct registered type so a report that declares `Pie` renders,
8
- * but drawn with a centre hole: the hole removes the wedge apexes, where a
9
- * pie's area is least readable, and gives the legend somewhere to breathe.
10
- * Nothing is lost — both encode a part-of-whole share by angle.
11
- */
12
- export function PieWidget(props: VisualizationProps) {
13
- return <DoughnutWidget {...props} />;
14
- }
15
-
16
- export default PieWidget;
@@ -1,123 +0,0 @@
1
- import { Box, Skeleton, Stack, Typography, useTheme } from '@mui/material';
2
- import { Bar, Line } from 'react-chartjs-2';
3
- import { ensureChartsRegistered } from './chartSetup';
4
- import { chartChrome } from './chartTheme';
5
- import { buildSlotMap, colorForSlot, MAX_SERIES } from './palette';
6
- import { label as renderLabel } from '../../core/label';
7
- import type { ReportMetric, ReportResult, TranslateFn, VisualizationType } from '../../core/types';
8
-
9
- ensureChartsRegistered();
10
-
11
- export interface PreviewWidgetProps {
12
- result: ReportResult | null;
13
- loading: boolean;
14
- type: VisualizationType;
15
- translate: TranslateFn;
16
- /** From the list payload; without it the card would print raw metric keys. */
17
- metrics?: ReportMetric[];
18
- }
19
-
20
- const PREVIEW_HEIGHT = 96;
21
-
22
- /**
23
- * The card-sized rendering of a report.
24
- *
25
- * Deliberately not the full widget shrunk down. At this size axes, legends and
26
- * gridlines are unreadable ink — so the preview keeps only the shape (or, for a
27
- * report whose answer is a number, just the number). Everything is stripped:
28
- * no axes, no legend, no tooltip. Reading a value is what opening the report is
29
- * for; this is only enough to tell one report from another and see whether
30
- * anything is happening.
31
- */
32
- export function PreviewWidget({ result, loading, type, translate, metrics }: PreviewWidgetProps) {
33
- const theme = useTheme();
34
-
35
- if (loading) {
36
- return <Skeleton variant="rounded" height={PREVIEW_HEIGHT} sx={{ mt: 1 }} />;
37
- }
38
-
39
- if (!result || result.series.length === 0) {
40
- return (
41
- <Box sx={{ height: PREVIEW_HEIGHT, display: 'grid', placeItems: 'center', mt: 1 }}>
42
- <Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
43
- {translate('dash_reports.no_data', { _: 'No data for this range.' })}
44
- </Typography>
45
- </Box>
46
- );
47
- }
48
-
49
- const chrome = chartChrome(theme);
50
-
51
- // Kpi and Funnel both answer with numbers rather than a shape, so at card
52
- // size they render as figures instead of a chart nobody could read.
53
- if (type === 'Kpi' || type === 'Funnel') {
54
- const entries = Object.entries(result.totals).slice(0, 3);
55
-
56
- return (
57
- <Stack direction="row" sx={{ gap: 2.5, mt: 1.5, minHeight: PREVIEW_HEIGHT, alignItems: 'center', flexWrap: 'wrap' }}>
58
- {entries.map(([key, value]) => {
59
- const metric = metrics?.find((m) => m.key === key);
60
-
61
- return (
62
- <Box key={key}>
63
- <Typography variant="caption" sx={{ color: theme.palette.text.secondary, display: 'block' }}>
64
- {metric ? renderLabel(translate, metric.label, metric.labelText) : key}
65
- </Typography>
66
- <Typography sx={{ fontSize: '1.35rem', lineHeight: 1.2, color: theme.palette.text.primary }}>
67
- {value === null || value === undefined ? '—' : value.toLocaleString()}
68
- </Typography>
69
- </Box>
70
- );
71
- })}
72
- </Stack>
73
- );
74
- }
75
-
76
- const slots = buildSlotMap(null, result.meta?.pivot ?? undefined, result.series.map((s) => s.key));
77
- const datasets = result.series
78
- .filter((s) => (slots.get(s.key) ?? 0) < MAX_SERIES)
79
- .map((s) => ({
80
- label: s.label,
81
- data: s.data,
82
- color: colorForSlot(slots.get(s.key) ?? 0, chrome.dark),
83
- }));
84
-
85
- const options = {
86
- responsive: true,
87
- maintainAspectRatio: false,
88
- // Every affordance off: at 96px these are ink, not information.
89
- plugins: { legend: { display: false }, tooltip: { enabled: false } },
90
- scales: {
91
- x: { display: false },
92
- y: { display: false, beginAtZero: true },
93
- },
94
- elements: { point: { radius: 0 } },
95
- };
96
-
97
- const data = {
98
- labels: result.buckets.map(() => ''),
99
- datasets: datasets.map((d) => ({
100
- label: d.label,
101
- data: d.data,
102
- backgroundColor: d.color,
103
- borderColor: d.color,
104
- borderWidth: type === 'Line' ? 2 : 0,
105
- borderRadius: 3,
106
- borderSkipped: 'bottom' as const,
107
- maxBarThickness: 14,
108
- tension: 0.3,
109
- fill: false,
110
- pointRadius: 0,
111
- })),
112
- };
113
-
114
- return (
115
- <Box sx={{ height: PREVIEW_HEIGHT, mt: 1 }}>
116
- {type === 'Line'
117
- ? <Line data={data} options={options} height={PREVIEW_HEIGHT} />
118
- : <Bar data={data} options={options} height={PREVIEW_HEIGHT} />}
119
- </Box>
120
- );
121
- }
122
-
123
- export default PreviewWidget;
@@ -1,141 +0,0 @@
1
- import { useMemo } from 'react';
2
- import {
3
- Box, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, Typography, useTheme,
4
- } from '@mui/material';
5
- import { formatBucketLong } from '../../core/formatBucket';
6
- import { label as renderLabel } from '../../core/label';
7
- import type { VisualizationProps } from '../../core/visualizationRegistry';
8
-
9
- /**
10
- * The series as numbers.
11
- *
12
- * Not optional decoration. Three of the light-mode series hues sit below 3:1
13
- * contrast against a light surface, which is permitted only where a
14
- * non-colour path to every value exists — this is that path. It is also the
15
- * answer to "a tooltip is the only way to read a value".
16
- *
17
- * Figures are tabular here, unlike the KPI tile: in a column of numbers,
18
- * equal-width digits are what makes magnitudes line up and scan.
19
- */
20
- export function SeriesTableWidget({ result, translate }: VisualizationProps) {
21
- const theme = useTheme();
22
-
23
- const rows = useMemo(
24
- () => result.buckets.map((bucket, index) => ({
25
- bucket: formatBucketLong(bucket, result.granularity, translate),
26
- values: result.series.map((s) => s.data[index]),
27
- })),
28
- [result, translate],
29
- );
30
-
31
- return (
32
- <TableContainer sx={{ maxHeight: 420 }}>
33
- <Table size="small" stickyHeader>
34
- <TableHead>
35
- <TableRow>
36
- <TableCell sx={{ color: theme.palette.text.secondary }}>
37
- {translate('dash_reports.table.period', { _: 'Period' })}
38
- </TableCell>
39
- {result.series.map((s) => (
40
- <TableCell key={s.key} align="right" sx={{ color: theme.palette.text.secondary }}>
41
- {renderLabel(translate, s.label, s.labelText)}
42
- </TableCell>
43
- ))}
44
- </TableRow>
45
- </TableHead>
46
- <TableBody>
47
- {rows.map((row) => (
48
- <TableRow key={row.bucket} hover>
49
- <TableCell sx={{ color: theme.palette.text.primary }}>{row.bucket}</TableCell>
50
- {row.values.map((value, i) => (
51
- <TableCell
52
- key={i}
53
- align="right"
54
- sx={{ color: theme.palette.text.primary, fontVariantNumeric: 'tabular-nums' }}
55
- >
56
- {value === null || value === undefined ? '—' : value.toLocaleString()}
57
- </TableCell>
58
- ))}
59
- </TableRow>
60
- ))}
61
- </TableBody>
62
- </Table>
63
- </TableContainer>
64
- );
65
- }
66
-
67
- export interface DetailTableProps {
68
- page: {
69
- data: Array<Record<string, unknown>>;
70
- total: number;
71
- page: number;
72
- perPage: number;
73
- } | null;
74
- loading: boolean;
75
- currentPage: number;
76
- onPageChange: (page: number) => void;
77
- translate: (key: string, options?: Record<string, unknown>) => string;
78
- }
79
-
80
- /** The paginated source rows behind the chart. */
81
- export function DetailTable({ page, loading, currentPage, onPageChange, translate }: DetailTableProps) {
82
- const theme = useTheme();
83
- const rows = page?.data ?? [];
84
- const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
85
-
86
- if (!loading && rows.length === 0) {
87
- return (
88
- <Box sx={{ p: 3 }}>
89
- <Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
90
- {translate('dash_reports.no_rows', { _: 'No rows for this range.' })}
91
- </Typography>
92
- </Box>
93
- );
94
- }
95
-
96
- return (
97
- <Box>
98
- <TableContainer sx={{ maxHeight: 420 }}>
99
- <Table size="small" stickyHeader>
100
- <TableHead>
101
- <TableRow>
102
- {columns.map((column) => (
103
- <TableCell key={column} sx={{ color: theme.palette.text.secondary, whiteSpace: 'nowrap' }}>
104
- {translate(`dash_reports.columns.${column}`, { _: column })}
105
- </TableCell>
106
- ))}
107
- </TableRow>
108
- </TableHead>
109
- <TableBody>
110
- {rows.map((row, index) => (
111
- <TableRow key={index} hover>
112
- {columns.map((column) => (
113
- <TableCell key={column} sx={{ color: theme.palette.text.primary, whiteSpace: 'nowrap' }}>
114
- {formatCell(row[column])}
115
- </TableCell>
116
- ))}
117
- </TableRow>
118
- ))}
119
- </TableBody>
120
- </Table>
121
- </TableContainer>
122
- <TablePagination
123
- component="div"
124
- count={page?.total ?? 0}
125
- page={Math.max(0, currentPage - 1)}
126
- onPageChange={(_, next) => onPageChange(next + 1)}
127
- rowsPerPage={page?.perPage ?? 50}
128
- rowsPerPageOptions={[page?.perPage ?? 50]}
129
- />
130
- </Box>
131
- );
132
- }
133
-
134
- function formatCell(value: unknown): string {
135
- if (value === null || value === undefined) return '—';
136
- if (typeof value === 'boolean') return value ? '✓' : '—';
137
- if (typeof value === 'object') return JSON.stringify(value);
138
- return String(value);
139
- }
140
-
141
- export default SeriesTableWidget;
@@ -1,53 +0,0 @@
1
- import {
2
- BarController,
3
- BarElement,
4
- CategoryScale,
5
- Chart as ChartJS,
6
- Filler,
7
- Legend,
8
- LineController,
9
- LineElement,
10
- LinearScale,
11
- ArcElement,
12
- DoughnutController,
13
- PieController,
14
- PointElement,
15
- Tooltip,
16
- } from 'chart.js';
17
-
18
- /**
19
- * Register only the Chart.js pieces these widgets use.
20
- *
21
- * Explicit registration rather than `registerables`, which pulls in every
22
- * controller, scale and plugin Chart.js ships. The legacy Stats page imported
23
- * the lot for two chart types.
24
- *
25
- * Note what is absent: the datalabels plugin. A value printed on every point is
26
- * an anti-pattern — it is unreadable at any real series count and goes unread.
27
- * Values are reachable through the tooltip and, always, the table view.
28
- */
29
- let registered = false;
30
-
31
- export function ensureChartsRegistered(): void {
32
- if (registered) return;
33
-
34
- ChartJS.register(
35
- BarController,
36
- BarElement,
37
- LineController,
38
- LineElement,
39
- PointElement,
40
- ArcElement,
41
- DoughnutController,
42
- PieController,
43
- CategoryScale,
44
- LinearScale,
45
- Filler,
46
- Legend,
47
- Tooltip,
48
- );
49
-
50
- registered = true;
51
- }
52
-
53
- export default ensureChartsRegistered;