@bygd/nc-report-ui 0.1.6 → 0.1.8

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.
@@ -0,0 +1,1012 @@
1
+ import * as React from 'react';
2
+ import React__default, { useEffect, useMemo, useState, useRef } from 'react';
3
+ import Paper from '@material-ui/core/Paper';
4
+ import { makeStyles } from '@material-ui/core/styles';
5
+ import LinearProgress from '@material-ui/core/LinearProgress';
6
+ import { Chart as Chart$1 } from 'react-google-charts';
7
+ import numeral from 'numeral';
8
+ import axios from 'axios';
9
+ import Typography from '@material-ui/core/Typography';
10
+ import nunjucks from 'nunjucks';
11
+ import FormControl$2 from '@material-ui/core/FormControl';
12
+ import Select$1 from '@material-ui/core/Select';
13
+ import MenuItem$1 from '@material-ui/core/MenuItem';
14
+ import { useInView } from 'react-intersection-observer';
15
+ import { FormControl, Autocomplete, TextField, CircularProgress, Chip, Checkbox, FormHelperText } from '@mui/material';
16
+ import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
17
+ import CheckBoxIcon from '@mui/icons-material/CheckBox';
18
+ import Box from '@mui/material/Box';
19
+ import InputLabel from '@mui/material/InputLabel';
20
+ import MenuItem from '@mui/material/MenuItem';
21
+ import FormControl$1 from '@mui/material/FormControl';
22
+ import Select from '@mui/material/Select';
23
+ import EventEmitter from 'eventemitter3';
24
+ import Grid from '@material-ui/core/Grid';
25
+ import Container from '@material-ui/core/Container';
26
+
27
+ function _extends() {
28
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
29
+ for (var e = 1; e < arguments.length; e++) {
30
+ var t = arguments[e];
31
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
32
+ }
33
+ return n;
34
+ }, _extends.apply(null, arguments);
35
+ }
36
+
37
+ const useStyles$1 = makeStyles(theme => ({
38
+ root: {
39
+ width: '100%',
40
+ '& > * + *': {
41
+ marginTop: theme.spacing(2)
42
+ }
43
+ }
44
+ }));
45
+ function LinearIndeterminate(props) {
46
+ const classes = useStyles$1();
47
+ return /*#__PURE__*/React__default.createElement("div", _extends({
48
+ className: classes.root
49
+ }, props), /*#__PURE__*/React__default.createElement(LinearProgress, null));
50
+ }
51
+
52
+ function ValuePicker(view, schema, outputFormat = 'array') {
53
+ const columns = view.columns;
54
+ const metrics = schema.doc.metrics;
55
+ const dimensions = schema.doc.dimensions;
56
+ const all = [...metrics, ...dimensions];
57
+ const mapped = all.reduce((result, item) => {
58
+ result[item.name] = item;
59
+ return result;
60
+ }, {});
61
+ const _get = row => {
62
+ const output = outputFormat === 'array' ? [] : {};
63
+ const resp = columns.reduce((result, item) => {
64
+ const value = row[item.name];
65
+ const ref = mapped[item.name];
66
+ let formatted = value;
67
+ if (ref.type === 'timestamp') formatted = new Date(value);else if (!ref.format) formatted = value;else if (ref.format && ref.prefix) formatted = {
68
+ v: +value,
69
+ f: ref.prefix + " " + numeral(value).format(ref.format)
70
+ };else if (ref.format) formatted = {
71
+ v: +value,
72
+ f: numeral(value).format(ref.format)
73
+ };else formatted = value;
74
+ if (outputFormat === 'array') result.push(formatted);else result[item.name] = formatted?.f || formatted;
75
+ return result;
76
+ }, output);
77
+ return resp;
78
+ };
79
+ return _get;
80
+ }
81
+ function HeaderPicker(view, schema) {
82
+ const metrics = schema.doc.metrics;
83
+ const dimensions = schema.doc.dimensions;
84
+ const all = [...metrics, ...dimensions];
85
+ const result = view.columns.reduce((result, item) => {
86
+ const column = all.find(f => f.name === item.name) || item;
87
+ result.push(column.title || column.name);
88
+ return result;
89
+ }, []);
90
+ return result;
91
+ }
92
+
93
+ //const BASE_URL = 'http://localhost:8081';
94
+ const BASE_URL = 'https://report-api.netcapital.pro';
95
+ const apiClient = axios.create({
96
+ baseURL: BASE_URL,
97
+ headers: {
98
+ 'Content-Type': 'application/json'
99
+ }
100
+ });
101
+ const Api = {
102
+ getDashboard: async ({
103
+ id
104
+ }) => {
105
+ const {
106
+ data
107
+ } = await apiClient.get(`/entity/dashboards/${id}`);
108
+ return data;
109
+ },
110
+ /**
111
+ * Get chart by ID
112
+ * @param {Object} params - Parameters object
113
+ * @param {string} params.id - Chart ID
114
+ * @returns {Promise} Axios response promise
115
+ */
116
+ getChart: async ({
117
+ id
118
+ }) => {
119
+ const {
120
+ data
121
+ } = await apiClient.get(`/entity/charts/${id}`);
122
+ return data;
123
+ },
124
+ /**
125
+ * Get report by ID
126
+ * @param {Object} params - Parameters object
127
+ * @param {string} params.id - Report ID
128
+ * @returns {Promise} Axios response promise
129
+ */
130
+ getReport: async ({
131
+ id
132
+ }) => {
133
+ const {
134
+ data
135
+ } = await apiClient.get(`/entity/reports/${id}`);
136
+ return data;
137
+ },
138
+ /**
139
+ * Get report schema/metadata by ID
140
+ * @param {Object} params - Parameters object
141
+ * @param {string} params.id - Report ID
142
+ * @returns {Promise} Axios response promise
143
+ */
144
+ getReportSchema: async ({
145
+ id,
146
+ query = {}
147
+ }) => {
148
+ // console.log({getReportSchema:{id,query}});
149
+ const {
150
+ data
151
+ } = await apiClient.post(`/reports/${id}/metadata`, query);
152
+ return data;
153
+ },
154
+ /**
155
+ * Run report with optional query parameters
156
+ * @param {Object} params - Parameters object
157
+ * @param {string} params.id - Report ID
158
+ * @param {Object} [params.query] - Optional query object to send in request body
159
+ * @returns {Promise} Axios response promise
160
+ */
161
+ runReport: async ({
162
+ id,
163
+ query = {}
164
+ }) => {
165
+ // console.log({runReport:{id,query}});
166
+
167
+ const {
168
+ data
169
+ } = await apiClient.post(`/reports/${id}/run`, query);
170
+ return data;
171
+ },
172
+ getDateRanges: async () => {
173
+ const {
174
+ data
175
+ } = await apiClient.get(`/globals/date-ranges`);
176
+ return data;
177
+ }
178
+ };
179
+
180
+ const useStyles = makeStyles(theme => ({
181
+ headerRow: {
182
+ fontFamily: theme.typography.fontFamily,
183
+ fontSize: theme.typography.fontSize
184
+ },
185
+ tableRow: {
186
+ fontFamily: theme.typography.fontFamily,
187
+ fontSize: theme.typography.fontSize
188
+ },
189
+ oddTableRow: {
190
+ fontFamily: theme.typography.fontFamily,
191
+ fontSize: theme.typography.fontSize
192
+ },
193
+ headerCell: {
194
+ backgroundColor: "white",
195
+ padding: "4px !important"
196
+ }
197
+ // tableCell:{
198
+ // // padding:"4px !important"
199
+ // },
200
+ }));
201
+ const dateRangeFormats = {
202
+ "none": "YYYY-MM-dd",
203
+ "day": "MMM dd",
204
+ "week": "MMM dd",
205
+ "month": "YYYY MMM",
206
+ "year": "YYYY"
207
+ };
208
+ function GoogleChart({
209
+ chart,
210
+ source,
211
+ view,
212
+ report,
213
+ schema,
214
+ dashboard,
215
+ query
216
+ }) {
217
+ const [data, setData] = React__default.useState([]);
218
+ const classes = useStyles();
219
+ const [dateRanges, setDateRanges] = React__default.useState([]);
220
+ useEffect(() => {
221
+ (async () => {
222
+ const data = await Api.getDateRanges();
223
+ setDateRanges(data);
224
+ })();
225
+ }, []);
226
+ const dateRangeFormat = useMemo(() => {
227
+ const defaultFormat = 'YYYY-MM-dd';
228
+ const name = query?.date_range?.name;
229
+ const dateRange = dateRanges.find(f => f.name === name);
230
+ if (!dateRange) return defaultFormat;
231
+ const format = dateRangeFormats[dateRange?.granularity] || defaultFormat;
232
+ return format;
233
+ }, [dateRanges, query]);
234
+ const getOptions = data => {
235
+ if (chart?.doc.chart?.type === 'Table') {
236
+ return {
237
+ ...chart?.doc.chart?.options,
238
+ cssClassNames: classes
239
+ };
240
+ } else if (chart?.doc.chart?.type === 'AreaChart') {
241
+ const hAxisFromChart = chart?.doc.chart?.options?.hAxis || {};
242
+ const hAxisDefaults = {
243
+ ticks: data.slice(1).map(row => row[0]),
244
+ format: dateRangeFormat
245
+ };
246
+ const hAxis = {
247
+ ...hAxisDefaults,
248
+ ...hAxisFromChart
249
+ };
250
+
251
+ // console.log({GoogleChart:{chart,source,view,report,schema,query,dateRanges,dateRangeFormat,ticks:hAxis.ticks}})
252
+
253
+ return {
254
+ ...chart?.doc.chart?.options,
255
+ ...{
256
+ hAxis,
257
+ focusTarget: 'category'
258
+ }
259
+ };
260
+ } else return chart?.doc.chart?.options;
261
+ };
262
+ React__default.useEffect(() => {
263
+ if (!chart || !view || !source || !report || !schema) return;
264
+ let result = [];
265
+ result.push(HeaderPicker(view, schema));
266
+ const valuePicker = ValuePicker(view, schema);
267
+ source?.forEach(item => {
268
+ const val = valuePicker(item);
269
+ result.push(val);
270
+ });
271
+ source?.length ? setData(result) : setData();
272
+ }, [chart, source, view, report, schema]);
273
+ return data ? /*#__PURE__*/React__default.createElement(Chart$1, _extends({
274
+ width: chart?.doc.size?.width || '100%',
275
+ height: dashboard ? chart?.doc.size?.height || '300px' : '100%',
276
+ chartType: chart?.doc.chart?.type,
277
+ data: data,
278
+ options: getOptions(data)
279
+ }, chart?.doc.chart?.props)) : /*#__PURE__*/React__default.createElement("div", null);
280
+ }
281
+
282
+ function LabelChart({
283
+ chart,
284
+ source,
285
+ view,
286
+ report,
287
+ schema
288
+ }) {
289
+ const [data, setData] = React__default.useState([]);
290
+ const [value, setValue] = React__default.useState();
291
+ const [subText, setSubText] = React__default.useState();
292
+ React__default.useEffect(() => {
293
+ if (!chart || !view || !source || !report || !schema) return;
294
+ let result = [];
295
+ // result.push(HeaderPicker(view, schema));
296
+ const valuePicker = ValuePicker(view, schema);
297
+ source.forEach(item => result.push(valuePicker(item)));
298
+ setData(result);
299
+ const options = chart?.doc?.chart?.options || {};
300
+ const valueIndex = options?.index || 0;
301
+ if (result.length > 0) setValue(result[0][valueIndex]);
302
+ if (options.subText?.format) {
303
+ nunjucks.configure({
304
+ autoescape: false
305
+ });
306
+ const valuePickerObj = ValuePicker(view, schema, 'object');
307
+ const val = valuePickerObj(source[0]);
308
+ const subTextRendered = nunjucks.renderString(options.subText?.format, val);
309
+ setSubText(subTextRendered);
310
+ }
311
+ }, [chart, source, view, report, schema]);
312
+ return /*#__PURE__*/React__default.createElement("div", {
313
+ style: {
314
+ display: "flex",
315
+ justifyContent: "center",
316
+ alignContent: "center",
317
+ alignItems: "center",
318
+ height: "100%",
319
+ flexDirection: "column"
320
+ }
321
+ }, /*#__PURE__*/React__default.createElement(Typography, {
322
+ variant: "h6"
323
+ }, value?.f || value), /*#__PURE__*/React__default.createElement(Typography, {
324
+ variant: "subtitle1"
325
+ }, subText));
326
+ }
327
+
328
+ const Components = {
329
+ GoogleChart,
330
+ LabelChart
331
+ };
332
+ function Internal(name, props) {
333
+ if (typeof Components[name] !== "undefined") {
334
+ return /*#__PURE__*/React__default.createElement(Components[name], props);
335
+ } else return /*#__PURE__*/React__default.createElement(LinearIndeterminate, props);
336
+ }
337
+
338
+ /**
339
+ * CheckboxMultiAutocomplete
340
+ *
341
+ * Props:
342
+ * - items: Array<{ key: string, value: string }>
343
+ * - selectedKeys?: string[] (controlled)
344
+ * - defaultSelectedKeys?: string[] (uncontrolled)
345
+ * - onChange?: (selectedKeys: string[], selectedItems: {key, value}[]) => void
346
+ * - inputValue?: string (controlled input)
347
+ * - defaultInputValue?: string (uncontrolled input)
348
+ * - onInputChange?: (text: string) => void (debounced by debounceMs)
349
+ * - debounceMs?: number (default 300)
350
+ * - label?, placeholder?, loading?, disabled?, size? = 'small', error?, helperText?,
351
+ * limitTags? = 3, disableClearable?, id?, textFieldProps?
352
+ */
353
+
354
+ function useDebouncedCallback(fn, delay = 300) {
355
+ const fnRef = useRef(fn);
356
+ const timer = useRef(null);
357
+ fnRef.current = fn;
358
+ useEffect(() => () => {
359
+ if (timer.current) window.clearTimeout(timer.current);
360
+ }, []);
361
+ return React.useCallback((...args) => {
362
+ if (!fnRef.current) return;
363
+ if (timer.current) window.clearTimeout(timer.current);
364
+ timer.current = window.setTimeout(() => {
365
+ fnRef.current && fnRef.current(...args);
366
+ }, delay);
367
+ }, [delay]);
368
+ }
369
+ const icon = /*#__PURE__*/React.createElement(CheckBoxOutlineBlankIcon, {
370
+ fontSize: "small"
371
+ });
372
+ const checkedIcon = /*#__PURE__*/React.createElement(CheckBoxIcon, {
373
+ fontSize: "small"
374
+ });
375
+ function CheckboxMultiAutocomplete({
376
+ items,
377
+ selectedKeys,
378
+ defaultSelectedKeys,
379
+ onChange,
380
+ inputValue,
381
+ defaultInputValue,
382
+ onInputChange,
383
+ debounceMs = 300,
384
+ label,
385
+ placeholder,
386
+ loading,
387
+ disabled,
388
+ size = "small",
389
+ error,
390
+ helperText,
391
+ limitTags = 3,
392
+ disableClearable,
393
+ id,
394
+ textFieldProps
395
+ }) {
396
+ // Normalize props to avoid "is not iterable" and similar runtime errors
397
+ const safeItems = useMemo(() => Array.isArray(items) ? items : [], [items]);
398
+
399
+ // Controlled/uncontrolled selection
400
+ const isSelectionControlled = Array.isArray(selectedKeys);
401
+ const [innerKeys, setInnerKeys] = useState(Array.isArray(defaultSelectedKeys) ? defaultSelectedKeys : []);
402
+ const currentKeys = isSelectionControlled ? selectedKeys : innerKeys;
403
+
404
+ // Map lookups for efficiency
405
+ const byKey = useMemo(() => {
406
+ const m = new Map();
407
+ for (const it of safeItems) m.set(it && it.key, it);
408
+ return m;
409
+ }, [safeItems]);
410
+ const selectedItems = useMemo(() => (Array.isArray(currentKeys) ? currentKeys : []).map(k => byKey.get(k)).filter(Boolean), [currentKeys, byKey]);
411
+
412
+ // Controlled/uncontrolled input
413
+ const isInputControlled = typeof inputValue === "string";
414
+ const [innerInput, setInnerInput] = useState(typeof defaultInputValue === "string" ? defaultInputValue : "");
415
+ const currentInput = isInputControlled ? inputValue : innerInput;
416
+ const debouncedInput = useDebouncedCallback(text => {
417
+ onInputChange && onInputChange(text);
418
+ }, debounceMs);
419
+ const valueForAutocomplete = selectedItems; // MUI expects full option objects
420
+
421
+ return /*#__PURE__*/React.createElement(FormControl, {
422
+ fullWidth: true,
423
+ error: error,
424
+ disabled: disabled
425
+ }, /*#__PURE__*/React.createElement(Autocomplete, {
426
+ openOnFocus: true,
427
+ forcePopupIcon: true,
428
+ id: id,
429
+ multiple: true,
430
+ disableCloseOnSelect: true,
431
+ options: safeItems,
432
+ value: valueForAutocomplete,
433
+ inputValue: currentInput,
434
+ getOptionLabel: o => o && typeof o === "object" ? o.value ?? "" : "",
435
+ isOptionEqualToValue: (option, value) => option && value && option.key === value.key,
436
+ loading: loading,
437
+ limitTags: limitTags,
438
+ disableClearable: disableClearable,
439
+ onChange: (event, newValue /* array of option objects */) => {
440
+ const newKeys = Array.isArray(newValue) ? newValue.map(o => o && o.key) : [];
441
+ if (!isSelectionControlled) setInnerKeys(newKeys);
442
+ onChange && onChange(newKeys, Array.isArray(newValue) ? newValue : []);
443
+ },
444
+ onInputChange: (event, newInput, reason) => {
445
+ if (!isInputControlled) setInnerInput(newInput ?? "");
446
+ if (reason === "input") {
447
+ debouncedInput(newInput ?? "");
448
+ } else if (reason === "clear") {
449
+ debouncedInput("");
450
+ }
451
+ },
452
+ renderOption: (props, option, {
453
+ selected
454
+ }) => /*#__PURE__*/React.createElement("li", _extends({}, props, {
455
+ key: option && option.key || Math.random().toString(36)
456
+ }), /*#__PURE__*/React.createElement(Checkbox, {
457
+ icon: icon,
458
+ checkedIcon: checkedIcon,
459
+ style: {
460
+ marginRight: 8
461
+ },
462
+ checked: !!selected
463
+ }), option && option.value),
464
+ renderTags: (tagValue, getTagProps) => (Array.isArray(tagValue) ? tagValue : []).map((option, index) => /*#__PURE__*/React.createElement(Chip, _extends({}, getTagProps({
465
+ index
466
+ }), {
467
+ key: option && option.key || index,
468
+ label: option && option.value || ""
469
+ }))),
470
+ renderInput: params => /*#__PURE__*/React.createElement(TextField, _extends({}, params, {
471
+ label: label,
472
+ placeholder: placeholder,
473
+ InputProps: {
474
+ ...params.InputProps,
475
+ endAdornment: /*#__PURE__*/React.createElement(React.Fragment, null, loading ? /*#__PURE__*/React.createElement(CircularProgress, {
476
+ size: 18
477
+ }) : null, params.InputProps.endAdornment)
478
+ },
479
+ size: size
480
+ }, textFieldProps))
481
+ }), helperText ? /*#__PURE__*/React.createElement(FormHelperText, null, helperText) : null);
482
+ }
483
+
484
+ async function fetchItems(query, filter) {
485
+ if (!filter?.source) return [];
486
+ const data = await Api.runReport({
487
+ id: filter.source
488
+ });
489
+ return data.map(d => {
490
+ const m = {
491
+ key: d[filter.field],
492
+ value: d[filter.field]
493
+ };
494
+ return m;
495
+ });
496
+ }
497
+ const ChartFilterMultiSelect = ({
498
+ filter,
499
+ channel,
500
+ query
501
+ }) => {
502
+ const [items, setItems] = useState([]);
503
+ const [selected, setSelected] = useState([]);
504
+ const [loading, setLoading] = useState(false);
505
+ const [inputQuery, setInputQuery] = useState("");
506
+ useEffect(() => {
507
+ setSelected(query?.filter?.[filter?.filter] || []);
508
+ }, [query]);
509
+ useEffect(() => {
510
+ (async () => {
511
+ setLoading(true);
512
+ const data = await fetchItems(query, filter);
513
+ setItems(data);
514
+ setLoading(false);
515
+ })();
516
+ }, [inputQuery]);
517
+ return /*#__PURE__*/React__default.createElement("div", {
518
+ style: {
519
+ display: "flex"
520
+ }
521
+ }, /*#__PURE__*/React__default.createElement("div", {
522
+ style: {
523
+ maxWidth: 640,
524
+ minWidth: 200
525
+ }
526
+ }, /*#__PURE__*/React__default.createElement(CheckboxMultiAutocomplete, {
527
+ id: filter?.title,
528
+ label: filter?.title,
529
+ placeholder: "Type to search\u2026",
530
+ items: items,
531
+ selectedKeys: selected,
532
+ onChange: (keys, selectedItems) => {
533
+ setSelected(keys);
534
+ channel.emit('filterChanged', filter, {
535
+ [filter.filter]: keys
536
+ });
537
+ },
538
+ onInputChange: text => {
539
+ setInputQuery(text);
540
+ },
541
+ loading: loading,
542
+ debounceMs: 300,
543
+ helperText: selected.length ? `${selected.length} selected` : ""
544
+ })));
545
+ };
546
+
547
+ function SingleSelect({
548
+ items,
549
+ value,
550
+ label,
551
+ onChange,
552
+ sx = {
553
+ width: '100%'
554
+ }
555
+ }) {
556
+ // Check if the current value exists in items, otherwise use empty string
557
+ const validValue = items.some(itm => itm.key === value) ? value : '';
558
+ return /*#__PURE__*/React__default.createElement(Box, {
559
+ sx: sx
560
+ }, /*#__PURE__*/React__default.createElement(FormControl$1, {
561
+ fullWidth: true
562
+ }, /*#__PURE__*/React__default.createElement(InputLabel, {
563
+ id: "demo-simple-select-label"
564
+ }, label), /*#__PURE__*/React__default.createElement(Select, {
565
+ labelId: "demo-simple-select-label",
566
+ id: "demo-simple-select",
567
+ value: validValue,
568
+ label: label,
569
+ onChange: onChange,
570
+ MenuProps: {
571
+ style: {
572
+ maxHeight: '300px'
573
+ }
574
+ }
575
+ // sx={{height:'40px'}}
576
+ ,
577
+ size: "small"
578
+ }, items.map(itm => {
579
+ const {
580
+ key,
581
+ value
582
+ } = itm;
583
+ return /*#__PURE__*/React__default.createElement(MenuItem, {
584
+ key: key,
585
+ value: key
586
+ }, value);
587
+ }))));
588
+ }
589
+
590
+ const ChartFilterDateRange = ({
591
+ filter,
592
+ channel,
593
+ query
594
+ }) => {
595
+ const [items, setItems] = useState([]);
596
+ const [selected, setSelected] = useState("");
597
+ useEffect(() => {
598
+ setSelected(query?.date_range?.name);
599
+ }, [query]);
600
+ useEffect(() => {
601
+ (async () => {
602
+ const data = await Api.getDateRanges();
603
+ const items = data.filter(f => filter.allowedOptions.length > 0 ? filter.allowedOptions.some(o => o === f.name) : true).map(d => {
604
+ return {
605
+ key: d.name,
606
+ value: d.title
607
+ };
608
+ });
609
+ setItems(items);
610
+ })();
611
+ }, []);
612
+ return /*#__PURE__*/React__default.createElement("div", {
613
+ style: {
614
+ minWidth: "200px",
615
+ maxWidth: "640px"
616
+ }
617
+ }, /*#__PURE__*/React__default.createElement(SingleSelect, {
618
+ items: items,
619
+ value: selected,
620
+ label: filter?.title,
621
+ onChange: (e, newValue) => {
622
+ const val = e.target.value;
623
+ setSelected(val);
624
+ channel.emit('filterChanged', filter, {
625
+ date_range: {
626
+ name: val
627
+ }
628
+ });
629
+ }
630
+ }));
631
+ };
632
+
633
+ const ChartFilters = ({
634
+ filters = [],
635
+ channel,
636
+ query
637
+ }) => {
638
+ return /*#__PURE__*/React__default.createElement("div", {
639
+ style: {
640
+ display: "flex",
641
+ flexWrap: "wrap",
642
+ gap: "8px"
643
+ }
644
+ }, filters.map(f => {
645
+ if (f.type === 'multi-select') {
646
+ return /*#__PURE__*/React__default.createElement(ChartFilterMultiSelect, {
647
+ key: f.title,
648
+ channel: channel,
649
+ filter: f,
650
+ query: query
651
+ });
652
+ } else if (f.type === 'date-range') {
653
+ return /*#__PURE__*/React__default.createElement(ChartFilterDateRange, {
654
+ key: f.title,
655
+ channel: channel,
656
+ filter: f,
657
+ query: query
658
+ });
659
+ } else {
660
+ return null;
661
+ }
662
+ }));
663
+ };
664
+
665
+ var useChannel = channel => {
666
+ const channelRef = React__default.useRef(channel || new EventEmitter());
667
+ // React.useEffect(() => {
668
+ // }, []);
669
+ return channelRef.current;
670
+ };
671
+
672
+ var useFilterManager = channel => {
673
+ const filterRef = React__default.useState({});
674
+ const onFilterChanged = React__default.useCallback((entity, value) => {
675
+ let merged = {
676
+ ...filterRef.current,
677
+ ...value
678
+ };
679
+ filterRef.current = merged;
680
+ for (var field in merged) {
681
+ if (field !== 'date_range' && !merged[field].length) delete merged[field];
682
+ }
683
+ channel?.emit('mergedFilterChanged', {
684
+ value: merged
685
+ });
686
+ }, [channel]);
687
+ React__default.useEffect(() => {
688
+ channel?.on('filterChanged', onFilterChanged);
689
+ return () => {
690
+ channel?.off('filterChanged', onFilterChanged);
691
+ };
692
+ }, [channel]);
693
+ };
694
+
695
+ var Chart = ({
696
+ id,
697
+ cache,
698
+ dashboard,
699
+ channel,
700
+ ...props
701
+ }) => {
702
+ const [chart, setChart] = React__default.useState();
703
+ const [report, setReport] = React__default.useState();
704
+ const [schema, setSchema] = React__default.useState();
705
+ const [source, setSource] = React__default.useState();
706
+ const [chartType, setChartType] = React__default.useState(Internal());
707
+ const [activeView, setActiveView] = React__default.useState();
708
+ const [activeViewIndex, setActiveViewIndex] = React__default.useState(0);
709
+ const [loading, setLoading] = React__default.useState(false);
710
+ const [filter, setFilter] = React__default.useState();
711
+ const [dirty, setDirty] = React__default.useState(false);
712
+ const filterRef = React__default.useRef({
713
+ value: props?.filter
714
+ });
715
+ const [currentQuery, setCurrentQuery] = React__default.useState({});
716
+ const {
717
+ ref,
718
+ inView,
719
+ entry
720
+ } = useInView({
721
+ /* Optional options */
722
+ threshold: 0.10,
723
+ delay: 1000
724
+ });
725
+ const channelUsed = useChannel(channel);
726
+ useFilterManager(channelUsed);
727
+
728
+ // console.log(id, inView);
729
+
730
+ const init = async () => {
731
+ // CHART
732
+ const chartId = id;
733
+ const chartTemp = cache?.[chartId] || (await Api.getChart({
734
+ id: chartId
735
+ }));
736
+
737
+ // console.log({chartTemp});
738
+
739
+ // REPORT
740
+ const reportId = chartTemp.doc.source?.id;
741
+ const reportTemp = reportId ? cache?.[reportId] || (await Api.getReport({
742
+ id: reportId
743
+ })) : undefined;
744
+
745
+ // SCHEMA
746
+ const schemaTemp = cache?.[`schema_${reportId}`] || props?.schema || (await Api.getReportSchema({
747
+ id: reportId
748
+ }));
749
+
750
+ // console.log({schemaTemp});
751
+
752
+ // ACTIVE VIEW
753
+ const activeViewTemp = chartTemp.doc?.view?.[0];
754
+ setActiveViewIndex(0);
755
+ setReport(reportTemp);
756
+ setChart(chartTemp);
757
+ setActiveView(activeViewTemp);
758
+ setSchema(schemaTemp);
759
+ //setSchema({});
760
+
761
+ setDirty(true);
762
+
763
+ // // SOURCE
764
+ // if (inView) {
765
+ // const sourceId = chartTemp.doc.source.id;
766
+ // const sourceTemp = await Report.run(undefined, { id: sourceId });
767
+ // setSource(chartTemp.doc.source.reverse ? sourceTemp.reverse() : sourceTemp);
768
+ // }
769
+ };
770
+ const reload = async () => {
771
+ setChartType(Internal(chart.doc.chart?.component, {
772
+ view: activeView,
773
+ source: source,
774
+ chart: chart,
775
+ report: report,
776
+ schema: schema,
777
+ dashboard: dashboard,
778
+ query: currentQuery
779
+ }));
780
+ };
781
+ const onViewChanged = React__default.useCallback(event => {
782
+ const viewIndex = event.target.value;
783
+ setActiveViewIndex(viewIndex);
784
+ setActiveView(chart.doc.view[viewIndex]);
785
+ }, [chart]);
786
+
787
+ // React.useEffect(() => {
788
+ // let cron;
789
+ // if (chart?.doc?.refresh?.enabled === true) {
790
+ // cron = new CronJob(chart.doc.refresh.cron, () => {
791
+ // if (inView) {
792
+ // loadSource(filterRef.current);
793
+ // }
794
+ // });
795
+ // cron.start();
796
+ // }
797
+
798
+ // return () => {
799
+ // cron && cron.stop();
800
+ // }
801
+ // }, [chart, inView]);
802
+
803
+ React__default.useEffect(() => {
804
+ if (!id) return;
805
+ init().catch(error => {
806
+ console.error(error.message);
807
+ });
808
+ }, [id]);
809
+ React__default.useEffect(() => {
810
+ if (!chart || !source || chart?.doc?.source?.id && !report || !schema) return;
811
+ reload().catch(error => {
812
+ console.error(error.message);
813
+ });
814
+ }, [chart, source, activeView, report, schema]);
815
+ const loadSource = userFilter => {
816
+ const queryFilter = report?.doc.query.filter || {};
817
+ const finalFilter = {
818
+ ...queryFilter,
819
+ ...props?.filter,
820
+ ...userFilter?.value
821
+ };
822
+
823
+ // const finalFilter = {...props?.filter,...userFilter?.value };
824
+
825
+ // console.log({queryFilter,userFilter,propsFilter:props.filter,finalFilter});
826
+
827
+ const date_range = finalFilter.date_range || report?.doc.query.date_range;
828
+ delete finalFilter.date_range;
829
+ setDirty(false);
830
+ setFilter();
831
+ setLoading(true);
832
+ if (chart?.doc?.source?.type === 'context') {
833
+ setSource({
834
+ filter: finalFilter
835
+ });
836
+ setLoading(false);
837
+ return;
838
+ }
839
+ const query = {
840
+ filter: finalFilter,
841
+ date_range,
842
+ parameters: props?.params
843
+ };
844
+
845
+ //console.log('loadSource',{reportId: report.id, query});
846
+
847
+ setCurrentQuery(query);
848
+ Api.runReport({
849
+ id: report.id,
850
+ query
851
+ }).then(response => {
852
+ setSource(chart.doc.source.reverse ? response.reverse() : response);
853
+ }).catch(error => {
854
+ setSource([]);
855
+ }).finally(() => {
856
+ setLoading(false);
857
+ });
858
+ };
859
+ const onMergedFilterChanged = merged => {
860
+ // console.log({onMergedFilterChanged:merged});
861
+ filterRef.current = merged;
862
+ if (inView) loadSource(merged);else {
863
+ setFilter(merged);
864
+ setDirty(true);
865
+ }
866
+ };
867
+ React__default.useEffect(() => {
868
+ channelUsed?.on('mergedFilterChanged', onMergedFilterChanged);
869
+ return () => {
870
+ channelUsed?.off('mergedFilterChanged', onMergedFilterChanged);
871
+ };
872
+ }, [channelUsed, report, chart, inView]);
873
+ React__default.useEffect(() => {
874
+ if (inView && dirty) loadSource(filter);
875
+ }, [inView, dirty]);
876
+ return /*#__PURE__*/React__default.createElement(Paper, {
877
+ ref: ref,
878
+ style: {
879
+ margin: "4px",
880
+ padding: "16px",
881
+ flexGrow: dashboard ? undefined : 1,
882
+ display: "flex",
883
+ flexDirection: "column",
884
+ gap: '16px'
885
+ },
886
+ elevation: 0
887
+ }, /*#__PURE__*/React__default.createElement(LinearIndeterminate, {
888
+ style: {
889
+ visibility: loading ? 'visible' : 'hidden'
890
+ }
891
+ }), /*#__PURE__*/React__default.createElement("div", {
892
+ style: {
893
+ display: "flex"
894
+ }
895
+ }, /*#__PURE__*/React__default.createElement("div", null, chart && /*#__PURE__*/React__default.createElement("div", {
896
+ style: {
897
+ display: "flex",
898
+ alignItems: "center",
899
+ lineHeight: 0
900
+ }
901
+ }, /*#__PURE__*/React__default.createElement(Typography, {
902
+ noWrap: true,
903
+ variant: "h6"
904
+ }, props?.title || chart.doc.name, " "))), /*#__PURE__*/React__default.createElement("div", {
905
+ style: {
906
+ flexGrow: 1
907
+ }
908
+ }), /*#__PURE__*/React__default.createElement("div", null, chart?.doc?.view?.length > 1 && /*#__PURE__*/React__default.createElement(FormControl$2, null, /*#__PURE__*/React__default.createElement(Select$1, {
909
+ labelId: "date-range-select-label",
910
+ id: "date-range-select",
911
+ value: activeViewIndex,
912
+ onChange: onViewChanged,
913
+ style: {
914
+ fontSize: "12px"
915
+ },
916
+ disableUnderline: true
917
+ }, chart.doc.view.map((item, index) => /*#__PURE__*/React__default.createElement(MenuItem$1, {
918
+ key: index,
919
+ value: index
920
+ }, item.title)))))), /*#__PURE__*/React__default.createElement("div", {
921
+ style: {
922
+ minHeight: chart?.doc?.size?.height || (dashboard ? "300px" : "0"),
923
+ position: "relative",
924
+ paddingTop: chart?.doc?.size?.vpad,
925
+ paddingBottom: chart?.doc?.size?.vpad,
926
+ flexGrow: dashboard ? undefined : 1
927
+ }
928
+ }, /*#__PURE__*/React__default.createElement(ChartFilters, {
929
+ filters: chart?.doc?.filters,
930
+ channel: channelUsed,
931
+ query: currentQuery
932
+ }), chartType));
933
+ };
934
+
935
+ function Dashboard({
936
+ id,
937
+ auth,
938
+ params
939
+ }) {
940
+ const [dashboard, setDashboard] = React__default.useState();
941
+ //const [schema, setSchema] = React.useState();
942
+ const [schema] = React__default.useState();
943
+ const [rows, setRows] = React__default.useState([]);
944
+ const cache = React__default.useRef({});
945
+ const channel = useChannel();
946
+ useFilterManager(channel);
947
+ const init = async () => {
948
+ // get dashboard entity
949
+ const dashboardTemp = await Api.getDashboard({
950
+ id: id
951
+ });
952
+ cache.current[dashboardTemp?.id] = dashboardTemp;
953
+ setDashboard(dashboardTemp);
954
+
955
+ // create rows and columns
956
+ await create_rows_and_columns(dashboardTemp);
957
+ };
958
+ const create_rows_and_columns = async entity => {
959
+ const srcRows = entity?.doc?.rows || [];
960
+ const targetRows = [];
961
+ for (let i = 0; i < srcRows.length; i++) {
962
+ const row = srcRows[i];
963
+ row.columns?.forEach(column => {
964
+ // filter
965
+ if (column.override?.filter?.length) {
966
+ const filter = {};
967
+ column.override.filter.forEach(item => {
968
+ if (item.hasOwnProperty('value')) filter[item.field] = [item.value];
969
+ // else filter[item.field] = [sourceDataRow[item.field]];
970
+ });
971
+ column.filter = filter;
972
+ }
973
+ });
974
+ targetRows.push(row);
975
+ }
976
+ setRows(targetRows);
977
+ };
978
+ React__default.useEffect(() => {
979
+ if (!id) return;
980
+ init().catch(error => console.error(error.message));
981
+ }, [id]);
982
+ return /*#__PURE__*/React__default.createElement(Container, null, /*#__PURE__*/React__default.createElement(Grid, {
983
+ container: true
984
+ }, rows?.map((row, index) =>
985
+ /*#__PURE__*/
986
+ // hasResourceRole(row) &&
987
+ React__default.createElement(Grid, {
988
+ key: index,
989
+ container: true,
990
+ item: true
991
+ }, row?.columns?.map((column, index) => /*#__PURE__*/React__default.createElement(Grid, _extends({
992
+ key: index,
993
+ item: true
994
+ }, column.layout), /*#__PURE__*/React__default.createElement(Chart, {
995
+ auth: auth,
996
+ cache: cache.current,
997
+ id: column.id,
998
+ dashboard: dashboard,
999
+ schema: schema,
1000
+ channel: null,
1001
+ title: column.title,
1002
+ filter: column.filter,
1003
+ params: params
1004
+ })))))));
1005
+ }
1006
+
1007
+ var index = {
1008
+ Chart,
1009
+ Dashboard
1010
+ };
1011
+
1012
+ export { index as default };