Skip to content

Commit e7f863c

Browse files
committed
Merged in develop-trigyn-usage-reports (pull request #7102)
R2-3247 Develop trigyn usage reports
2 parents f7433ea + 68ae4e6 commit e7f863c

39 files changed

+698
-22
lines changed

app/controllers/api/v2/concerns/export.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ module Api::V2::Concerns::Export
88

99
def export
1010
authorize_export!
11-
1211
# The '::' is necessary so Export model does not conflict with current concern
1312
@export = ::Export.new(
1413
exporter:, record_type:, module_id:,
1514
file_name: export_params[:file_name], visible: visible_param,
16-
managed_report: @managed_report, opts: export_params
15+
managed_report: report, hostname: request.hostname, opts: export_params
1716
)
1817
@export.run
1918
status = @export.status == ::Export::SUCCESS ? 200 : 422
@@ -28,6 +27,10 @@ def module_id
2827
export_params[:module_id]
2928
end
3029

30+
def report
31+
nil
32+
end
33+
3134
def visible_param
3235
return nil if export_params[:visible].nil?
3336

app/controllers/api/v2/managed_reports_controller.rb

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def model_class
3232
ManagedReport
3333
end
3434

35+
def report
36+
@managed_report
37+
end
38+
3539
def export_params
3640
{ file_name: params[:file_name], locale: params[:locale], subreport_id: params[:subreport] }
3741
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
4+
5+
# API endpoint for generating usage reports
6+
class Api::V2::UsageReportsController < ApplicationApiController
7+
include Api::V2::Concerns::Export
8+
9+
def show
10+
authorize! :show, UsageReport
11+
@usage_report = report
12+
end
13+
14+
protected
15+
16+
def usage_report_params
17+
return @usage_report_params if @usage_report_params.present?
18+
19+
@usage_report_params = params.permit(:from, :to, :file_name, :export_type)
20+
@usage_report_params = DestringifyService.destringify(@usage_report_params)
21+
end
22+
23+
def exporter
24+
Exporters::UsageReportExporter
25+
end
26+
27+
def report
28+
@report ||= UsageReport.new(usage_report_params.slice(:from, :to)).tap(&:build)
29+
end
30+
31+
def module_id
32+
nil
33+
end
34+
35+
def record_type
36+
UsageReport
37+
end
38+
39+
def model_class
40+
UsageReport
41+
end
42+
end

app/javascript/components/action-dialog/component.jsx

-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ function ActionDialog({
137137
<Dialog
138138
open={open}
139139
onClose={onCloseDialog}
140-
fullWidth
141140
maxWidth={maxSize || "sm"}
142141
aria-labelledby="action-dialog-title"
143142
aria-describedby="action-dialog-description"

app/javascript/components/form/components/form-section-field.jsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2-
32
import PropTypes from "prop-types";
43
import { cx } from "@emotion/css";
54

@@ -10,7 +9,7 @@ import formComponent from "../utils/form-component";
109

1110
import css from "./styles.css";
1211

13-
function FormSectionField({ checkErrors, field, formMethods, formMode, disableUnderline = false }) {
12+
function FormSectionField({ checkErrors, field, formMethods, formMode, disableUnderline }) {
1413
const { errors } = formMethods;
1514
const {
1615
Field,

app/javascript/components/form/use-form-field.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2-
32
import get from "lodash/get";
43
import { useMemo } from "react";
54

app/javascript/components/pages/admin/admin-nav-item.jsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ function AdminNavItem({ item, isParent = false, open, handleClick, nestedClass,
2626
);
2727

2828
const listItemProps = {
29-
key: item.to,
3029
button: true,
3130
disabled: item.disabled || disabledApplication,
3231
...(isParent ? { onClick: handleClick } : { component: Link }),
@@ -39,7 +38,7 @@ function AdminNavItem({ item, isParent = false, open, handleClick, nestedClass,
3938
const jewel = renderJewel ? <Jewel value={renderJewel} isForm /> : null;
4039

4140
return (
42-
<ListItem {...listItemProps}>
41+
<ListItem {...listItemProps} key={item.to}>
4342
<ListItemText className={nestedClass || null}>{i18n.t(item.label)}</ListItemText>
4443
{isParent ? handleOpen : null}
4544
{jewel}

app/javascript/components/pages/admin/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { default as ConfigurationsList } from "./configurations-list";
1919
export { default as ConfigurationsForm } from "./configurations-form";
2020
export { default as LocationsList } from "./locations-list";
2121
export { default as CodeOfConduct } from "./code-of-conduct";
22+
export { default as UsageReports } from "./usage-reports";

app/javascript/components/pages/admin/index.unit.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ describe("pages/admin - index", () => {
2525
"UserGroupsForm",
2626
"UserGroupsList",
2727
"UsersForm",
28-
"UsersList"
28+
"UsersList",
29+
"UsageReports"
2930
].forEach(property => {
3031
expect(indexValues).to.have.property(property);
3132
delete indexValues[property];

app/javascript/components/pages/admin/roles-form/constants.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export const RESOURCES = [
3838
"activity_log",
3939
"matching_configuration",
4040
"duplicate",
41-
"kpi"
41+
"kpi",
42+
"usage_report"
4243
];
4344

4445
export const ROLES_PERMISSIONS = Object.freeze({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { METHODS } from "../../../../config";
2+
import { ENQUEUE_SNACKBAR, generate } from "../../../notifier";
3+
import { CLEAR_DIALOG } from "../../../action-dialog";
4+
5+
import actions from "./actions";
6+
import { USAGE_REPORTS_EXPORT_PATH } from "./constants";
7+
8+
export const fetchUsageExport = (params, message) => ({
9+
type: actions.FETCH_USAGE_REPORTS,
10+
api: {
11+
path: USAGE_REPORTS_EXPORT_PATH,
12+
method: METHODS.GET,
13+
params,
14+
successCallback: [
15+
{
16+
action: ENQUEUE_SNACKBAR,
17+
payload: {
18+
message,
19+
options: {
20+
variant: "success",
21+
key: generate.messageKey(message)
22+
}
23+
},
24+
redirectWithIdFromResponse: false,
25+
redirect: false
26+
},
27+
{
28+
action: CLEAR_DIALOG
29+
}
30+
]
31+
}
32+
});
33+
34+
export const clearExportedUsageReport = () => ({
35+
type: actions.CLEAR_EXPORTED_USAGE_REPORT
36+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import { namespaceActions } from "../../../../libs";
4+
5+
import { NAMESPACE } from "./constants";
6+
7+
export default namespaceActions(NAMESPACE, [
8+
"CLEAR_EXPORTED_USAGE_REPORT",
9+
"FETCH_USAGE_REPORTS",
10+
"FETCH_USAGE_REPORTS_FAILURE",
11+
"FETCH_USAGE_REPORTS_FINISHED",
12+
"FETCH_USAGE_REPORTS_STARTED",
13+
"FETCH_USAGE_REPORTS_SUCCESS"
14+
]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const NAME = "UsageReports";
2+
export const NAMESPACE = "usage_reports";
3+
export const USAGE_REPORTS_EXPORT_PATH = "usage_reports/current/export";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import { SwapVert } from "@mui/icons-material";
4+
5+
import { useI18n } from "../../../i18n";
6+
import { PageHeading, PageContent } from "../../../page";
7+
import { useDialog } from "../../../action-dialog";
8+
import { FormAction } from "../../../form";
9+
10+
import UsageReport from "./export/component";
11+
import { USAGE_REPORT_DIALOG } from "./export/constants";
12+
13+
function Container() {
14+
const i18n = useI18n();
15+
const { setDialog, pending, dialogOpen, setDialogPending, dialogClose } = useDialog(USAGE_REPORT_DIALOG);
16+
const handleExport = dialog => setDialog({ dialog, open: true });
17+
const handleClickExport = () => handleExport(USAGE_REPORT_DIALOG);
18+
19+
return (
20+
<>
21+
<PageHeading title={i18n.t("settings.navigation.usage_reports")}>
22+
<FormAction actionHandler={handleClickExport} text={i18n.t("buttons.export")} startIcon={<SwapVert />} />
23+
</PageHeading>
24+
<PageContent>
25+
<h3>{i18n.t("usage_report.instructions")}</h3>
26+
<UsageReport open={dialogOpen} pending={pending} close={dialogClose} setPending={setDialogPending} />
27+
</PageContent>
28+
</>
29+
);
30+
}
31+
32+
Container.displayName = "UsageReports";
33+
34+
Container.propTypes = {};
35+
36+
export default Container;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import PropTypes from "prop-types";
4+
import { useDispatch } from "react-redux";
5+
import { object, string, date } from "yup";
6+
import { useEffect } from "react";
7+
8+
import { useI18n } from "../../../../i18n";
9+
import ActionDialog from "../../../../action-dialog";
10+
import Form from "../../../../form";
11+
import { formatFileName } from "../../../../record-actions/exports/utils";
12+
import { toServerDateFormat, useMemoizedSelector } from "../../../../../libs";
13+
import { clearExportedUsageReport, fetchUsageExport } from "../action-creators";
14+
import { getUsageReportExport } from "../selectors";
15+
16+
import { NAME, FORM_ID, EXPORTED_URL } from "./constants";
17+
import { form } from "./form";
18+
import css from "./style.css";
19+
20+
function Component({ close, open, pending, setPending }) {
21+
const i18n = useI18n();
22+
const exportedUsageReport = useMemoizedSelector(state => getUsageReportExport(state));
23+
const dispatch = useDispatch();
24+
const dialogPending = typeof pending === "object" ? pending.get("pending") : pending;
25+
26+
const fieldDisplayName = {
27+
fromDate: i18n.t("key_performance_indicators.date_range_dialog.from"),
28+
toDate: i18n.t("key_performance_indicators.date_range_dialog.to"),
29+
file_name: i18n.t("usage_report.file_name")
30+
};
31+
32+
const validationSchema = object({
33+
fromDate: date()
34+
.nullable()
35+
.required(i18n.t("fields.required_field", { field: fieldDisplayName.fromDate }))
36+
.typeError(i18n.t("usage_report.invalid_date_format", { field: fieldDisplayName.fromDate })),
37+
toDate: date()
38+
.nullable()
39+
.required(i18n.t("fields.required_field", { field: fieldDisplayName.toDate }))
40+
.typeError(i18n.t("usage_report.invalid_date_format", { field: fieldDisplayName.toDate }))
41+
.test({
42+
name: "is-after",
43+
message: i18n.t("usage_report.invalid_date_range", {
44+
from: fieldDisplayName.fromDate,
45+
to: fieldDisplayName.toDate
46+
}),
47+
test: (value, context) => {
48+
const { fromDate } = context.parent;
49+
50+
return !fromDate || !value || value > fromDate;
51+
}
52+
}),
53+
file_name: string().required(i18n.t("fields.required_field", { field: fieldDisplayName.file_name }))
54+
});
55+
56+
// Form submission
57+
const onSubmit = getData => {
58+
const fileName = formatFileName(getData.file_name, "xlsx");
59+
const defaultBody = {
60+
export_format: "xlsx",
61+
record_type: "usage_report",
62+
file_name: fileName,
63+
selected_from_date: toServerDateFormat(getData.fromDate),
64+
selected_to_date: toServerDateFormat(getData.toDate)
65+
};
66+
const data = { ...defaultBody };
67+
68+
setPending(true);
69+
dispatch(fetchUsageExport(data, i18n.t("exports.exported")));
70+
};
71+
72+
const handleCustomClose = () => close();
73+
74+
useEffect(() => {
75+
if (!exportedUsageReport.isEmpty() && exportedUsageReport.get(EXPORTED_URL)) {
76+
window.open(exportedUsageReport.get(EXPORTED_URL));
77+
dispatch(clearExportedUsageReport());
78+
}
79+
}, [exportedUsageReport.get(EXPORTED_URL, "")]);
80+
81+
return (
82+
<ActionDialog
83+
open={open}
84+
confirmButtonProps={{
85+
form: FORM_ID,
86+
type: "submit"
87+
}}
88+
cancelHandler={handleCustomClose}
89+
dialogTitle={i18n.t("usage_report.label")}
90+
confirmButtonLabel={i18n.t("buttons.export")}
91+
pending={dialogPending}
92+
omitCloseAfterSuccess
93+
>
94+
<Form
95+
useCancelPrompt
96+
formID={FORM_ID}
97+
mode="new"
98+
formSections={form(fieldDisplayName)}
99+
onSubmit={onSubmit}
100+
formClassName={`${css["usage-reports-form"]}`}
101+
validations={validationSchema}
102+
/>
103+
</ActionDialog>
104+
);
105+
}
106+
107+
Component.displayName = NAME;
108+
109+
Component.propTypes = {
110+
close: PropTypes.func,
111+
open: PropTypes.bool,
112+
pending: PropTypes.bool,
113+
setPending: PropTypes.func
114+
};
115+
116+
export default Component;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const NAME = "UsageReport";
2+
export const USAGE_REPORT_DIALOG = "UsageReportDialog";
3+
export const EXPORT_TYPES = Object.freeze({ EXCEL: "xlsx" });
4+
export const FORM_ID = "export-users-form";
5+
export const EXPORTED_URL = "export_file_url";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-disable import/prefer-default-export */
2+
3+
import { fromJS } from "immutable";
4+
5+
import { FieldRecord, FormSectionRecord, TEXT_FIELD, DATE_FIELD } from "../../../../form";
6+
7+
export const form = fieldDisplayName => {
8+
return fromJS([
9+
FormSectionRecord({
10+
unique_id: "import_locations",
11+
fields: [
12+
FieldRecord({
13+
display_name: fieldDisplayName.fromDate,
14+
required: true,
15+
name: "fromDate",
16+
type: DATE_FIELD
17+
}),
18+
FieldRecord({
19+
display_name: fieldDisplayName.toDate,
20+
required: true,
21+
name: "toDate",
22+
type: DATE_FIELD
23+
}),
24+
FieldRecord({
25+
name: "file_name",
26+
display_name: fieldDisplayName.file_name,
27+
required: true,
28+
type: TEXT_FIELD
29+
})
30+
]
31+
})
32+
]);
33+
};

0 commit comments

Comments
 (0)