From 11f3ef37d7e2af506312dbe05f7a49d32e2b1a6a Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Tue, 23 Aug 2022 23:12:58 +0700 Subject: [PATCH] [#5080] Added active state to fix indicator approval workflow Active status helps MnE managers to identify updates of indicators that have an unlocked period still in progress. the following what has to be done in order to finalize this feature : 1. Update status description. 2. Add new icon history. 3. Create audit trail modal 4. Call /indicator_period_data_framework/ by period Pulling data audit trails from endpoint /indicator_period_data_framework/ by period ID of selected update. --- akvo/rsr/spa/app/components/Icon.jsx | 15 ++ .../spa/app/components/StatusIndicator.jsx | 21 +- akvo/rsr/spa/app/components/StatusPeriod.jsx | 5 +- akvo/rsr/spa/app/components/index.js | 1 + akvo/rsr/spa/app/images/clock-history.svg | 3 + akvo/rsr/spa/app/images/edit-pencil.svg | 3 + .../modules/results-admin/TobeReported.jsx | 255 +++++++++++------- .../modules/results-admin/TobeReported.scss | 89 ++++-- .../components/AuditTrailModal.jsx | 100 +++++++ .../components/UpdateItems.jsx | 2 +- .../app/modules/results/EnumeratorPage.jsx | 4 +- .../spa/app/modules/results/enumerator.jsx | 4 +- akvo/rsr/spa/app/modules/results/period.jsx | 2 +- akvo/rsr/spa/app/utils/constants.js | 11 + akvo/rsr/spa/app/utils/images.js | 13 + 15 files changed, 387 insertions(+), 141 deletions(-) create mode 100644 akvo/rsr/spa/app/components/Icon.jsx create mode 100644 akvo/rsr/spa/app/images/clock-history.svg create mode 100644 akvo/rsr/spa/app/images/edit-pencil.svg create mode 100644 akvo/rsr/spa/app/modules/results-admin/components/AuditTrailModal.jsx create mode 100644 akvo/rsr/spa/app/utils/images.js diff --git a/akvo/rsr/spa/app/components/Icon.jsx b/akvo/rsr/spa/app/components/Icon.jsx new file mode 100644 index 0000000000..e5cc2073b8 --- /dev/null +++ b/akvo/rsr/spa/app/components/Icon.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Icon as AntIcon } from 'antd' +import SVGInline from 'react-svg-inline' +import get from 'lodash/get' +import { icons } from '../utils/images' + + +const Icon = ({ type, ...props }) => { + const customIcon = get(icons, type) + return customIcon + ? + : +} + +export default Icon diff --git a/akvo/rsr/spa/app/components/StatusIndicator.jsx b/akvo/rsr/spa/app/components/StatusIndicator.jsx index c4084f95fc..f5f104cb31 100644 --- a/akvo/rsr/spa/app/components/StatusIndicator.jsx +++ b/akvo/rsr/spa/app/components/StatusIndicator.jsx @@ -1,25 +1,14 @@ import React from 'react' import { Col, Row, Typography } from 'antd' +import { statusDescription } from '../utils/constants' const { Text } = Typography -const StatusIndicator = ({ status }) => { - let description = 'No status yet' - if (status === 'D') { - description = 'Draft update created' - } - if (status === 'P') { - description = 'Update submitted' - } - if (status === 'R') { - description = 'Update declined' - } - if (status === 'A') { - description = 'Approved update reported' - } +const StatusIndicator = ({ status, updateClass }) => { + const description = statusDescription[status] || statusDescription[updateClass] || statusDescription.NO_STATUS return ( - - + + Status : {description} diff --git a/akvo/rsr/spa/app/components/StatusPeriod.jsx b/akvo/rsr/spa/app/components/StatusPeriod.jsx index 4cfa8294d5..986ecb5732 100644 --- a/akvo/rsr/spa/app/components/StatusPeriod.jsx +++ b/akvo/rsr/spa/app/components/StatusPeriod.jsx @@ -1,6 +1,8 @@ import React from 'react' import SVGInline from 'react-svg-inline' import { Button } from 'antd' +import { useTranslation } from 'react-i18next' + import approvedSvg from '../images/status-approved.svg' import pendingSvg from '../images/status-pending.svg' import revisionSvg from '../images/status-revision.svg' @@ -8,7 +10,8 @@ import { DeclinePopup } from './DeclinePopup' const Aux = node => node.children -export const StatusPeriod = ({ update, pinned, index, handleUpdateStatus, t }) => { +export const StatusPeriod = ({ update, pinned, index, handleUpdateStatus }) => { + const { t } = useTranslation() if (update.status === 'A') { return (
diff --git a/akvo/rsr/spa/app/components/index.js b/akvo/rsr/spa/app/components/index.js index 41b9e4c4a2..8e4933c014 100644 --- a/akvo/rsr/spa/app/components/index.js +++ b/akvo/rsr/spa/app/components/index.js @@ -5,3 +5,4 @@ export { MobileSlider } from './MobileSlider' export { PrevUpdate } from './PrevUpdate' export { DeclinePopup } from './DeclinePopup' export { IndicatorItem } from './IndicatorItem' +export { default as Icon } from './Icon' diff --git a/akvo/rsr/spa/app/images/clock-history.svg b/akvo/rsr/spa/app/images/clock-history.svg new file mode 100644 index 0000000000..b51a38aae3 --- /dev/null +++ b/akvo/rsr/spa/app/images/clock-history.svg @@ -0,0 +1,3 @@ + + + diff --git a/akvo/rsr/spa/app/images/edit-pencil.svg b/akvo/rsr/spa/app/images/edit-pencil.svg new file mode 100644 index 0000000000..ef7cd4b6a7 --- /dev/null +++ b/akvo/rsr/spa/app/images/edit-pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/akvo/rsr/spa/app/modules/results-admin/TobeReported.jsx b/akvo/rsr/spa/app/modules/results-admin/TobeReported.jsx index 8bcf3ecc45..778298a644 100644 --- a/akvo/rsr/spa/app/modules/results-admin/TobeReported.jsx +++ b/akvo/rsr/spa/app/modules/results-admin/TobeReported.jsx @@ -8,19 +8,17 @@ import { Row, Col, Modal, - Icon, - message + message, + Tooltip } from 'antd' import { useTranslation } from 'react-i18next' import SimpleMarkdown from 'simple-markdown' -import SVGInline from 'react-svg-inline' import classNames from 'classnames' import moment from 'moment' -import { isEmpty } from 'lodash' +import { isEmpty, kebabCase } from 'lodash' import { connect } from 'react-redux' import './TobeReported.scss' -import editButton from '../../images/edit-button.svg' import api from '../../utils/api' import ReportedEdit from './components/ReportedEdit' import { isPeriodNeedsReportingForAdmin } from '../results/filters' @@ -28,10 +26,14 @@ import Highlighted from '../../components/Highlighted' import StatusIndicator from '../../components/StatusIndicator' import ResultType from '../../components/ResultType' import * as actions from '../results/actions' +import { ACTIVE_PERIOD } from '../../utils/constants' +import { Icon } from '../../components' +import AuditTrailModal from './components/AuditTrailModal' -const { Text } = Typography +const { Text, Title } = Typography const TobeReported = ({ + resultRdr, keyword, results, updates, @@ -51,6 +53,12 @@ const TobeReported = ({ const [activeKey, setActiveKey] = useState(null) const [deletion, setDeletion] = useState([]) const [errors, setErrors] = useState([]) + const [openHistory, setOpenHistory] = useState({ + scores: [], + results: [], + item: null, + visible: false + }) const formRef = useRef() const mdParse = SimpleMarkdown.defaultBlockParse @@ -147,96 +155,155 @@ const TobeReported = ({ formRef.current.form.setConfig('keepDirtyOnReinitialize', true) } + const handleOnShowHistory = item => { + setOpenHistory({ + ...openHistory, + item: { + ...item, + fetched: false + }, + visible: !openHistory.visible + }) + api + .get(`/indicator_period_data_framework/?period=${item?.period?.id}&format=json`) + .then(({ data }) => { + const { results: dataResults } = data + setOpenHistory({ + scores: item?.indicator?.scores, + results: dataResults, + item: { + ...item, + fetched: true + }, + visible: !openHistory.visible + }) + }) + .catch(() => message.error('Something went wrong')) + } + + const handleOnCloseModal = () => { + setOpenHistory({ + scores: [], + results: [], + item: null, + visible: false + }) + } + return ( - { - const iKey = item?.id || `${item?.indicator?.id}0${ix}` - const updateClass = item?.statusDisplay?.toLowerCase()?.replace(/\s+/g, '-') - return ( - - - - - {isEmpty(period) && ( -
- {moment(item?.period?.periodStart, 'DD/MM/YYYY').format('DD MMM YYYY')} - {moment(item?.period?.periodEnd, 'DD/MM/YYYY').format('DD MMM YYYY')} -
- )} - - -
- Title : - -
- {((!isEmpty(item?.indicator?.description.trim())) && item?.indicator?.description?.trim().length > 5) && ( -
- {t('Description')} -

{mdOutput(mdParse(item?.indicator?.description))}

-
- )} - - - { - (activeKey === iKey) - ? ( -
- -
- ) - : ( - - ) - } - -
-
- {(editing && activeKey) && ( - - - - - - )} -
- ) - }} - /> +
+ )} + {(activeKey !== iKey) && ( + <> + {(allSubmissions.length > 0) && ( + + + + )} + + + + + )} + +
+ + { + (editing && activeKey) && ( + + + + + + ) + } + + ) + }} + /> + ) } diff --git a/akvo/rsr/spa/app/modules/results-admin/TobeReported.scss b/akvo/rsr/spa/app/modules/results-admin/TobeReported.scss index d0b2c55698..9c036660df 100644 --- a/akvo/rsr/spa/app/modules/results-admin/TobeReported.scss +++ b/akvo/rsr/spa/app/modules/results-admin/TobeReported.scss @@ -1,3 +1,11 @@ +$draft-color: #ccb30a; +$draft-bg: #D68D19; +$no-status-color: #a7a5a5; +$revision-color: #f5222d; +$active-color: #1890ff; +$approved-color: #52c41a; +$gray-500: #667085; + .tobe-reported { .ant-collapse-borderless { background-color: #fff; @@ -22,38 +30,36 @@ } .ant-card { &.draft { - border-left: 3px solid #ccb30a; + border-left: 3px solid $draft-color; } &.no-update-status { - border-left: 3px solid #a7a5a5; + border-left: 3px solid $no-status-color; } &.return-for-revision { - border-left: 3px solid #f5222d; + border-left: 3px solid $revision-color; } - &.pending-approval { - border-left: 3px solid #1890ff; + &.active-period { + border-left: 3px solid $active-color; } &.approved { - border-left: 3px solid #52c41a; + border-left: 3px solid $approved-color; } } - } - .ant-card { - &.active { - &.draft { - background-color: #ffebc5; + .header-status { + .draft .ant-typography { + color: $draft-color !important; } - &.no-update-status { - background-color: #efefef; + .no-update-status .ant-typography { + color: $no-status-color !important; } - &.return-for-revision { - background-color: #f7eded; + .return-for-revision .ant-typography { + color: $revision-color !important; } - &.pending-approval { - background-color: #edf5fb; + .active-period .ant-typography { + color: $active-color !important; } - &.approved { - background-color: #e5fadb; + .approved .ant-typography { + color: $approved-color !important; } } } @@ -82,18 +88,53 @@ display: none; } .mne-view .action { - text-align: center; + display: flex; + flex-direction: row; + justify-content: end; + .ant-btn.ant-btn-link { + color: $gray-500; + } } .tobe-reported .ant-collapse-borderless .ant-collapse-item .ant-collapse-content .ant-collapse-content-box .action-bar-xs { display: none; } +.ant-col.label { + .status { + display: flex; + gap: 8px; + align-items: end; + svg { + width: 24px; + transform: translateY(7px); + } + &.approved { + color: #009b8f; + } + &.pending { + color: #e36f3c; + } + &.returned { + color: #961417; + } + &.draft { + color: $draft-color; + } + } +} @media (min-width: 320px) and (max-width: 576px) { .mne-view .ant-page-header .ant-page-header-content .ant-row .filter-period { padding-top: 16px; } .mne-view .action-text { + margin-left: 8px; display: block; } + .mne-view .action { + flex-direction: column; + .ant-btn.ant-btn.ant-btn-link { + width: 100%; + } + } .tobe-reported .tobe-reported-item .action .ant-btn { display: flex; justify-content: center; @@ -106,16 +147,16 @@ margin-top: 8px; &:hover { background-color: #f4f4f4; - &.pending-approval { + &.active-period { color: #fff; - background-color: rgb(24, 144, 255); + background-color: $active-color; } &.draft { - background-color: #D68D19; + background-color: $draft-bg; } &.approved { color: #fff; - background-color: #52c41a; + background-color: $approved-color; } } } diff --git a/akvo/rsr/spa/app/modules/results-admin/components/AuditTrailModal.jsx b/akvo/rsr/spa/app/modules/results-admin/components/AuditTrailModal.jsx new file mode 100644 index 0000000000..37e8e3b583 --- /dev/null +++ b/akvo/rsr/spa/app/modules/results-admin/components/AuditTrailModal.jsx @@ -0,0 +1,100 @@ +import React from 'react' +import { + Row, + Col, + Collapse, + Modal, + Skeleton, + Typography, + Descriptions, +} from 'antd' +import moment from 'moment' +import { Icon } from '../../../components' +import { AuditTrail } from '../../../components/AuditTrail' +import { setNumberFormat } from '../../../utils/misc' +import { StatusPeriod } from '../../../components/StatusPeriod' + +const { Title, Text } = Typography +const { Panel } = Collapse + +const AuditTrailModal = ({ + scores = [], + results = [], + item, + visible, + onClose, +}) => { + const { fetched, period, indicator } = item || {} + const defaultActiveKey = results.map((r) => r.id).slice(0, 1) + return ( + + + Audit trail + + )} + > +
+ {`${moment(period?.periodStart, 'DD/MM/YYYY').format('DD MMM YYYY')} - ${moment(period?.periodEnd, 'DD/MM/YYYY').format('DD MMM YYYY')}`} +
+ + Title}> +

{indicator?.title}

+
+
+ + + {results.map((update, index) => { + const { auditTrail: audits, narrative: textReport, ...props } = update + const author = (update?.userDetails?.firstName?.length || update?.userDetails?.lastName?.length) + ? `${update?.userDetails?.firstName} ${update?.userDetails?.lastName}` + : update?.userDetails?.email + return ( + + +

+ created at
+ {moment(update.createdAt).format('DD MMM YYYY')} +

+ + + {author} + + +
+ {update.value && setNumberFormat(update.value)} +
+ + + + +
+ )} + key={update.id} + > + + + ) + })} + + + + ) +} + +export default AuditTrailModal diff --git a/akvo/rsr/spa/app/modules/results-overview/components/UpdateItems.jsx b/akvo/rsr/spa/app/modules/results-overview/components/UpdateItems.jsx index fc6e084d67..a92c053a77 100644 --- a/akvo/rsr/spa/app/modules/results-overview/components/UpdateItems.jsx +++ b/akvo/rsr/spa/app/modules/results-overview/components/UpdateItems.jsx @@ -137,7 +137,7 @@ const UpdateItems = ({ } - + {['m&e', 'admin'].includes(role) && ( , - + ]} {(update.isNew && editing === index) && (
e.stopPropagation()}> diff --git a/akvo/rsr/spa/app/utils/constants.js b/akvo/rsr/spa/app/utils/constants.js index f5787bd248..f6ceea9929 100644 --- a/akvo/rsr/spa/app/utils/constants.js +++ b/akvo/rsr/spa/app/utils/constants.js @@ -32,3 +32,14 @@ export const indicatorTypes = [ { label: 'quantitative', value: 1}, { label: 'qualitative', value: 2} ] + +export const ACTIVE_PERIOD = 'active-period' + +export const statusDescription = { + [ACTIVE_PERIOD]: 'Active', + D: 'Draft update created', + P: 'Update submitted', + R: 'Update declined', + A: 'Approved update reported', + NO_STATUS: 'No status yet' +} diff --git a/akvo/rsr/spa/app/utils/images.js b/akvo/rsr/spa/app/utils/images.js new file mode 100644 index 0000000000..d9af96445d --- /dev/null +++ b/akvo/rsr/spa/app/utils/images.js @@ -0,0 +1,13 @@ +import clockHistory from '../images/clock-history.svg' +import editPencil from '../images/edit-pencil.svg' +import editButton from '../images/edit-button.svg' + +export const icons = { + clock: { + history: clockHistory + }, + edit: { + pencil: editPencil, + button: editButton + } +}