Skip to content

Commit

Permalink
Merge pull request #11 from openimis/reports
Browse files Browse the repository at this point in the history
Reports
  • Loading branch information
edarchis authored Mar 13, 2022
2 parents 27b9691 + 5b79c1d commit e80b4de
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 38 deletions.
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"start": "rollup -c -w"
},
"peerDependency": {
"react-intl": "^5.8.1"
"react-intl": "^5.8.1",
"react": "^17.0.2",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
Expand All @@ -30,13 +32,15 @@
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-url": "^5.0.0",
"moment": "^2.25.3",
"prop-types": "^15.7.2",
"clsx": "^1.1.1",
"rollup": "^2.10.0",
"@material-ui/core": "^4.9.14",
"@material-ui/icons": "^4.9.1",
"@openimis/fe-core": "^1.4.0-rc3",
"moment": "^2.25.3",
"react-autosuggest": "^10.0.2",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5",
"redux-api-middleware": "^3.2.1",
"rollup": "^2.10.0"
"react-router-dom": "^5.2.0"
},
"files": [
"dist"
Expand Down
81 changes: 81 additions & 0 deletions src/components/GenerateReportPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@material-ui/core";
import { ErrorBoundary, SelectInput, useBoolean, useModulesManager, useTranslations } from "@openimis/fe-core";
import React, { useEffect, useState } from "react";
import { useReportQuery } from "../hooks";
import ReportPicker from "./ReportPicker";

const GenerateReportPicker = (props) => {
const { name, outputFormat = "pdf", children } = props;
const modulesManager = useModulesManager();
const { formatMessage } = useTranslations("tools.GenerateReportPicker", modulesManager);
const [isOpen, { toggle }] = useBoolean(false);
const [values, setValues] = useState({ outputFormat });
const reportQuery = useReportQuery({ name }, { skip: !name });
const [report, setReport] = useState();
const moduleReport = modulesManager.getReport(report?.name);

useEffect(() => {
if (reportQuery?.report) {
setReport(reportQuery?.report);
}
}, [reportQuery]);

const onSubmit = () => {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(moduleReport.getParams(values))) {
query.set(key, value);
}
window.open(`/api/report/${report.name}/${values.outputFormat}/?${query.toString()}`, "_blank", "download");
toggle();
};
const isValid = Boolean(report && values.outputFormat && moduleReport?.isValid && moduleReport.isValid(values));
return (
<>
{children ? (
children({ toggle })
) : (
<Button onClick={toggle} variant="contained" size="small">
{formatMessage("triggerBtn")}
</Button>
)}
<Dialog open={isOpen} onClose={toggle}>
<DialogTitle>{formatMessage("title")}</DialogTitle>
<DialogContent>
<ErrorBoundary>
{!name && (
<Box mb={2}>
<ReportPicker value={report} onChange={setReport} />
</Box>
)}
{!props.outputFormat && (
<Box mb={1}>
<SelectInput
module="tools"
required
label="GenerateReportPicker.formatLabel"
options={[
{ value: "pdf", label: "PDF" },
{ value: "xlsx", label: "XLSX" },
]}
value={values.outputFormat}
onChange={(outputFormat) => setValues({ ...values, outputFormat })}
/>
</Box>
)}
{moduleReport?.component && (
<moduleReport.component report={report} values={values} setValues={setValues} />
)}
</ErrorBoundary>
</DialogContent>
<DialogActions>
<Button onClick={toggle}>{formatMessage("cancelBtn")}</Button>
<Button disabled={!isValid} onClick={onSubmit}>
{formatMessage("generateBtn")}
</Button>
</DialogActions>
</Dialog>
</>
);
};

export default GenerateReportPicker;
62 changes: 62 additions & 0 deletions src/components/ReportBro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useState, useCallback, useRef, useEffect } from "react";

const ReportBro = (props) => {
const { definition, onChange, ...delegated } = props;
const [isReady, setReady] = useState(false);
const iframeRef = useRef(null);

useEffect(() => {
const elm = iframeRef.current;
if (!elm) return;

const listener = (event) => {
try {
const message = JSON.parse(event.data);
switch (message.type) {
case "READY":
setReady(true);
break;
case "SAVE":
onChange(JSON.stringify(message.payload));
break;
}
} catch (err) {
console.warn("Message cannot be handled by this component");
}
};
window.addEventListener("message", listener);

return () => window.removeEventListener("message", listener);
}, []);

const postMessage = useCallback(
(message) => {
try {
iframeRef.current.contentWindow.postMessage(JSON.stringify(message), "*");
} catch (err) {
console.error(err);
}
},
[iframeRef]
);

useEffect(() => {
if (isReady && definition) {
postMessage({ type: "LOAD", payload: JSON.parse(definition) });
}
}, [definition, isReady]);

return (
<iframe
ref={iframeRef}
src={`/api/report/reportbro/designer`}
frameBorder="0"
width="100%"
height="100%"
allowFullScreen
{...delegated}
/>
);
};

export default ReportBro;
65 changes: 65 additions & 0 deletions src/components/ReportDefinitionEditorDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useEffect, useState } from "react";
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, CircularProgress } from "@material-ui/core";
import { makeStyles } from "@material-ui/styles";
import { useTranslations, useModulesManager } from "@openimis/fe-core";
import ReportBro from "./ReportBro";
import { useOverrideReportMutation, useReportQuery } from "../hooks";

const useDialogStyles = makeStyles(() => ({
root: {
zIndex: "2001 !important",
},
paper: {
height: "100%",
},
}));

const useDialogContentStyles = makeStyles(() => ({
dialogContent: {
padding: 0,
},
}));

const ReportDefinitionEditorDialog = (props) => {
const { name, onClose } = props;
const modulesManager = useModulesManager();
const { formatMessage } = useTranslations("tools", modulesManager);
const [resetKey, setResetKey] = useState(null);
const dialogClasses = useDialogStyles();
const classes = useDialogContentStyles();
const { report, isLoading } = useReportQuery({ name });
const { mutate } = useOverrideReportMutation();
const [definition, setDefinition] = useState(null);

useEffect(() => {
setDefinition(report?.definition ?? report?.defaultReport);
}, [report]);

const onReset = () => {
setResetKey(Date.now());
setDefinition(report?.defaultReport);
};

const handleChange = async (value) => {
await mutate({ name, definition: value });
onClose();
};

return (
<Dialog classes={dialogClasses} maxWidth="xl" open fullWidth onClose={onClose}>
<DialogTitle>{formatMessage("ReportDefinitionEditor.title")}</DialogTitle>
<DialogContent className={classes.dialogContent}>
{report && <ReportBro key={resetKey} definition={definition} onChange={handleChange} />}
{isLoading && <CircularProgress />}
</DialogContent>
<DialogActions>
{report?.defaultReport && (
<Button onClick={onReset}>{formatMessage("tools.ReportDefinitionEditor.resetToDefault")}</Button>
)}
<Button onClick={onClose}>{formatMessage("tools.ReportDefinitionEditor.cancel")}</Button>
</DialogActions>
</Dialog>
);
};

export default ReportDefinitionEditorDialog;
44 changes: 44 additions & 0 deletions src/components/ReportForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Grid } from "@material-ui/core";
import { makeStyles } from "@material-ui/styles";
import { Form, PublishedComponent } from "@openimis/fe-core";
import React from "react";

const useStyles = makeStyles((theme) => ({
item: theme.paper.item,
}));

const MainPanel = ({ edited, onEditedChanged }) => {
const classes = useStyles();
return (
<Grid container>
<Grid item xs={4} className={classes.item}>
<PublishedComponent
pubRef="tools.ReportDefinitionEditor"
value={edited.definition}
defaultValue={edited.defaultReport}
onChange={(definition) => onEditedChanged({ ...edited, definition })}
/>
</Grid>
</Grid>
);
};

const ReportForm = ({ onBack, onSave, report, onChange }) => {
if (!report) {
return null;
}
return (
<Form
module="tools"
title="ReportForm.title"
titleParams={{ name: report.name }}
HeadPanel={MainPanel}
onEditedChanged={onChange}
edited={report}
edited_id={report.name}
save={onSave}
back={onBack}
/>
);
};
export default ReportForm;
36 changes: 36 additions & 0 deletions src/components/ReportPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useMemo, useState } from "react";
import { Autocomplete, useModulesManager, useTranslations } from "@openimis/fe-core";
import { useReportsQuery } from "../hooks";

const ReportPicker = (props) => {
const { onChange, readOnly = false, required = false, withLabel = true, value, label, placeholder } = props;
const modulesManager = useModulesManager();
const { formatMessage } = useTranslations("tools", modulesManager);
const [searchString, setSearchString] = useState("");
const { isLoading, data } = useReportsQuery();

const options = useMemo(() => {
if (searchString) {
return data?.reports.filter((x) => x.name.toLowerCase().includes(searchString.toLowerCase()));
}
return data?.reports;
}, [searchString, data]);

return (
<Autocomplete
required={required}
placeholder={placeholder}
label={label ?? formatMessage("ReportPicker.label")}
withLabel={withLabel}
readOnly={readOnly}
options={options ?? []}
isLoading={isLoading}
value={value}
getOptionLabel={(o) => o?.description}
onChange={onChange}
onInputChange={setSearchString}
/>
);
};

export default ReportPicker;
51 changes: 51 additions & 0 deletions src/components/ReportSearcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useCallback, useState } from "react";
import { useReportsQuery } from "../hooks";
import { Searcher, useTranslations, useModulesManager } from "@openimis/fe-core";
import GenerateReportPicker from "./GenerateReportPicker";
import { Box, Button } from "@material-ui/core";
import ReportDefinitionEditorDialog from "./ReportDefinitionEditorDialog";

const HEADERS = ["tools.report.description", "tools.report.module", "tools.report.name", ""];

const ReportSearcher = () => {
const modulesManager = useModulesManager();
const { formatMessageWithValues, formatMessage } = useTranslations("tools", modulesManager);
const { data, isLoading, error, refetch } = useReportsQuery();
const [editedReport, setEditedReport] = useState();
const itemFormatters = useCallback(
() => [
(r) => r.description,
(r) => r.module,
(r) => r.name,
(r) => (
<Box display="flex" justifyContent={"flex-end"} gridGap={12}>
<Button onClick={() => setEditedReport(r)} size="small">
{formatMessage("ReportSearcher.editBtn")}
</Button>
<GenerateReportPicker name={r.name} />
</Box>
),
],
[]
);

return (
<>
<Searcher
module="report"
tableTitle={formatMessageWithValues("ReportSearcher.tableTitle", { count: data?.reports?.length })}
items={data?.reports ?? []}
fetchingItems={isLoading}
errorItems={error}
fetch={() => refetch()}
itemsPageInfo={{ totalCount: data?.reports?.length ?? 0 }}
headers={() => HEADERS}
itemFormatters={itemFormatters}
withPagination={false}
/>
{editedReport && <ReportDefinitionEditorDialog name={editedReport.name} onClose={() => setEditedReport(null)} />}
</>
);
};

export default ReportSearcher;
Loading

0 comments on commit e80b4de

Please sign in to comment.