@dashadmin/dash-reports 1.3.73 → 1.3.74-srcfree.2
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.
- package/dist/ReportFilters.d.ts +9 -0
- package/dist/ReportFiltersDrawer.d.ts +12 -0
- package/dist/ReportPage.d.ts +9 -0
- package/dist/ReportsDashboard.d.ts +7 -0
- package/dist/ReportsIndex.d.ts +7 -0
- package/dist/ReportsModule.d.ts +7 -0
- package/dist/TenantDashboard.d.ts +12 -0
- package/dist/adapters/mui/BarWidget.d.ts +3 -0
- package/dist/adapters/mui/DoughnutWidget.d.ts +3 -0
- package/dist/adapters/mui/FunnelWidget.d.ts +3 -0
- package/dist/adapters/mui/KpiWidget.d.ts +3 -0
- package/dist/adapters/mui/LineWidget.d.ts +3 -0
- package/dist/adapters/mui/PieWidget.d.ts +3 -0
- package/dist/adapters/mui/PreviewWidget.d.ts +10 -0
- package/dist/adapters/mui/TableWidget.d.ts +16 -0
- package/dist/adapters/mui/chartSetup.d.ts +2 -0
- package/dist/adapters/mui/chartTheme.d.ts +71 -0
- package/dist/adapters/mui/index.d.ts +12 -0
- package/dist/adapters/mui/palette.d.ts +9 -0
- package/dist/adapters/mui/useChartData.d.ts +14 -0
- package/dist/core/formatBucket.d.ts +4 -0
- package/dist/core/label.d.ts +3 -0
- package/dist/core/queryParams.d.ts +3 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/useDashboard.d.ts +50 -0
- package/dist/core/useReportExport.d.ts +7 -0
- package/dist/core/useReportList.d.ts +7 -0
- package/dist/core/useReportPreview.d.ts +7 -0
- package/dist/core/useReportSeries.d.ts +8 -0
- package/dist/core/useReportSpec.d.ts +9 -0
- package/dist/core/useReportTable.d.ts +10 -0
- package/dist/core/visualizationLayout.d.ts +13 -0
- package/dist/core/visualizationRegistry.d.ts +14 -0
- package/dist/createReportsResource.d.ts +40 -0
- package/dist/i18n/en.d.ts +83 -0
- package/dist/i18n/es.d.ts +83 -0
- package/{src/index.tsx → dist/index.d.ts} +3 -31
- package/package.json +13 -10
- package/src/ReportFilters.tsx +0 -145
- package/src/ReportFiltersDrawer.tsx +0 -73
- package/src/ReportPage.tsx +0 -280
- package/src/ReportsDashboard.tsx +0 -61
- package/src/ReportsIndex.tsx +0 -113
- package/src/ReportsModule.tsx +0 -42
- package/src/TenantDashboard.tsx +0 -290
- package/src/adapters/mui/BarWidget.tsx +0 -66
- package/src/adapters/mui/DoughnutWidget.tsx +0 -112
- package/src/adapters/mui/FunnelWidget.tsx +0 -108
- package/src/adapters/mui/KpiWidget.tsx +0 -123
- package/src/adapters/mui/LineWidget.tsx +0 -70
- package/src/adapters/mui/PieWidget.tsx +0 -16
- package/src/adapters/mui/PreviewWidget.tsx +0 -123
- package/src/adapters/mui/TableWidget.tsx +0 -141
- package/src/adapters/mui/chartSetup.ts +0 -53
- package/src/adapters/mui/chartTheme.ts +0 -84
- package/src/adapters/mui/index.ts +0 -29
- package/src/adapters/mui/palette.ts +0 -85
- package/src/adapters/mui/useChartData.ts +0 -62
- package/src/core/formatBucket.ts +0 -73
- package/src/core/label.ts +0 -28
- package/src/core/queryParams.ts +0 -27
- package/src/core/types.ts +0 -157
- package/src/core/useDashboard.ts +0 -161
- package/src/core/useReportExport.ts +0 -68
- package/src/core/useReportList.ts +0 -51
- package/src/core/useReportPreview.ts +0 -68
- package/src/core/useReportSeries.ts +0 -59
- package/src/core/useReportSpec.ts +0 -79
- package/src/core/useReportTable.ts +0 -71
- package/src/core/visualizationLayout.ts +0 -34
- package/src/core/visualizationRegistry.ts +0 -39
- package/src/createReportsResource.tsx +0 -81
- package/src/i18n/en.ts +0 -95
- package/src/i18n/es.ts +0 -84
- /package/{src/i18n/index.ts → dist/i18n/index.d.ts} +0 -0
package/src/ReportsDashboard.tsx
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { Alert, Box, LinearProgress, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { useDashboardTenants } from './core/useDashboard';
|
|
3
|
-
import { TenantDashboard } from './TenantDashboard';
|
|
4
|
-
import type { TranslateFn } from './core/types';
|
|
5
|
-
|
|
6
|
-
export interface ReportsDashboardProps {
|
|
7
|
-
translate: TranslateFn;
|
|
8
|
-
title?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The home-dashboard stats block.
|
|
13
|
-
*
|
|
14
|
-
* Renders one TenantDashboard per tenant the caller manages — a TenancyAdmin
|
|
15
|
-
* sees every restaurant, a tenant user sees exactly one (and then without a
|
|
16
|
-
* redundant heading naming the only tenant there is).
|
|
17
|
-
*
|
|
18
|
-
* Always live: the window is the tenant's currently-open cash count period, and
|
|
19
|
-
* an operator watching mid-service wants what has happened so far, not a figure
|
|
20
|
-
* frozen earlier. Looking back at a closed period is a deliberate act via the
|
|
21
|
-
* per-tenant period selector, and that view is served from the snapshot taken
|
|
22
|
-
* at closing.
|
|
23
|
-
*/
|
|
24
|
-
export function ReportsDashboard({ translate, title }: ReportsDashboardProps) {
|
|
25
|
-
const theme = useTheme();
|
|
26
|
-
const { tenants, loading, error } = useDashboardTenants();
|
|
27
|
-
|
|
28
|
-
if (loading) return <LinearProgress />;
|
|
29
|
-
|
|
30
|
-
if (error) {
|
|
31
|
-
return (
|
|
32
|
-
<Alert severity="warning" sx={{ my: 2 }}>
|
|
33
|
-
{translate('dash_reports.errors.dashboard', { _: 'These stats could not be loaded.' })}
|
|
34
|
-
</Alert>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (tenants.length === 0) return null;
|
|
39
|
-
|
|
40
|
-
const single = tenants.length === 1;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Box sx={{ mt: 2 }}>
|
|
44
|
-
<Typography variant="h5" sx={{ color: theme.palette.text.primary, mb: 2 }}>
|
|
45
|
-
{title ?? translate('dash_reports.dashboard.title', { _: 'This period' })}
|
|
46
|
-
</Typography>
|
|
47
|
-
|
|
48
|
-
{tenants.map((entry) => (
|
|
49
|
-
<TenantDashboard
|
|
50
|
-
key={entry.tenant.id}
|
|
51
|
-
tenantId={entry.tenant.id}
|
|
52
|
-
tenantName={entry.tenant.name}
|
|
53
|
-
translate={translate}
|
|
54
|
-
showHeading={!single}
|
|
55
|
-
/>
|
|
56
|
-
))}
|
|
57
|
-
</Box>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default ReportsDashboard;
|
package/src/ReportsIndex.tsx
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Alert, Box, Card, CardActionArea, CardContent, LinearProgress, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { useReportList } from './core/useReportList';
|
|
3
|
-
import { label } from './core/label';
|
|
4
|
-
import { useReportPreview } from './core/useReportPreview';
|
|
5
|
-
import { PreviewWidget } from './adapters/mui/PreviewWidget';
|
|
6
|
-
import type { ReportListEntry, TranslateFn } from './core/types';
|
|
7
|
-
|
|
8
|
-
export interface ReportsIndexProps {
|
|
9
|
-
translate: TranslateFn;
|
|
10
|
-
onOpen: (reportKey: string) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The report picker.
|
|
15
|
-
*
|
|
16
|
-
* Renders from the list endpoint alone — ONE request. The engine this replaces
|
|
17
|
-
* fetched every report's full dataset up front to draw its landing page (a
|
|
18
|
-
* monthly series plus one request per chart type, per report), so opening the
|
|
19
|
-
* index cost 2N requests before anyone had chosen anything. Data is fetched
|
|
20
|
-
* when a report is opened, not before.
|
|
21
|
-
*
|
|
22
|
-
* The list is already role-filtered server-side, so whatever arrives is what
|
|
23
|
-
* this caller may see.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* One report's card, with its own preview request.
|
|
27
|
-
*
|
|
28
|
-
* A component per card rather than one batched fetch, so the previews load in
|
|
29
|
-
* parallel and fail independently: a report whose preview errors shows a card
|
|
30
|
-
* without a chart instead of taking the whole list down with it.
|
|
31
|
-
*/
|
|
32
|
-
function ReportCard({
|
|
33
|
-
report, translate, onOpen,
|
|
34
|
-
}: { report: ReportListEntry; translate: TranslateFn; onOpen: (key: string) => void }) {
|
|
35
|
-
const theme = useTheme();
|
|
36
|
-
const { result, loading } = useReportPreview(
|
|
37
|
-
report.key,
|
|
38
|
-
report.previewGranularity,
|
|
39
|
-
report.defaultRangeDays,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Card variant="outlined">
|
|
44
|
-
<CardActionArea onClick={() => onOpen(report.key)} sx={{ height: '100%' }}>
|
|
45
|
-
<CardContent>
|
|
46
|
-
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
|
|
47
|
-
{label(translate, report.title, report.titleText)}
|
|
48
|
-
</Typography>
|
|
49
|
-
|
|
50
|
-
<PreviewWidget
|
|
51
|
-
result={result}
|
|
52
|
-
loading={loading}
|
|
53
|
-
type={report.defaultVisualization}
|
|
54
|
-
translate={translate}
|
|
55
|
-
metrics={report.metrics}
|
|
56
|
-
/>
|
|
57
|
-
|
|
58
|
-
</CardContent>
|
|
59
|
-
</CardActionArea>
|
|
60
|
-
</Card>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function ReportsIndex({ translate, onOpen }: ReportsIndexProps) {
|
|
65
|
-
const theme = useTheme();
|
|
66
|
-
const { reports, loading, error } = useReportList();
|
|
67
|
-
|
|
68
|
-
if (loading) return <LinearProgress />;
|
|
69
|
-
|
|
70
|
-
if (error) {
|
|
71
|
-
return (
|
|
72
|
-
<Alert severity="error" sx={{ m: 2 }}>
|
|
73
|
-
{translate('dash_reports.errors.list', { _: 'Reports could not be loaded.' })}
|
|
74
|
-
</Alert>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (reports.length === 0) {
|
|
79
|
-
return (
|
|
80
|
-
<Box sx={{ p: 4 }}>
|
|
81
|
-
<Typography variant="body1" sx={{ color: theme.palette.text.secondary }}>
|
|
82
|
-
{translate('dash_reports.no_reports', { _: 'No reports are available for your account.' })}
|
|
83
|
-
</Typography>
|
|
84
|
-
</Box>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<Box sx={{ p: { xs: 2, md: 3 } }}>
|
|
90
|
-
<Typography variant="h5" sx={{ color: theme.palette.text.primary, mb: 3 }}>
|
|
91
|
-
{translate('dash_reports.title', { _: 'Reports' })}
|
|
92
|
-
</Typography>
|
|
93
|
-
|
|
94
|
-
<Box
|
|
95
|
-
sx={{
|
|
96
|
-
display: 'grid',
|
|
97
|
-
gap: 2,
|
|
98
|
-
gridTemplateColumns: {
|
|
99
|
-
xs: '1fr',
|
|
100
|
-
sm: 'repeat(2, 1fr)',
|
|
101
|
-
lg: 'repeat(3, 1fr)',
|
|
102
|
-
},
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
105
|
-
{reports.map((report) => (
|
|
106
|
-
<ReportCard key={report.key} report={report} translate={translate} onOpen={onOpen} />
|
|
107
|
-
))}
|
|
108
|
-
</Box>
|
|
109
|
-
</Box>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default ReportsIndex;
|
package/src/ReportsModule.tsx
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Box, Button, Stack } from '@mui/material';
|
|
2
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
3
|
-
import { ReportsIndex } from './ReportsIndex';
|
|
4
|
-
import { ReportPage } from './ReportPage';
|
|
5
|
-
import type { TranslateFn } from './core/types';
|
|
6
|
-
|
|
7
|
-
export interface ReportsModuleProps {
|
|
8
|
-
translate: TranslateFn;
|
|
9
|
-
/** Base path this module is mounted at, e.g. '/reports'. */
|
|
10
|
-
basePath: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The whole reports module behind a single route.
|
|
15
|
-
*
|
|
16
|
-
* One resource, not one per report: the set of reports is decided by the
|
|
17
|
-
* BACKEND registry, so hardcoding a resource per report in the frontend would
|
|
18
|
-
* mean a frontend change (and a redeploy) every time a domain registers one —
|
|
19
|
-
* exactly the coupling the spec-driven design exists to remove. A domain that
|
|
20
|
-
* registers a sixth report gets it here with no frontend change at all.
|
|
21
|
-
*/
|
|
22
|
-
export function ReportsModule({ translate, basePath }: ReportsModuleProps) {
|
|
23
|
-
const { reportKey } = useParams();
|
|
24
|
-
const navigate = useNavigate();
|
|
25
|
-
|
|
26
|
-
if (!reportKey) {
|
|
27
|
-
return <ReportsIndex translate={translate} onOpen={(key) => navigate(`${basePath}/${key}`)} />;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Box>
|
|
32
|
-
<Stack direction="row" sx={{ px: { xs: 2, md: 3 }, pt: 2 }}>
|
|
33
|
-
<Button size="small" onClick={() => navigate(basePath)}>
|
|
34
|
-
← {translate('dash_reports.back_to_list', { _: 'All reports' })}
|
|
35
|
-
</Button>
|
|
36
|
-
</Stack>
|
|
37
|
-
<ReportPage reportKey={reportKey} translate={translate} />
|
|
38
|
-
</Box>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default ReportsModule;
|
package/src/TenantDashboard.tsx
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Alert, Box, Card, CardContent, Chip, FormControl, LinearProgress, MenuItem,
|
|
4
|
-
Select, Stack, Typography, useTheme,
|
|
5
|
-
} from '@mui/material';
|
|
6
|
-
import { useTenantDashboard, useTenantPeriods } from './core/useDashboard';
|
|
7
|
-
import { getVisualization } from './core/visualizationRegistry';
|
|
8
|
-
import { label as renderLabel } from './core/label';
|
|
9
|
-
import { visualizationBoxSx } from './core/visualizationLayout';
|
|
10
|
-
import type { DashboardWidget } from './core/useDashboard';
|
|
11
|
-
import type { ReportSpec, TranslateFn, VisualizationType } from './core/types';
|
|
12
|
-
import './adapters/mui';
|
|
13
|
-
|
|
14
|
-
export interface TenantDashboardProps {
|
|
15
|
-
tenantId: string;
|
|
16
|
-
tenantName?: string;
|
|
17
|
-
translate: TranslateFn;
|
|
18
|
-
/** Hide the tenant heading when only one tenant is ever shown. */
|
|
19
|
-
showHeading?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Pin to one period instead of letting the viewer choose.
|
|
22
|
-
*
|
|
23
|
-
* Used by the cash count show view, which is already about one specific
|
|
24
|
-
* period — offering a picker there would let someone navigate away from the
|
|
25
|
-
* record they are looking at without the surrounding page changing.
|
|
26
|
-
*/
|
|
27
|
-
period?: string;
|
|
28
|
-
/** Hide the period selector (implied when `period` is given). */
|
|
29
|
-
hidePeriodPicker?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Tile widths as a 12-column span, chosen so rows tile exactly.
|
|
34
|
-
*
|
|
35
|
-
* The previous set (sm 3 / md 4) left a 1-column gap on the first row and a
|
|
36
|
-
* 3-column gap on the second, which is why the dashboard looked ragged.
|
|
37
|
-
*/
|
|
38
|
-
const SPAN: Record<string, number> = { sm: 4, md: 6, lg: 8, full: 12 };
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The same, against the 6-column container breakpoint.
|
|
42
|
-
*
|
|
43
|
-
* `sm` is 2 rather than 3 so the three distribution tiles still tile one exact
|
|
44
|
-
* row here; at 3 they wrapped 2+1 and left an orphan.
|
|
45
|
-
*/
|
|
46
|
-
const SPAN_SM: Record<string, number> = { sm: 2, md: 6, lg: 6, full: 6 };
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* One tenant's period dashboard.
|
|
50
|
-
*
|
|
51
|
-
* Rendered once per tenant: a TenancyAdmin gets one of these per restaurant,
|
|
52
|
-
* a tenant user gets exactly one. Each fetches its own data, so a slow or
|
|
53
|
-
* failing tenant costs one panel rather than the page.
|
|
54
|
-
*
|
|
55
|
-
* The period selector defaults to the open cash count and can look back at
|
|
56
|
-
* closed ones. That distinction is surfaced, not hidden — a closed period is
|
|
57
|
-
* served from the snapshot taken when it was closed, and saying so is the
|
|
58
|
-
* difference between "these numbers are final" and "these are still moving".
|
|
59
|
-
*/
|
|
60
|
-
export function TenantDashboard({
|
|
61
|
-
tenantId,
|
|
62
|
-
tenantName,
|
|
63
|
-
translate,
|
|
64
|
-
showHeading = true,
|
|
65
|
-
period: pinnedPeriod,
|
|
66
|
-
hidePeriodPicker,
|
|
67
|
-
}: TenantDashboardProps) {
|
|
68
|
-
const theme = useTheme();
|
|
69
|
-
const [selectedPeriod, setPeriod] = useState('current');
|
|
70
|
-
|
|
71
|
-
const period = pinnedPeriod ?? selectedPeriod;
|
|
72
|
-
const showPicker = !pinnedPeriod && !hidePeriodPicker;
|
|
73
|
-
|
|
74
|
-
const { data, loading, error } = useTenantDashboard(tenantId, period);
|
|
75
|
-
// Not fetched at all when the picker is hidden — a request whose only purpose
|
|
76
|
-
// is to fill a control nobody will see.
|
|
77
|
-
const { periods } = useTenantPeriods(showPicker ? tenantId : null);
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<Box sx={{ mb: 4 }}>
|
|
81
|
-
{/* Title, period and picker share a card of their own.
|
|
82
|
-
Loose on the page they read as page furniture rather than as the
|
|
83
|
-
controls governing the tiles below; boxing them makes the relationship
|
|
84
|
-
explicit and gives the header the same visual weight as the widgets. */}
|
|
85
|
-
<Card variant="outlined" sx={{ mb: 2 }}>
|
|
86
|
-
<Stack
|
|
87
|
-
direction="row"
|
|
88
|
-
sx={{
|
|
89
|
-
alignItems: 'center',
|
|
90
|
-
justifyContent: 'space-between',
|
|
91
|
-
flexWrap: 'wrap',
|
|
92
|
-
gap: 1.5,
|
|
93
|
-
px: 2,
|
|
94
|
-
py: 1.5,
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
{showHeading && (
|
|
98
|
-
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
|
|
99
|
-
{tenantName ?? tenantId}
|
|
100
|
-
</Typography>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
<Stack direction="row" sx={{ alignItems: 'center', gap: 1 }}>
|
|
104
|
-
{data?.period && (
|
|
105
|
-
<Chip
|
|
106
|
-
size="small"
|
|
107
|
-
variant="outlined"
|
|
108
|
-
label={`${data.period.start.slice(0, 10)} → ${data.period.end.slice(0, 10)}`}
|
|
109
|
-
/>
|
|
110
|
-
)}
|
|
111
|
-
|
|
112
|
-
{/* A closed period's figures are frozen; an open one is still moving.
|
|
113
|
-
Conflating the two would let someone quote a number that changes
|
|
114
|
-
under them ten minutes later. */}
|
|
115
|
-
{data && (
|
|
116
|
-
<Chip
|
|
117
|
-
size="small"
|
|
118
|
-
color={data.period?.isOpen ? 'warning' : 'default'}
|
|
119
|
-
label={translate(
|
|
120
|
-
data.period?.isOpen ? 'dash_reports.period.open' : 'dash_reports.period.closed',
|
|
121
|
-
{ _: data.period?.isOpen ? 'In progress' : 'Closed' },
|
|
122
|
-
)}
|
|
123
|
-
/>
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
{showPicker && periods.length > 1 && (
|
|
127
|
-
<FormControl size="small" sx={{ minWidth: 190 }}>
|
|
128
|
-
<Select value={selectedPeriod} onChange={(e) => setPeriod(String(e.target.value))}>
|
|
129
|
-
{periods.map((p) => (
|
|
130
|
-
<MenuItem key={p.id} value={p.id}>
|
|
131
|
-
{p.id === 'current'
|
|
132
|
-
? translate('dash_reports.period.current', { _: 'Current period' })
|
|
133
|
-
: p.label}
|
|
134
|
-
</MenuItem>
|
|
135
|
-
))}
|
|
136
|
-
</Select>
|
|
137
|
-
</FormControl>
|
|
138
|
-
)}
|
|
139
|
-
</Stack>
|
|
140
|
-
</Stack>
|
|
141
|
-
</Card>
|
|
142
|
-
|
|
143
|
-
{loading && <LinearProgress />}
|
|
144
|
-
|
|
145
|
-
{error && (
|
|
146
|
-
<Alert severity="warning">
|
|
147
|
-
{translate('dash_reports.errors.dashboard', { _: 'These stats could not be loaded.' })}
|
|
148
|
-
</Alert>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
{data && (
|
|
152
|
-
<Box
|
|
153
|
-
sx={{
|
|
154
|
-
// Container queries, not viewport breakpoints. This block is
|
|
155
|
-
// rendered both full-width and inside a 50% column, and viewport
|
|
156
|
-
// breakpoints cannot tell those apart — a wide window would lay out
|
|
157
|
-
// twelve columns inside a half-width box and shred the tiles.
|
|
158
|
-
containerType: 'inline-size',
|
|
159
|
-
display: 'grid',
|
|
160
|
-
gap: 2,
|
|
161
|
-
gridTemplateColumns: 'repeat(12, 1fr)',
|
|
162
|
-
'@container (max-width: 900px)': { gridTemplateColumns: 'repeat(6, 1fr)' },
|
|
163
|
-
'@container (max-width: 560px)': { gridTemplateColumns: '1fr' },
|
|
164
|
-
// Without this, every tile stretches to the tallest in its row —
|
|
165
|
-
// a short KPI tile next to a seven-stage funnel became a mostly
|
|
166
|
-
// empty box the height of the funnel.
|
|
167
|
-
alignItems: 'start',
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
{data.widgets.map((widget) => (
|
|
171
|
-
<WidgetTile key={widget.key} widget={widget} translate={translate} />
|
|
172
|
-
))}
|
|
173
|
-
</Box>
|
|
174
|
-
)}
|
|
175
|
-
</Box>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function WidgetTile({ widget, translate }: { widget: DashboardWidget; translate: TranslateFn }) {
|
|
180
|
-
const theme = useTheme();
|
|
181
|
-
const span = SPAN[widget.size ?? 'md'] ?? 4;
|
|
182
|
-
|
|
183
|
-
const Visualization = widget.visualization ? getVisualization(widget.visualization as VisualizationType) : null;
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<Card
|
|
187
|
-
variant="outlined"
|
|
188
|
-
sx={{
|
|
189
|
-
gridColumn: `span ${span}`,
|
|
190
|
-
// Matches the container breakpoints on the grid above.
|
|
191
|
-
'@container (max-width: 900px)': {
|
|
192
|
-
gridColumn: `span ${SPAN_SM[widget.size ?? 'md'] ?? 6}`,
|
|
193
|
-
},
|
|
194
|
-
'@container (max-width: 560px)': { gridColumn: '1 / -1' },
|
|
195
|
-
}}
|
|
196
|
-
>
|
|
197
|
-
<CardContent>
|
|
198
|
-
<Typography variant="subtitle2" sx={{ color: theme.palette.text.secondary, mb: 1 }}>
|
|
199
|
-
{renderLabel(translate, widget.title, widget.titleText)}
|
|
200
|
-
</Typography>
|
|
201
|
-
|
|
202
|
-
{widget.error ? (
|
|
203
|
-
// One tile's failure is one tile, never the dashboard.
|
|
204
|
-
<Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
|
|
205
|
-
{translate('dash_reports.errors.widget', { _: 'Unavailable' })}
|
|
206
|
-
</Typography>
|
|
207
|
-
) : widget.result && Visualization && hasData(widget) ? (
|
|
208
|
-
// Canvas widgets get a definite height; Kpi/Funnel/Table size to
|
|
209
|
-
// their content. Without the bound, Chart.js grows the tile forever.
|
|
210
|
-
<Box sx={visualizationBoxSx(widget.visualization, 220)}>
|
|
211
|
-
<Visualization
|
|
212
|
-
result={widget.result}
|
|
213
|
-
spec={specFor(widget)}
|
|
214
|
-
translate={translate}
|
|
215
|
-
/>
|
|
216
|
-
</Box>
|
|
217
|
-
) : (
|
|
218
|
-
<Box sx={{ py: 3, display: 'grid', placeItems: 'center' }}>
|
|
219
|
-
<Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
|
|
220
|
-
{translate('dash_reports.no_data', { _: 'No data for this range.' })}
|
|
221
|
-
</Typography>
|
|
222
|
-
</Box>
|
|
223
|
-
)}
|
|
224
|
-
</CardContent>
|
|
225
|
-
</Card>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* The minimal spec a widget renderer needs.
|
|
231
|
-
*
|
|
232
|
-
* A tile does not fetch the report's full spec — that would be a second request
|
|
233
|
-
* per tile for information the series response already carries. The renderers
|
|
234
|
-
* only read `metrics` (for Kpi and Funnel labels) and `dimensions` (for stable
|
|
235
|
-
* colour slots), so this supplies exactly that.
|
|
236
|
-
*/
|
|
237
|
-
/**
|
|
238
|
-
* Whether a tile has anything worth drawing.
|
|
239
|
-
*
|
|
240
|
-
* A doughnut whose every slice is zero renders as an empty ring with a legend
|
|
241
|
-
* of nothings — strictly worse than saying "no data", because it looks like a
|
|
242
|
-
* chart that failed rather than a period in which nothing happened.
|
|
243
|
-
*/
|
|
244
|
-
function hasData(widget: DashboardWidget): boolean {
|
|
245
|
-
const result = widget.result;
|
|
246
|
-
if (!result) return false;
|
|
247
|
-
|
|
248
|
-
const anySeries = (result.series ?? []).some((s) =>
|
|
249
|
-
(s.data ?? []).some((v) => v !== null && v !== undefined && v !== 0),
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const anyTotal = Object.values(result.totals ?? {}).some(
|
|
253
|
-
(v) => v !== null && v !== undefined && v !== 0,
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
return anySeries || anyTotal;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function specFor(widget: DashboardWidget): ReportSpec {
|
|
260
|
-
// The backend ships the real definitions; the series fallback exists only
|
|
261
|
-
// for a pivoted tile, whose series are dimension values rather than metrics.
|
|
262
|
-
// Falling back for a metric tile would lose `decimal` and format an average
|
|
263
|
-
// of 0.26 minutes as "0".
|
|
264
|
-
const metrics = widget.result?.metrics
|
|
265
|
-
?? (widget.result?.series ?? []).map((s) => ({
|
|
266
|
-
key: s.key,
|
|
267
|
-
label: s.label,
|
|
268
|
-
labelText: s.labelText,
|
|
269
|
-
kind: 'count' as const,
|
|
270
|
-
decimal: false,
|
|
271
|
-
}));
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
key: widget.reportKey,
|
|
275
|
-
title: widget.title,
|
|
276
|
-
granularities: ['total'],
|
|
277
|
-
defaultGranularity: 'total',
|
|
278
|
-
defaultVisualization: (widget.visualization ?? 'Bar') as VisualizationType,
|
|
279
|
-
previewGranularity: 'total',
|
|
280
|
-
timezone: widget.result?.timezone ?? 'UTC',
|
|
281
|
-
dimensions: [],
|
|
282
|
-
metrics,
|
|
283
|
-
visualizations: [],
|
|
284
|
-
filters: {},
|
|
285
|
-
exporters: [],
|
|
286
|
-
defaultRangeDays: 30,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export default TenantDashboard;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Bar } 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
|
-
* Magnitude per bucket.
|
|
11
|
-
*
|
|
12
|
-
* Bars are thin with a small rounded cap on the data end only — the baseline
|
|
13
|
-
* end stays square so the bar reads as anchored rather than floating. Grouped
|
|
14
|
-
* rather than stacked by default: a stack answers "what is the total", a group
|
|
15
|
-
* answers "how do these compare", and comparison is what a pivoted report is
|
|
16
|
-
* for. Adjacent fills carry a surface-coloured gap instead of a stroke, so
|
|
17
|
-
* they separate without a border drawn around every mark.
|
|
18
|
-
*/
|
|
19
|
-
export function BarWidget(props: VisualizationProps) {
|
|
20
|
-
const { chrome, datasets, labels, longLabels } = useChartData(props);
|
|
21
|
-
const stacked = props.options?.stacked === true;
|
|
22
|
-
|
|
23
|
-
const data = {
|
|
24
|
-
labels,
|
|
25
|
-
datasets: datasets.map((d) => ({
|
|
26
|
-
label: d.label,
|
|
27
|
-
data: d.data,
|
|
28
|
-
backgroundColor: d.color,
|
|
29
|
-
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
|
|
30
|
-
borderSkipped: 'bottom' as const,
|
|
31
|
-
borderWidth: 0,
|
|
32
|
-
maxBarThickness: 28,
|
|
33
|
-
categoryPercentage: 0.7,
|
|
34
|
-
barPercentage: 0.9,
|
|
35
|
-
})),
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const base = baseChartOptions(chrome, datasets.length > 1);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<Bar
|
|
42
|
-
data={data}
|
|
43
|
-
height={props.height ?? 320}
|
|
44
|
-
options={{
|
|
45
|
-
...base,
|
|
46
|
-
scales: {
|
|
47
|
-
...base.scales,
|
|
48
|
-
x: { ...base.scales.x, stacked },
|
|
49
|
-
y: { ...base.scales.y, stacked },
|
|
50
|
-
},
|
|
51
|
-
plugins: {
|
|
52
|
-
...base.plugins,
|
|
53
|
-
tooltip: {
|
|
54
|
-
...base.plugins.tooltip,
|
|
55
|
-
callbacks: {
|
|
56
|
-
title: (items: Array<{ dataIndex: number }>) =>
|
|
57
|
-
longLabels[items[0]?.dataIndex] ?? '',
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
}}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export default BarWidget;
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { useTheme } from '@mui/material';
|
|
3
|
-
import { Doughnut } from 'react-chartjs-2';
|
|
4
|
-
import { ensureChartsRegistered } from './chartSetup';
|
|
5
|
-
import { chartChrome } from './chartTheme';
|
|
6
|
-
import { label as renderLabel } from '../../core/label';
|
|
7
|
-
import { buildSlotMap, colorForSlot, MAX_SERIES } from './palette';
|
|
8
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
9
|
-
|
|
10
|
-
ensureChartsRegistered();
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Composition across the whole window.
|
|
14
|
-
*
|
|
15
|
-
* Collapses the timeline: each series becomes its window total, so this answers
|
|
16
|
-
* "what share" rather than "when". Only meaningful for a part-of-whole reading,
|
|
17
|
-
* which is why the backend offers it on some reports and not others.
|
|
18
|
-
*
|
|
19
|
-
* A 2px surface-coloured gap between arcs separates neighbours without drawing
|
|
20
|
-
* a stroke around every slice.
|
|
21
|
-
*/
|
|
22
|
-
export function DoughnutWidget({ result, spec, translate, height }: VisualizationProps) {
|
|
23
|
-
const theme = useTheme();
|
|
24
|
-
|
|
25
|
-
const { labels, values, colors, chrome, total } = useMemo(() => {
|
|
26
|
-
const c = chartChrome(theme);
|
|
27
|
-
const pivotKey = result.meta?.pivot ?? undefined;
|
|
28
|
-
const slots = buildSlotMap(spec, pivotKey, result.series.map((s) => s.key));
|
|
29
|
-
|
|
30
|
-
const totals = result.series.map((series) => ({
|
|
31
|
-
key: series.key,
|
|
32
|
-
label: renderLabel(translate, series.label, series.labelText),
|
|
33
|
-
slot: slots.get(series.key) ?? 0,
|
|
34
|
-
value: series.data.reduce<number>((sum, v) => sum + (v ?? 0), 0),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
// Fold the tail rather than inventing a ninth hue.
|
|
38
|
-
const kept = totals.filter((t) => t.slot < MAX_SERIES);
|
|
39
|
-
const folded = totals.filter((t) => t.slot >= MAX_SERIES);
|
|
40
|
-
|
|
41
|
-
if (folded.length > 0) {
|
|
42
|
-
kept.push({
|
|
43
|
-
key: '__other__',
|
|
44
|
-
label: translate('dash_reports.other', { _: 'Other', count: folded.length }),
|
|
45
|
-
slot: MAX_SERIES,
|
|
46
|
-
value: folded.reduce((sum, t) => sum + t.value, 0),
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
chrome: c,
|
|
52
|
-
labels: kept.map((t) => t.label),
|
|
53
|
-
values: kept.map((t) => t.value),
|
|
54
|
-
colors: kept.map((t) => colorForSlot(t.slot, c.dark)),
|
|
55
|
-
total: kept.reduce((sum, t) => sum + t.value, 0),
|
|
56
|
-
};
|
|
57
|
-
}, [result, spec, translate, theme]);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<Doughnut
|
|
61
|
-
height={height ?? 320}
|
|
62
|
-
data={{
|
|
63
|
-
labels,
|
|
64
|
-
datasets: [{
|
|
65
|
-
data: values,
|
|
66
|
-
backgroundColor: colors,
|
|
67
|
-
borderColor: chrome.surface,
|
|
68
|
-
borderWidth: 2,
|
|
69
|
-
}],
|
|
70
|
-
}}
|
|
71
|
-
options={{
|
|
72
|
-
responsive: true,
|
|
73
|
-
maintainAspectRatio: false,
|
|
74
|
-
cutout: '58%',
|
|
75
|
-
plugins: {
|
|
76
|
-
legend: {
|
|
77
|
-
display: true,
|
|
78
|
-
position: 'bottom' as const,
|
|
79
|
-
labels: {
|
|
80
|
-
color: chrome.axisText,
|
|
81
|
-
usePointStyle: true,
|
|
82
|
-
pointStyle: 'circle' as const,
|
|
83
|
-
boxWidth: 8,
|
|
84
|
-
boxHeight: 8,
|
|
85
|
-
padding: 16,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
tooltip: {
|
|
89
|
-
backgroundColor: chrome.tooltipBg,
|
|
90
|
-
titleColor: chrome.tooltipText,
|
|
91
|
-
bodyColor: chrome.tooltipText,
|
|
92
|
-
borderColor: chrome.grid,
|
|
93
|
-
borderWidth: 1,
|
|
94
|
-
padding: 10,
|
|
95
|
-
cornerRadius: 6,
|
|
96
|
-
usePointStyle: true,
|
|
97
|
-
callbacks: {
|
|
98
|
-
// The share is the point of this chart, so the tooltip states it
|
|
99
|
-
// rather than leaving the reader to estimate an angle.
|
|
100
|
-
label: (item: { label?: string; parsed: number }) => {
|
|
101
|
-
const share = total > 0 ? Math.round((item.parsed / total) * 1000) / 10 : 0;
|
|
102
|
-
return `${item.label}: ${item.parsed.toLocaleString()} (${share}%)`;
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
}}
|
|
108
|
-
/>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export default DoughnutWidget;
|