@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
@@ -0,0 +1,83 @@
1
+ export declare const es: {
2
+ dash_reports: {
3
+ title: string;
4
+ units: {
5
+ seconds: string;
6
+ minutes: string;
7
+ hours: string;
8
+ };
9
+ dashboard: {
10
+ title: string;
11
+ };
12
+ period: {
13
+ open: string;
14
+ closed: string;
15
+ current: string;
16
+ };
17
+ back_to_list: string;
18
+ no_reports: string;
19
+ loading: string;
20
+ no_data: string;
21
+ no_rows: string;
22
+ other: string;
23
+ show_detail: string;
24
+ hide_detail: string;
25
+ week_of: string;
26
+ filters: {
27
+ title: string;
28
+ reset: string;
29
+ done: string;
30
+ start: string;
31
+ end: string;
32
+ granularity: string;
33
+ pivot: string;
34
+ no_pivot: string;
35
+ };
36
+ granularity: {
37
+ daily: string;
38
+ weekly: string;
39
+ monthly: string;
40
+ yearly: string;
41
+ total: string;
42
+ hourOfDay: string;
43
+ dayOfWeek: string;
44
+ };
45
+ viz: {
46
+ Bar: string;
47
+ Line: string;
48
+ Pie: string;
49
+ Doughnut: string;
50
+ Kpi: string;
51
+ Funnel: string;
52
+ Table: string;
53
+ };
54
+ funnel: {
55
+ step: string;
56
+ skipped: string;
57
+ };
58
+ table: {
59
+ period: string;
60
+ };
61
+ weekdays: {
62
+ monday: string;
63
+ tuesday: string;
64
+ wednesday: string;
65
+ thursday: string;
66
+ friday: string;
67
+ saturday: string;
68
+ sunday: string;
69
+ };
70
+ export: {
71
+ xlsx: string;
72
+ csv: string;
73
+ };
74
+ errors: {
75
+ spec: string;
76
+ list: string;
77
+ series: string;
78
+ dashboard: string;
79
+ widget: string;
80
+ };
81
+ };
82
+ };
83
+ export default es;
@@ -1,12 +1,3 @@
1
- /**
2
- * @dashadmin/dash-reports — the domain-agnostic reporting UI.
3
- *
4
- * Renders a report entirely from the spec app/ReportCore serves for it, so a
5
- * new report is a backend definition plus one resource entry, with no change
6
- * here.
7
- */
8
-
9
- // Core — framework-agnostic, no chart or MUI import.
10
1
  export * from './core/types';
11
2
  export { useReportSpec } from './core/useReportSpec';
12
3
  export { useReportSeries } from './core/useReportSeries';
@@ -16,37 +7,18 @@ export { useReportPreview } from './core/useReportPreview';
16
7
  export { useReportExport } from './core/useReportExport';
17
8
  export { toQueryParams } from './core/queryParams';
18
9
  export { formatBucket, formatBucketLong } from './core/formatBucket';
19
- export {
20
- registerVisualization,
21
- getVisualization,
22
- registeredVisualizations,
23
- } from './core/visualizationRegistry';
10
+ export { registerVisualization, getVisualization, registeredVisualizations, } from './core/visualizationRegistry';
24
11
  export type { VisualizationProps } from './core/visualizationRegistry';
25
-
26
- // MUI / Chart.js adapter. Importing it registers the default widget set.
27
12
  export * from './adapters/mui';
28
-
29
- // Composed UI.
30
13
  export { ReportPage } from './ReportPage';
31
14
  export type { ReportPageProps } from './ReportPage';
32
15
  export { ReportFilters } from './ReportFilters';
33
16
  export { ReportsModule } from './ReportsModule';
34
17
  export { ReportsDashboard } from './ReportsDashboard';
35
18
  export { TenantDashboard } from './TenantDashboard';
36
- export {
37
- useDashboardTenants,
38
- useTenantDashboard,
39
- useTenantPeriods,
40
- } from './core/useDashboard';
41
- export type {
42
- DashboardWidget,
43
- DashboardTenant,
44
- DashboardPeriod,
45
- DashboardData,
46
- } from './core/useDashboard';
19
+ export { useDashboardTenants, useTenantDashboard, useTenantPeriods, } from './core/useDashboard';
20
+ export type { DashboardWidget, DashboardTenant, DashboardPeriod, DashboardData, } from './core/useDashboard';
47
21
  export { ReportsIndex } from './ReportsIndex';
48
22
  export { createReportsResource } from './createReportsResource';
49
23
  export type { CreateReportsResourceOptions } from './createReportsResource';
50
-
51
- // Strings, merged at app entry — see dash-admin's mergeTranslations.
52
24
  export { en as reportsEn, es as reportsEs } from './i18n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashadmin/dash-reports",
3
- "version": "1.3.72",
3
+ "version": "1.3.74-srcfree.1",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "devDependencies": {
@@ -12,11 +12,11 @@
12
12
  "dependencies": {
13
13
  "@mui/material": "^9.3.0",
14
14
  "chart.js": "^4.5.0",
15
- "@dashadmin/dash-axios-hook": "1.3.72",
15
+ "@dashadmin/dash-axios-hook": "1.3.74-srcfree.1",
16
16
  "react-chartjs-2": "^5.3.1"
17
17
  },
18
18
  "module": "dist/index.js",
19
- "types": "./src/index.tsx",
19
+ "types": "./dist/index.d.ts",
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
22
  "./*": "./dist/*.js",
@@ -27,22 +27,25 @@
27
27
  "registry": "https://registry.npmjs.org"
28
28
  },
29
29
  "scripts": {
30
- "build": "tsup"
30
+ "build": "tsup && pnpm run build:types",
31
+ "build:types": "tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src --skipLibCheck || true"
31
32
  },
32
33
  "type": "module",
33
34
  "files": [
34
- "dist",
35
- "src"
35
+ "dist"
36
36
  ],
37
37
  "typesVersions": {
38
38
  "*": {
39
+ "dist/*": [
40
+ "dist/*"
41
+ ],
39
42
  "src/*": [
40
- "src/*",
41
- "src/*/index"
43
+ "dist/*",
44
+ "dist/*/index"
42
45
  ],
43
46
  "*": [
44
- "src/*",
45
- "src/*/index"
47
+ "dist/*",
48
+ "dist/*/index"
46
49
  ]
47
50
  }
48
51
  }
@@ -1,145 +0,0 @@
1
- import {
2
- Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Stack, TextField,
3
- } from '@mui/material';
4
- import { label } from './core/label';
5
- import type { ReportFilterState, ReportSpec, TranslateFn } from './core/types';
6
-
7
- export interface ReportFiltersProps {
8
- spec: ReportSpec;
9
- value: ReportFilterState;
10
- onChange: (next: ReportFilterState) => void;
11
- translate: TranslateFn;
12
- }
13
-
14
- /**
15
- * The filter form.
16
- *
17
- * Laid out as a single column because it lives in a right-hand drawer — every
18
- * control gets full width, so long option labels (a restaurant name, a product)
19
- * are readable instead of truncated into a narrow inline slot.
20
- *
21
- * One filter set for the whole report, not per chart: every visualisation on
22
- * the page reads the same query, and per-chart filters would imply they can
23
- * disagree. Because the drawer hides that state while closed, ReportPage keeps
24
- * a summary of what is applied visible in the header — a filter nobody can see
25
- * is a filter they forget is on.
26
- */
27
- export function ReportFilters({ spec, value, onChange, translate }: ReportFiltersProps) {
28
- const set = (patch: Partial<ReportFilterState>) => onChange({ ...value, ...patch });
29
-
30
- const setDimension = (key: string, selected: Array<string | number | boolean>) =>
31
- onChange({ ...value, dimensions: { ...value.dimensions, [key]: selected } });
32
-
33
- return (
34
- <Stack spacing={2.5} sx={{ p: 2.5, width: { xs: '86vw', sm: 360 } }}>
35
- <TextField
36
- type="date"
37
- size="small"
38
- fullWidth
39
- label={translate('dash_reports.filters.start', { _: 'From' })}
40
- slotProps={{ inputLabel: { shrink: true } }}
41
- value={value.start}
42
- onChange={(e) => set({ start: e.target.value })}
43
- />
44
-
45
- <TextField
46
- type="date"
47
- size="small"
48
- fullWidth
49
- label={translate('dash_reports.filters.end', { _: 'To' })}
50
- slotProps={{ inputLabel: { shrink: true } }}
51
- value={value.end}
52
- onChange={(e) => set({ end: e.target.value })}
53
- />
54
-
55
- <FormControl size="small" fullWidth>
56
- <InputLabel id="report-granularity">
57
- {translate('dash_reports.filters.granularity', { _: 'Group by' })}
58
- </InputLabel>
59
- <Select
60
- labelId="report-granularity"
61
- label={translate('dash_reports.filters.granularity', { _: 'Group by' })}
62
- value={value.granularity}
63
- onChange={(e) => set({ granularity: e.target.value as ReportFilterState['granularity'] })}
64
- >
65
- {spec.granularities.map((g) => (
66
- <MenuItem key={g} value={g}>
67
- {translate(`dash_reports.granularity.${g}`, { _: g })}
68
- </MenuItem>
69
- ))}
70
- </Select>
71
- </FormControl>
72
-
73
- {spec.dimensions.length > 0 && (
74
- <FormControl size="small" fullWidth>
75
- <InputLabel id="report-pivot">
76
- {translate('dash_reports.filters.pivot', { _: 'Split by' })}
77
- </InputLabel>
78
- <Select
79
- labelId="report-pivot"
80
- label={translate('dash_reports.filters.pivot', { _: 'Split by' })}
81
- value={value.pivot ?? ''}
82
- onChange={(e) => set({ pivot: e.target.value || undefined })}
83
- >
84
- {/* No split is a real choice, not an absence — with none, the chart
85
- shows one series per metric instead. */}
86
- <MenuItem value="">
87
- {translate('dash_reports.filters.no_pivot', { _: 'Nothing' })}
88
- </MenuItem>
89
- {spec.dimensions.map((d) => (
90
- <MenuItem key={d.key} value={d.key}>
91
- {label(translate, d.label, d.labelText)}
92
- </MenuItem>
93
- ))}
94
- </Select>
95
- </FormControl>
96
- )}
97
-
98
- {spec.dimensions
99
- .filter((d) => (d.values?.length ?? 0) > 0)
100
- .map((dimension) => {
101
- const selected = value.dimensions[dimension.key] ?? [];
102
-
103
- return (
104
- <FormControl key={dimension.key} size="small" fullWidth>
105
- <InputLabel id={`report-dim-${dimension.key}`}>
106
- {label(translate, dimension.label, dimension.labelText)}
107
- </InputLabel>
108
- <Select
109
- labelId={`report-dim-${dimension.key}`}
110
- multiple={dimension.multiple !== false}
111
- value={selected}
112
- input={<OutlinedInput label={label(translate, dimension.label, dimension.labelText)} />}
113
- onChange={(e) => {
114
- const next = e.target.value;
115
- setDimension(dimension.key, Array.isArray(next) ? next : [next]);
116
- }}
117
- renderValue={(picked) => (
118
- <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
119
- {(Array.isArray(picked) ? picked : [picked]).map((v) => {
120
- const option = dimension.values?.find((o) => String(o.value) === String(v));
121
- return (
122
- <Chip
123
- key={String(v)}
124
- size="small"
125
- label={option ? label(translate, option.label, option.labelText) : String(v)}
126
- />
127
- );
128
- })}
129
- </Box>
130
- )}
131
- >
132
- {dimension.values?.map((option) => (
133
- <MenuItem key={String(option.value)} value={option.value as string}>
134
- {label(translate, option.label, option.labelText)}
135
- </MenuItem>
136
- ))}
137
- </Select>
138
- </FormControl>
139
- );
140
- })}
141
- </Stack>
142
- );
143
- }
144
-
145
- export default ReportFilters;
@@ -1,73 +0,0 @@
1
- import { Box, Button, Divider, Drawer, IconButton, Stack, Typography, useTheme } from '@mui/material';
2
- import { ReportFilters } from './ReportFilters';
3
- import type { ReportFilterState, ReportSpec, TranslateFn } from './core/types';
4
-
5
- export interface ReportFiltersDrawerProps {
6
- open: boolean;
7
- onClose: () => void;
8
- spec: ReportSpec;
9
- value: ReportFilterState;
10
- onChange: (next: ReportFilterState) => void;
11
- onReset: () => void;
12
- translate: TranslateFn;
13
- }
14
-
15
- /**
16
- * The filter panel, anchored right.
17
- *
18
- * Changes apply live rather than behind an "Apply" button: the charts already
19
- * refetch on every filter change and keep the previous result visible while
20
- * loading, so there is nothing for a confirm step to protect against — it would
21
- * only add a click and a second source of truth for "what is currently applied".
22
- *
23
- * `keepMounted` because the drawer holds live form state; unmounting it on
24
- * close would reset every select the moment the user peeked at the chart.
25
- */
26
- export function ReportFiltersDrawer({
27
- open, onClose, spec, value, onChange, onReset, translate,
28
- }: ReportFiltersDrawerProps) {
29
- const theme = useTheme();
30
-
31
- return (
32
- <Drawer
33
- anchor="right"
34
- open={open}
35
- onClose={onClose}
36
- keepMounted
37
- slotProps={{ paper: { sx: { backgroundImage: 'none' } } }}
38
- >
39
- <Stack sx={{ height: '100%' }}>
40
- <Stack
41
- direction="row"
42
- sx={{ alignItems: 'center', justifyContent: 'space-between', px: 2.5, py: 2 }}
43
- >
44
- <Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
45
- {translate('dash_reports.filters.title', { _: 'Filters' })}
46
- </Typography>
47
- <IconButton onClick={onClose} size="small" aria-label={translate('ra.action.close', { _: 'Close' })}>
48
- <Box component="span" sx={{ fontSize: 20, lineHeight: 1 }}>×</Box>
49
- </IconButton>
50
- </Stack>
51
-
52
- <Divider />
53
-
54
- <Box sx={{ flex: 1, overflowY: 'auto' }}>
55
- <ReportFilters spec={spec} value={value} onChange={onChange} translate={translate} />
56
- </Box>
57
-
58
- <Divider />
59
-
60
- <Stack direction="row" spacing={1} sx={{ p: 2 }}>
61
- <Button fullWidth variant="outlined" onClick={onReset}>
62
- {translate('dash_reports.filters.reset', { _: 'Reset' })}
63
- </Button>
64
- <Button fullWidth variant="contained" onClick={onClose}>
65
- {translate('dash_reports.filters.done', { _: 'Done' })}
66
- </Button>
67
- </Stack>
68
- </Stack>
69
- </Drawer>
70
- );
71
- }
72
-
73
- export default ReportFiltersDrawer;
@@ -1,280 +0,0 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
- import {
3
- Alert, Badge, Box, Button, Card, CardContent, Chip, Divider, LinearProgress, Stack, Tab, Tabs, Typography, useTheme,
4
- } from '@mui/material';
5
- import { useReportSpec } from './core/useReportSpec';
6
- import { label } from './core/label';
7
- import { visualizationBoxSx } from './core/visualizationLayout';
8
- import { useReportSeries } from './core/useReportSeries';
9
- import { useReportTable } from './core/useReportTable';
10
- import { getVisualization } from './core/visualizationRegistry';
11
- import { ReportFiltersDrawer } from './ReportFiltersDrawer';
12
- import { DetailTable } from './adapters/mui/TableWidget';
13
- import { useReportExport } from './core/useReportExport';
14
- import type { ReportFilterState, TranslateFn, VisualizationType } from './core/types';
15
- import './adapters/mui';
16
-
17
- export interface ReportPageProps {
18
- reportKey: string;
19
- translate: TranslateFn;
20
- /** Optional heading override; defaults to the spec's translated title. */
21
- title?: string;
22
- }
23
-
24
- function isoDaysAgo(days: number): string {
25
- const d = new Date();
26
- d.setDate(d.getDate() - days);
27
- return d.toISOString().slice(0, 10);
28
- }
29
-
30
- /**
31
- * A whole report, rendered from its spec.
32
- *
33
- * Nothing here knows what the report contains — the filters, the available
34
- * chart types, the metric labels and the export formats all come from the
35
- * backend. Adding a report to a domain is a backend change plus one resource
36
- * entry; this component does not change.
37
- */
38
- export function ReportPage({ reportKey, translate, title }: ReportPageProps) {
39
- const theme = useTheme();
40
- const { spec, loading: specLoading, error: specError } = useReportSpec(reportKey);
41
-
42
- const [filters, setFilters] = useState<ReportFilterState | null>(null);
43
- const [activeViz, setActiveViz] = useState<VisualizationType | null>(null);
44
- const [showDetail, setShowDetail] = useState(false);
45
- const [filtersOpen, setFiltersOpen] = useState(false);
46
-
47
- // One definition of "the default filters", used both to seed and to reset —
48
- // two copies would drift and Reset would stop returning to the initial view.
49
- const defaultFilters = useMemo<ReportFilterState | null>(
50
- () =>
51
- spec
52
- ? {
53
- start: isoDaysAgo(spec.defaultRangeDays ?? 30),
54
- end: new Date().toISOString().slice(0, 10),
55
- granularity: spec.defaultGranularity,
56
- pivot: undefined,
57
- dimensions: {},
58
- }
59
- : null,
60
- [spec],
61
- );
62
-
63
- // Seed once the spec arrives — the default window and granularity are the
64
- // report's to choose, not this component's.
65
- useEffect(() => {
66
- if (!defaultFilters || filters) return;
67
-
68
- setFilters(defaultFilters);
69
- // The report's declared default, not merely the first in the array —
70
- // otherwise reordering visualizations silently changes what opens.
71
- setActiveViz(
72
- (spec?.defaultVisualization as VisualizationType | undefined)
73
- ?? spec?.visualizations[0]?.type
74
- ?? null,
75
- );
76
- }, [defaultFilters, filters, spec]);
77
-
78
- /**
79
- * What is currently narrowing the data, as readable chips.
80
- *
81
- * The drawer hides the filter state while closed, so this keeps it on screen:
82
- * a filter someone cannot see is one they forget is applied, and then read the
83
- * numbers as if it were not.
84
- */
85
- const activeChips = useMemo(() => {
86
- if (!spec || !filters) return [];
87
-
88
- const chips: string[] = [];
89
-
90
- Object.entries(filters.dimensions ?? {}).forEach(([key, values]) => {
91
- if (!Array.isArray(values) || values.length === 0) return;
92
-
93
- const dimension = spec.dimensions.find((d) => d.key === key);
94
- if (!dimension) return;
95
-
96
- const labels = values.map((v) => {
97
- const option = dimension.values?.find((o) => String(o.value) === String(v));
98
- return option ? label(translate, option.label, option.labelText) : String(v);
99
- });
100
-
101
- chips.push(`${label(translate, dimension.label, dimension.labelText)}: ${labels.join(', ')}`);
102
- });
103
-
104
- if (filters.pivot) {
105
- const dimension = spec.dimensions.find((d) => d.key === filters.pivot);
106
- if (dimension) {
107
- chips.push(
108
- `${translate('dash_reports.filters.pivot', { _: 'Split by' })}: ${label(translate, dimension.label, dimension.labelText)}`,
109
- );
110
- }
111
- }
112
-
113
- return chips;
114
- }, [spec, filters, translate]);
115
-
116
- const { result, loading: seriesLoading, error: seriesError } = useReportSeries(reportKey, filters);
117
- const table = useReportTable(reportKey, filters, showDetail);
118
- const { exporting, exportReport } = useReportExport(reportKey);
119
-
120
- const Visualization = useMemo(
121
- () => (activeViz ? getVisualization(activeViz) : null),
122
- [activeViz],
123
- );
124
-
125
- if (specLoading) return <LinearProgress />;
126
-
127
- if (specError || !spec) {
128
- return (
129
- <Alert severity="error" sx={{ m: 2 }}>
130
- {translate('dash_reports.errors.spec', { _: 'This report could not be loaded.' })}
131
- </Alert>
132
- );
133
- }
134
-
135
- return (
136
- <Box sx={{ p: { xs: 2, md: 3 } }}>
137
- <Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between', mb: 2, gap: 2, flexWrap: 'wrap' }}>
138
- <Typography variant="h5" sx={{ color: theme.palette.text.primary }}>
139
- {title ?? label(translate, spec.title, spec.titleText)}
140
- </Typography>
141
-
142
- <Stack direction="row" spacing={1}>
143
- {spec.exporters.map((format) => (
144
- <Button
145
- key={format}
146
- size="small"
147
- variant="outlined"
148
- disabled={exporting || !filters}
149
- onClick={() => filters && exportReport(filters, format)}
150
- >
151
- {translate(`dash_reports.export.${format}`, { _: format.toUpperCase() })}
152
- </Button>
153
- ))}
154
-
155
- <Badge badgeContent={activeChips.length} color="primary">
156
- <Button size="small" variant="outlined" onClick={() => setFiltersOpen(true)}>
157
- {translate('dash_reports.filters.title', { _: 'Filters' })}
158
- </Button>
159
- </Badge>
160
- </Stack>
161
- </Stack>
162
-
163
- {/* The window is always narrowing the data, so it is always shown —
164
- unlike the dimension chips, which appear only when something is set. */}
165
- {filters && (
166
- <Stack direction="row" sx={{ flexWrap: 'wrap', gap: 1, mb: 2, alignItems: 'center' }}>
167
- <Chip
168
- size="small"
169
- variant="outlined"
170
- label={`${filters.start} → ${filters.end}`}
171
- />
172
- <Chip
173
- size="small"
174
- variant="outlined"
175
- label={translate(`dash_reports.granularity.${filters.granularity}`, { _: filters.granularity })}
176
- />
177
- {activeChips.map((chip) => (
178
- <Chip
179
- key={chip}
180
- size="small"
181
- label={chip}
182
- onDelete={undefined}
183
- />
184
- ))}
185
- </Stack>
186
- )}
187
-
188
- {seriesError && (
189
- <Alert severity="warning" sx={{ mb: 2 }}>
190
- {translate('dash_reports.errors.series', { _: 'This range could not be loaded.' })}
191
- </Alert>
192
- )}
193
-
194
- <Card variant="outlined">
195
- {spec.visualizations.length > 1 && (
196
- <>
197
- <Tabs
198
- value={activeViz}
199
- onChange={(_, next) => setActiveViz(next)}
200
- variant="scrollable"
201
- scrollButtons="auto"
202
- >
203
- {spec.visualizations.map((v) => (
204
- <Tab
205
- key={v.type}
206
- value={v.type}
207
- label={translate(`dash_reports.viz.${v.type}`, { _: v.type })}
208
- />
209
- ))}
210
- </Tabs>
211
- <Divider />
212
- </>
213
- )}
214
-
215
- {/* A refetch dims the existing chart rather than replacing it with a
216
- skeleton — flashing an empty box on every filter change loses the
217
- reader's place and reads as slower than it is. */}
218
- <Box sx={{ position: 'relative', opacity: seriesLoading && result ? 0.55 : 1, transition: 'opacity 120ms' }}>
219
- {seriesLoading && (
220
- <LinearProgress sx={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1 }} />
221
- )}
222
-
223
- {/* A DEFINITE height for canvas charts. minHeight is a floor, not a
224
- bound, and Chart.js with maintainAspectRatio:false grows to fill
225
- its parent — which grows the parent, which resizes the canvas,
226
- forever. */}
227
- <CardContent>
228
- {result && Visualization ? (
229
- <Box sx={visualizationBoxSx(activeViz, 340)}>
230
- <Visualization result={result} spec={spec} translate={translate} />
231
- </Box>
232
- ) : (
233
- <Box sx={{ height: 340, display: 'grid', placeItems: 'center' }}>
234
- <Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
235
- {seriesLoading
236
- ? translate('dash_reports.loading', { _: 'Loading…' })
237
- : translate('dash_reports.no_data', { _: 'No data for this range.' })}
238
- </Typography>
239
- </Box>
240
- )}
241
- </CardContent>
242
- </Box>
243
- </Card>
244
-
245
- <Box sx={{ mt: 2 }}>
246
- <Button size="small" onClick={() => setShowDetail((v) => !v)}>
247
- {showDetail
248
- ? translate('dash_reports.hide_detail', { _: 'Hide detail rows' })
249
- : translate('dash_reports.show_detail', { _: 'Show detail rows' })}
250
- </Button>
251
- </Box>
252
-
253
- {showDetail && (
254
- <Card variant="outlined" sx={{ mt: 1 }}>
255
- <DetailTable
256
- page={table.page}
257
- loading={table.loading}
258
- currentPage={table.currentPage}
259
- onPageChange={table.setPage}
260
- translate={translate}
261
- />
262
- </Card>
263
- )}
264
-
265
- {filters && (
266
- <ReportFiltersDrawer
267
- open={filtersOpen}
268
- onClose={() => setFiltersOpen(false)}
269
- spec={spec}
270
- value={filters}
271
- onChange={setFilters}
272
- onReset={() => defaultFilters && setFilters(defaultFilters)}
273
- translate={translate}
274
- />
275
- )}
276
- </Box>
277
- );
278
- }
279
-
280
- export default ReportPage;