Skip to content

Commit

Permalink
Merge pull request #5285 from akvo/5080-nuffic-indicator-approval-wor…
Browse files Browse the repository at this point in the history
…kflow

[#5080] Implement indicator approval workflow
  • Loading branch information
zuhdil authored Jun 20, 2023
2 parents cdb4279 + 026a018 commit 4da8949
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 139 deletions.
21 changes: 5 additions & 16 deletions akvo/rsr/spa/app/components/StatusIndicator.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Row>
<Col style={{ display: 'flex', gap: 10 }}>
<Row className="header-status">
<Col style={{ display: 'flex', gap: 10 }} className={updateClass}>
<Text strong>Status</Text>
<Text>:&nbsp;{description}</Text>
</Col>
Expand Down
279 changes: 181 additions & 98 deletions akvo/rsr/spa/app/modules/results-admin/TobeReported.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react'
import React, { useState, useRef, useEffect } from 'react'
import {
List,
Card,
Expand All @@ -9,30 +9,31 @@ import {
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, orderBy } 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'
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 PeriodTitle from './components/PeriodTitle'
import SubmissionsModal from './components/SubmissionsModal'

const { Text } = Typography

const TobeReported = ({
userRdr,
resultRdr,
keyword,
results,
updates,
Expand All @@ -52,11 +53,39 @@ const TobeReported = ({
const [activeKey, setActiveKey] = useState(null)
const [deletion, setDeletion] = useState([])
const [errors, setErrors] = useState([])
const [submissions, setSubmissions] = useState({
scores: [],
updates: [],
enumerators: [],
item: null,
visible: false
})
const [enumerators, setEnumerators] = useState(null)
const formRef = useRef()

const mdParse = SimpleMarkdown.defaultBlockParse
const mdOutput = SimpleMarkdown.defaultOutput
const dataSource = updates?.filter((u) => u?.id === null || u?.userDetails?.id === userRdr?.id)

useEffect(() => {
if (project?.id && enumerators === null) {
api.get(`/project/${project.id}/enumerators/`)
.then(({ data }) => {
const _enumerators = data.reduce((result, {email, name, indicators}) => {
indicators.forEach(indicator => {
if (! result.hasOwnProperty(indicator)) {
result[indicator] = []
}
result[indicator].push({email, name})
})
return result
}, {})
setEnumerators(_enumerators)
})
.catch(() => {
setEnumerators({})
})
}
}, [enumerators, project])

const deleteOnUpdate = (update) => {
Modal.confirm({
Expand Down Expand Up @@ -149,99 +178,153 @@ const TobeReported = ({
formRef.current.form.setConfig('keepDirtyOnReinitialize', true)
}

const handleOnShowSubmissions = item => {
setSubmissions({
...submissions,
item: { ...item, fetched: false },
visible: !submissions.visible
})
api.get(`/indicator_period_data_framework/?period=${item?.period?.id}&format=json`)
.then(({ data }) => {
const { results } = data
setSubmissions({
scores: item?.indicator?.scores,
updates: results,
enumerators: enumerators?.[item?.indicator?.id] || [],
item: {
...item,
fetched: true
},
visible: !submissions.visible
})
})
.catch(() => message.error('Something went wrong'))
}

const handleOnCloseSubmissions = () => {
setSubmissions({
scores: [],
updates: [],
enumerators: [],
item: null,
visible: false
})
}

return (
<List
grid={{ column: 1 }}
itemLayout="vertical"
className="tobe-reported"
dataSource={orderBy(dataSource, ['indicator.title'], ['asc'])}
renderItem={(item, ix) => {
const iKey = item?.id || `${item?.indicator?.id}0${ix}`
const updateClass = item?.statusDisplay?.toLowerCase()?.replace(/\s+/g, '-')
return (
<List.Item className="tobe-reported-item">
<Card className={classNames(updateClass, { active: (activeKey === iKey) })}>
<Row type="flex" justify="space-between" align="middle">
<Col lg={22} md={22} sm={24} xs={24}>
{isEmpty(period) && (
<div className="period-caption">
{moment(item?.period?.periodStart, 'DD/MM/YYYY').format('DD MMM YYYY')} - {moment(item?.period?.periodEnd, 'DD/MM/YYYY').format('DD MMM YYYY')}
</div>
)}
<StatusIndicator status={item?.status} />
<ResultType {...item?.indicator?.result} />
<br />
<Text strong>Title : </Text>
<Highlighted text={item?.indicator?.title} highlight={keyword} />
<br />
{((!isEmpty(item?.indicator?.description.trim())) && item?.indicator?.description?.trim().length > 5) && (
<details>
<summary>{t('Description')}</summary>
<p className="desc hide-for-mobile">{mdOutput(mdParse(item?.indicator?.description))}</p>
</details>
)}
</Col>
<Col lg={2} md={2} sm={24} xs={24} className="action">
{
(activeKey === iKey)
? (
<div className="action-close">
<Button onClick={handleCancel}>
<Icon type="close" />
<span className="action-text">Close</span>
</Button>
</div>
)
: (
<Button
type="link"
onClick={() => {
if (errors.length) {
setErrors([])
}
handleOnEdit(item)
setActiveKey(iKey)
}}
block
>
<SVGInline svg={editButton} className="edit-button" />
<span className="action-text">Edit Value</span>
</Button>
)
}
</Col>
</Row>
</Card>
{(editing && activeKey) && (
<Collapse activeKey={activeKey} bordered={false} accordion>
<Collapse.Panel key={iKey} showArrow={false}>
<ReportedEdit
{...{
activeKey,
formRef,
project,
editing,
editPeriod,
deleteFile,
deletion,
errors,
setErrors,
setActiveKey,
handleOnUpdate,
mneView: true,
deletePendingUpdate: deleteOnUpdate
}}
/>
</Collapse.Panel>
</Collapse>
)}
</List.Item>
)
}}
/>
<>
<SubmissionsModal {...submissions} onClose={handleOnCloseSubmissions} />
<List
grid={{ column: 1 }}
itemLayout="vertical"
className="tobe-reported"
dataSource={updates}
renderItem={(item, ix) => {
const iKey = item?.id || `${item?.indicator?.id}0${ix}`
const allSubmissions = resultRdr
?.filter((r) => r.id === item.result?.id)
?.flatMap((r) => r.indicators)
?.filter((i) => i.id === item.indicator?.id)
?.flatMap((i) => i.periods)
?.filter((p) => (
p.id === item.period?.id &&
p.updates.length &&
!p.locked
))
?.flatMap((p) => p.updates)
const updateClass = (!item.status && allSubmissions.length) ? ACTIVE_PERIOD : kebabCase(item?.statusDisplay)
return (
<List.Item className="tobe-reported-item">
<Card className={classNames(updateClass, { active: (activeKey === iKey) })}>
<Row type="flex" justify="space-between" align="middle">
<Col lg={22} md={22} sm={24} xs={24}>
{isEmpty(period) && <PeriodTitle {...item.period} />}
<StatusIndicator status={item?.status} updateClass={updateClass} />
<ResultType {...item?.indicator?.result} />
<br />
<Text strong>Title : </Text>
<Highlighted text={item?.indicator?.title} highlight={keyword} />
<br />
{((!isEmpty(item?.indicator?.description.trim())) && item?.indicator?.description?.trim().length > 5) && (
<details>
<summary>{t('Description')}</summary>
<p className="desc hide-for-mobile">{mdOutput(mdParse(item?.indicator?.description))}</p>
</details>
)}
</Col>
<Col lg={2} md={2} sm={24} xs={24} className="action">
{
(activeKey === iKey)
? (
<div className="action-close">
<Button onClick={handleCancel}>
<Icon type="close" />
<span className="action-text">Close</span>
</Button>
</div>
)
: (
<>
{(allSubmissions.length > 0) && (
<Tooltip placement="top" title="Submissions">
<Button style={{ borderColor: 'transparent', fontSize: '22px' }} onClick={() => handleOnShowSubmissions(item)}>
<Icon type="solution" />
<span className="action-text">Submissions</span>
</Button>
</Tooltip>
)}
<Tooltip placement="top" title="Edit value">
<Button
style={{ borderColor: 'transparent', fontSize: '22px' }}
onClick={() => {
if (errors.length) {
setErrors([])
}
handleOnEdit(item)
setActiveKey(iKey)
}}
>
<Icon type="form" />
<span className="action-text">Edit Value</span>
</Button>
</Tooltip>
</>
)
}
</Col>
</Row>
</Card>
{(editing && activeKey) && (
<Collapse activeKey={activeKey} bordered={false} accordion>
<Collapse.Panel key={iKey} showArrow={false}>
<ReportedEdit
{...{
activeKey,
formRef,
project,
editing,
editPeriod,
deleteFile,
deletion,
errors,
setErrors,
setActiveKey,
handleOnUpdate,
mneView: true,
deletePendingUpdate: deleteOnUpdate
}}
/>
</Collapse.Panel>
</Collapse>
)}
</List.Item>
)
}}
/>
</>
)
}

export default connect(
(({ resultRdr, userRdr }) => ({ resultRdr, userRdr })), actions
(({ resultRdr }) => ({ resultRdr })), actions
)(TobeReported)
Loading

0 comments on commit 4da8949

Please sign in to comment.