diff --git a/app/assets/scripts/actions/index.js b/app/assets/scripts/actions/index.js index cee3fd126..9bbeb590f 100644 --- a/app/assets/scripts/actions/index.js +++ b/app/assets/scripts/actions/index.js @@ -59,6 +59,40 @@ export const getMe = () => ( fetchJSON('api/v2/user/me/', GET_ME, withToken()) ); +export const GET_REGIONAL_PROJECTS = 'GET_REGIONAL_PROJECTS'; +export const getRegionalProjects = (regionId, filterValues) => { + const filters = { + region: regionId, + limit: 9999, + ...filterValues + }; + const query = buildAPIQS(filters, { arrayFormat: 'comma' }); + return fetchJSON(`api/v2/project/?${query}`, GET_REGIONAL_PROJECTS, withToken()); +}; + +export const GET_REGIONAL_PROJECTS_OVERVIEW = 'GET_REGIONAL_PROJECTS_OVERVIEW'; +export function getRegionalProjectsOverview (regionId) { + return fetchJSON(`api/v2/region-project/${regionId}/overview/`, GET_REGIONAL_PROJECTS_OVERVIEW, withToken()); +} + +export const GET_REGIONAL_MOVEMENT_ACTIVITIES = 'GET_REGIONAL_MOVEMENT_ACTIVITIES'; +export function getRegionalMovementActivities (regionId, filters) { + const query = buildAPIQS(filters, { arrayFormat: 'comma' }); + return fetchJSON(`api/v2/region-project/${regionId}/movement-activities/?${query}`, GET_REGIONAL_MOVEMENT_ACTIVITIES, withToken()); +} + +export const GET_NATIONAL_SOCIETY_ACTIVITIES = 'GET_NATIONAL_SOCIETY_ACTIVITIES'; +export function getNationalSocietyActivities (regionId, filters) { + const query = buildAPIQS(filters, { arrayFormat: 'comma' }); + + return fetchJSON(`api/v2/region-project/${regionId}/national-society-activities/?${query}`, GET_NATIONAL_SOCIETY_ACTIVITIES, withToken()); +} + +export const GET_NATIONAL_SOCIETY_ACTIVITIES_WO_FILTERS = 'GET_NATIONAL_SOCIETY_ACTIVITIES_WO_FILTERS'; +export function getNationalSocietyActivitiesWoFilters (regionId) { + return fetchJSON(`api/v2/region-project/${regionId}/national-society-activities/`, GET_NATIONAL_SOCIETY_ACTIVITIES_WO_FILTERS, withToken()); +} + export const GET_PROJECTS = 'GET_PROJECTS'; export function getProjects (countryId, filterValues) { const filters = { @@ -66,7 +100,7 @@ export function getProjects (countryId, filterValues) { ...filterValues }; const f = buildAPIQS(filters); - return fetchJSON(`api/v2/project/?${f}`, GET_PROJECTS, withToken()); + return fetchJSON(`api/v2/project/?${f}`, GET_PROJECTS, withToken(), { countryId }); } export const POST_PROJECT = 'POST_PROJECT'; diff --git a/app/assets/scripts/components/formatted-number.js b/app/assets/scripts/components/formatted-number.js index c9f563d05..11c71bdbd 100644 --- a/app/assets/scripts/components/formatted-number.js +++ b/app/assets/scripts/components/formatted-number.js @@ -29,7 +29,8 @@ const FormattedNumber = ({ if (addSeparator) { displayNumber = addCommaSeparator(displayNumber); } else if (fixedTo) { - displayNumber = Number.parseFloat(displayNumber).toFixed(fixedTo); + const shouldFix = (displayNumber - Math.floor(displayNumber)) !== 0; + displayNumber = Number.parseFloat(displayNumber).toFixed(shouldFix ? fixedTo : 0); } } diff --git a/app/assets/scripts/components/text-output.js b/app/assets/scripts/components/text-output.js index f3215b250..a5ad8598d 100644 --- a/app/assets/scripts/components/text-output.js +++ b/app/assets/scripts/components/text-output.js @@ -4,36 +4,54 @@ import _cs from 'classnames'; import FormattedNumber from './formatted-number'; import FormattedDate from './formatted-date'; -const TextOutput = ({ - className, - label, - value, - addSeparatorToValue, - normalizeValue, - fixedTo, - type = 'string', -}) => ( -
-
- {label} -
-
- { type === 'string' && value } - { type === 'number' && ( - - )} - { type === 'date' && ( - - )} -
-
+const isValueEmpty = (value) => ( + value === null || value === '' || value === undefined ); +function TextOutput (p) { + const { + className, + label, + value, + addSeparatorToValue, + normalizeValue, + fixedTo, + type = 'string', + reverseOrder, + hideEmptyValue, + } = p; + + return ( + (hideEmptyValue && isValueEmpty(value)) ? ( + null + ) : ( +
+
+ {label} +
+
+ { type === 'string' && value } + { type === 'number' && ( + + )} + { type === 'date' && ( + + )} +
+
+ ) + ); +} + export default TextOutput; diff --git a/app/assets/scripts/reducers/index.js b/app/assets/scripts/reducers/index.js index 27586cafe..62f43b0b5 100644 --- a/app/assets/scripts/reducers/index.js +++ b/app/assets/scripts/reducers/index.js @@ -2,6 +2,8 @@ import { combineReducers } from 'redux'; import { systemAlertsReducer } from '../components/system-alerts'; +import { createReducer } from '../utils/reducer-utils'; + import user from './user'; import profile from './profile'; import countries from './countries'; @@ -66,6 +68,11 @@ export const reducers = { projectForm, projectDelete, countryOverview, + regionalProjectsOverview: createReducer('GET_REGIONAL_PROJECTS_OVERVIEW'), + regionalMovementActivities: createReducer('GET_REGIONAL_MOVEMENT_ACTIVITIES'), + nationalSocietyActivities: createReducer('GET_NATIONAL_SOCIETY_ACTIVITIES'), + nationalSocietyActivitiesWoFilters: createReducer('GET_NATIONAL_SOCIETY_ACTIVITIES_WO_FILTERS'), + regionalProjects: createReducer('GET_REGIONAL_PROJECTS'), me, }; diff --git a/app/assets/scripts/reducers/projects.js b/app/assets/scripts/reducers/projects.js index 8901019b4..062482ecd 100644 --- a/app/assets/scripts/reducers/projects.js +++ b/app/assets/scripts/reducers/projects.js @@ -5,33 +5,19 @@ import { stateSuccess, } from '../utils/reducer-utils'; -const initialState = { - fetching: false, - fetched: false, - receivedAt: null, - data: {} -}; +const initialState = {}; export default function reducer (state = initialState, action) { let newState = { ...state }; switch (action.type) { case 'GET_PROJECTS_INFLIGHT': - newState = { - ...newState, - ...stateInflight(state, action), - }; + newState[action.countryId] = stateInflight(state, action); break; case 'GET_PROJECTS_FAILED': - newState = { - ...newState, - ...stateError(state, action), - }; + newState[action.countryId] = stateError(state, action); break; case 'GET_PROJECTS_SUCCESS': - newState = { - ...newState, - ...stateSuccess(state, action), - }; + newState[action.countryId] = stateSuccess(state, action); break; } diff --git a/app/assets/scripts/selectors/index.js b/app/assets/scripts/selectors/index.js index 3d238d6bb..4466309b5 100644 --- a/app/assets/scripts/selectors/index.js +++ b/app/assets/scripts/selectors/index.js @@ -1,7 +1,38 @@ +const initialState = { + fetching: false, + fetched: false, + receivedAt: null, + data: {} +}; + export const countryOverviewSelector = (state) => ( state.countryOverview ); +export const countryProjectSelector = (state, id) => ( + state.projects[id] || initialState +); + export const meSelector = (state) => ( state.me ); + +export const regionalMovementActivitiesSelector = (state) => ( + state.regionalMovementActivities +); + +export const regionalProjectsOverviewSelector = (state) => ( + state.regionalProjectsOverview +); + +export const nationalSocietyActivitiesSelector = (state) => ( + state.nationalSocietyActivities +); + +export const nationalSocietyActivitiesWoFiltersSelector = (state) => ( + state.nationalSocietyActivitiesWoFilters +); + +export const regionalProjectsSelector = (state) => ( + state.regionalProjects +); diff --git a/app/assets/scripts/utils/constants.js b/app/assets/scripts/utils/constants.js index 51865febf..ee9d44367 100644 --- a/app/assets/scripts/utils/constants.js +++ b/app/assets/scripts/utils/constants.js @@ -24,67 +24,67 @@ export const sectorList = [ key: '0', title: 'WASH', color: '#66c2a5', - inputValue: 'Wash', + inputValue: '0', }, { key: '1', title: 'PGI', color: '#fc8d62', - inputValue: 'Pgi', + inputValue: '1', }, { key: '2', title: 'CEA', color: '#8da0cb', - inputValue: 'Cea', + inputValue: '2', }, { key: '3', title: 'Migration', color: '#e78ac3', - inputValue: 'Migration', + inputValue: '3', }, { key: '4', title: 'Health (public)', color: '#a6d854', - inputValue: 'Health Public', + inputValue: '4', }, { key: '5', title: 'DRR', color: '#ffd92f', - inputValue: 'Drr', + inputValue: '5', }, { key: '6', title: 'Shelter', color: '#e5c494', - inputValue: 'Shelter', + inputValue: '6', }, { key: '7', title: 'NS Strengthening', color: '#b3b3b3', - inputValue: 'NS Strengthening', + inputValue: '7', }, { key: '8', title: 'Education', color: '#b3b3b3', - inputValue: 'Education', + inputValue: '8', }, { key: '9', title: 'Livelihoods and basic needs', color: '#b3b3b3', - inputValue: 'Livelihoods And Basic Needs', + inputValue: '9', }, { key: '10', title: 'Health (clinical)', color: '#a6d854', - inputValue: 'Health Clinical', + inputValue: '10', }, ]; @@ -93,79 +93,85 @@ export const secondarySectorList = [ key: '0', title: 'WASH', color: '#66c2a5', - inputValue: 'Wash', + inputValue: '0', }, { key: '1', title: 'PGI', color: '#fc8d62', - inputValue: 'Pgi', + inputValue: '1', }, { key: '2', title: 'CEA', color: '#8da0cb', - inputValue: 'Cea', + inputValue: '2', }, { key: '3', title: 'Migration', color: '#e78ac3', - inputValue: 'Migration', + inputValue: '3', }, { key: '4', title: 'Health (public)', color: '#a6d854', - inputValue: 'Health Public', + inputValue: '4', }, { key: '5', title: 'DRR', color: '#ffd92f', - inputValue: 'Drr', + inputValue: '5', }, { key: '6', title: 'Shelter', color: '#e5c494', - inputValue: 'Shelter', + inputValue: '6', }, { key: '7', title: 'NS Strengthening', color: '#b3b3b3', - inputValue: 'NS Strengthening', + inputValue: '7', }, { key: '8', title: 'Education', color: '#b3b3b3', - inputValue: 'Education', + inputValue: '8', }, { key: '9', title: 'Livelihoods and basic needs', color: '#b3b3b3', - inputValue: 'Livelihoods And Basic Needs', + inputValue: '9', }, { key: '10', title: 'Recovery', color: '#b3b3b3', - inputValue: 'Recovery', + inputValue: '10', }, { key: '11', title: 'Internal displacement', color: '#b3b3b3', - inputValue: 'Internal Displacement', + inputValue: '11', }, { key: '12', title: 'Health (clinical)', color: '#a6d854', - inputValue: 'Health Clinical', + inputValue: '12', + }, + { + key: '13', + title: 'COVID', + color: '#a6d854', + inputValue: '13', }, ]; @@ -193,8 +199,8 @@ export const statusList = [ export const statuses = listToMap(statusList, d => d.key, d => d.title); export const operationTypeList = [ - { value: 'Programme', label: 'Programme' }, - { value: 'Emergency Operation', label: 'Emergency operation' }, + { value: '0', label: 'Programme' }, + { value: '1', label: 'Emergency operation' }, ]; export const operationTypes = { diff --git a/app/assets/scripts/utils/country-centroids.js b/app/assets/scripts/utils/country-centroids.js index 585ecb5dc..e137e9a89 100644 --- a/app/assets/scripts/utils/country-centroids.js +++ b/app/assets/scripts/utils/country-centroids.js @@ -1,4 +1,6 @@ 'use strict'; +import { countryIsoMapById } from './field-report-constants'; + const centroids = { AD: [1.6261, 42.54203], AE: [54.33582, 23.89438], @@ -203,4 +205,9 @@ export function getCentroid (iso) { return centroids[iso.toUpperCase()] || [0, 0]; } +export function getCentroidByCountryId (id) { + const iso2 = countryIsoMapById[id] || ''; + return getCentroid(iso2); +} + export default centroids; diff --git a/app/assets/scripts/utils/reducer-utils.js b/app/assets/scripts/utils/reducer-utils.js index ae57697d3..de0e5902b 100644 --- a/app/assets/scripts/utils/reducer-utils.js +++ b/app/assets/scripts/utils/reducer-utils.js @@ -23,3 +23,34 @@ export const stateSuccess = (state, action) => { data: action.data }); }; + +const initialState = { + fetching: false, + fetched: false, + receivedAt: null, + data: {} +}; + +export function createReducer (actionName) { + const inflight = `${actionName}_INFLIGHT`; + const failed = `${actionName}_FAILED`; + const success = `${actionName}_SUCCESS`; + + return (state = initialState, action) => { + let newState = state; + + switch (action.type) { + case inflight: + newState = stateInflight(state, action); + break; + case failed: + newState = stateError(state, action); + break; + case success: + newState = stateSuccess(state, action); + break; + } + + return newState; + }; +} diff --git a/app/assets/scripts/utils/request.js b/app/assets/scripts/utils/request.js index e13fc472c..4e7380070 100644 --- a/app/assets/scripts/utils/request.js +++ b/app/assets/scripts/utils/request.js @@ -1,6 +1,7 @@ const emptyObject = {}; +const emptyList = []; -export const getDataFromResponse = (response, defaultValue = emptyObject) => { +export const getDataFromResponse = (response = emptyObject, defaultValue = emptyObject) => { if (!response) { return defaultValue; } @@ -11,3 +12,10 @@ export const getDataFromResponse = (response, defaultValue = emptyObject) => { return response.data; }; + +export const getResultsFromResponse = (response, defaultValue = emptyList) => { + const data = getDataFromResponse(response); + const { results = defaultValue } = data || emptyObject; + + return results; +}; diff --git a/app/assets/scripts/utils/utils.js b/app/assets/scripts/utils/utils.js index 7d745c91b..4d64e922f 100644 --- a/app/assets/scripts/utils/utils.js +++ b/app/assets/scripts/utils/utils.js @@ -5,6 +5,7 @@ import _toNumber from 'lodash.tonumber'; import _find from 'lodash.find'; import _filter from 'lodash.filter'; import { DateTime } from 'luxon'; +import { isNotDefined } from '@togglecorp/fujs'; import { getCentroid } from './country-centroids'; import { disasterType } from './field-report-constants'; @@ -294,3 +295,27 @@ export function getRecordsByType (types, records) { return sortedRecordsByType; } + +export const convertJsonToCsv = (data, columnDelimiter = ',', lineDelimiter = '\n', emptyValue = '') => { + if (!data || data.length <= 0) { + return undefined; + } + + let result = ''; + + data.forEach((items) => { + result += items.map((str) => { + if (isNotDefined(str)) { + return emptyValue; + } + const val = String(str); + if (val.includes(columnDelimiter)) { + return `"${val}"`; + } + return val; + }).join(columnDelimiter); + result += lineDelimiter; + }); + + return result; +}; diff --git a/app/assets/scripts/views/RegionalThreeW/activity-details.js b/app/assets/scripts/views/RegionalThreeW/activity-details.js new file mode 100644 index 000000000..ede500c96 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/activity-details.js @@ -0,0 +1,146 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { + listToGroupList as listToGroupMap, + mapToMap, +} from '@togglecorp/fujs'; +import { MdChevronRight } from 'react-icons/md'; + +import TextOutput from '../../components/text-output'; +import BlockLoading from '../../components/block-loading'; + +import { getResultsFromResponse } from '../../utils/request'; +import { getProjects as getProjectsAction } from '../../actions'; +import { countryProjectSelector } from '../../selectors'; +import { sectors } from '../../utils/constants'; + +const emptyObject = {}; + +function NSDetails (props) { + const { + sectorList, + nsDetails, + } = props.data; + + return ( +
+
+ { nsDetails.society_name } +
+
+ { Object.keys(sectorList).map(sectorId => ( +
+ { sectors[sectorId] }[{ sectorList[sectorId] }] +
+ ))} +
+
+ ); +} + +function ActivityDetails (props) { + const { + data, + projectsResponse, + getProjects, + } = props; + + const { + completed_projects_count: completed, + id: countryId, + name, + ongoing_projects_count: ongoing, + planned_projects_count: planned, + } = data || emptyObject; + + React.useEffect(() => { + if (countryId) { + getProjects(countryId); + } + }, [countryId]); + + const [projectList, pending] = React.useMemo(() => ([ + getResultsFromResponse(projectsResponse), + projectsResponse.fetching, + ]), [projectsResponse]); + + const nsSectorList = React.useMemo(() => { + const projectsByNS = listToGroupMap( + projectList, + d => d.reporting_ns, + d => ({ + sector: d.primary_sector, + nsDetails: d.reporting_ns_detail, + })); + + const sectorGroupedProjectsByNS = Object.keys(projectsByNS).map(nsId => ({ + sectorList: mapToMap( + listToGroupMap(projectsByNS[nsId], d => d.sector, d => true), + undefined, + d => d.length, + ), + nsDetails: projectsByNS[nsId][0].nsDetails, + })); + + return sectorGroupedProjectsByNS; + }, [projectList]); + + return ( +
+

+ + { name } + +

+
+
+ + + +
+
+ +
+
+ { pending ? ( + + ) : ( + nsSectorList.map(d => ( + + )) + )} +
+
+
+ ); +} + +const mapStateToProps = (state, props) => ({ + projectsResponse: countryProjectSelector(state, props.data.id), +}); + +const mapDispatchToProps = (dispatch) => ({ + getProjects: (...args) => dispatch(getProjectsAction(...args)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(React.memo(ActivityDetails)); diff --git a/app/assets/scripts/views/RegionalThreeW/budget-overview.js b/app/assets/scripts/views/RegionalThreeW/budget-overview.js new file mode 100644 index 000000000..82fc4b66a --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/budget-overview.js @@ -0,0 +1,47 @@ +import React from 'react'; +import _cs from 'classnames'; +import FormattedNumber from '../../components/formatted-number'; + +function TextOutput (p) { + const { + label, + value = 0, + } = p; + + return ( +
+ +
+ { label } +
+
+ ); +} + +function BudgetOverview (p) { + const { + totalBudget, + nsCountWithOngoingActivity, + className, + } = p; + + return ( +
+ + +
+ ); +} + +export default BudgetOverview; diff --git a/app/assets/scripts/views/RegionalThreeW/country-table.js b/app/assets/scripts/views/RegionalThreeW/country-table.js new file mode 100644 index 000000000..7f5be505d --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/country-table.js @@ -0,0 +1,156 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { addSeparator } from '@togglecorp/fujs'; +import _cs from 'classnames'; +import { + MdChevronRight, + MdExpandLess, +} from 'react-icons/md'; + +import BlockLoading from '../../components/block-loading'; +import { + programmeTypes, + sectors, + statuses, +} from '../../utils/constants'; +import { getResultsFromResponse } from '../../utils/request'; +import { getProjects as getProjectsAction } from '../../actions'; +import { countryProjectSelector } from '../../selectors'; + +const tableHeaders = [ + { + key: 'reporting_ns', + label: 'Supporting NS', + modifier: d => d.reporting_ns_detail.society_name, + }, + { + key: 'primary_sector', + label: 'Activity sector', + modifier: d => sectors[d.primary_sector], + }, + { + key: 'status', + label: 'Status', + modifier: d => statuses[d.status], + }, + { + key: 'programme_type', + label: 'Type', + modifier: d => programmeTypes[d.programme_type], + }, + { + key: 'target_total', + label: 'Total people targeted', + modifier: d => addSeparator(d.target_total), + }, + { + key: 'reached_total', + label: 'Total people reached', + modifier: d => addSeparator(d.reached_total), + }, + { + key: 'budget_amount', + label: 'Total budget', + modifier: d => addSeparator(d.budget_amount), + }, +]; + +const emptyList = []; + +function CountryTable (p) { + const { + data, + isActive, + onHeaderClick, + projectsResponse, + getProjects, + filters, + } = p; + + React.useEffect(() => { + if (isActive && data.id) { + getProjects(data.id, filters); + } + }, [data.id, isActive, filters]); + + const [projectList, pending] = React.useMemo(() => ([ + getResultsFromResponse(projectsResponse), + projectsResponse.fetching, + ]), [projectsResponse]); + + const handleHeaderClick = React.useCallback(() => { + if (onHeaderClick) { + onHeaderClick(isActive ? undefined : data.id); + } + }, [onHeaderClick, data.id, isActive]); + + const count = (data.projects_count || emptyList).length; + + return ( +
+ + { isActive && ( +
+ { pending ? ( + + ) : ( + + + + { tableHeaders.map(h => ( + + ))} + + + + { projectList.map(p => ( + + { tableHeaders.map(h => ( + + ))} + + ))} + +
+ {h.label} +
+ { h.modifier ? h.modifier(p) : (p[h.key] || '-') } +
+ )} +
+ )} +
+ ); +} + +const mapStateToProps = (state, props) => ({ + projectsResponse: countryProjectSelector(state, props.data.id), +}); + +const mapDispatchToProps = (dispatch) => ({ + getProjects: (...args) => dispatch(getProjectsAction(...args)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CountryTable); diff --git a/app/assets/scripts/views/RegionalThreeW/export-button.js b/app/assets/scripts/views/RegionalThreeW/export-button.js new file mode 100644 index 000000000..727ee3f12 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/export-button.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { saveAs } from 'file-saver'; +import _cs from 'classnames'; + +import { getResultsFromResponse } from '../../utils/request'; +import { convertJsonToCsv } from '../../utils/utils'; +import { getRegionalProjects as getRegionalProjectsAction } from '../../actions'; +import { regionalProjectsSelector } from '../../selectors'; + +import exportHeaders from '../ThreeW/export-headers'; + +function ExportButton (p) { + const { + regionId, + filters, + getRegionalProjects, + projectsResponse, + } = p; + + const [shouldStartRequest, setShouldStartRequest] = React.useState(false); + + React.useEffect(() => { + if (shouldStartRequest) { + getRegionalProjects(regionId, filters); + setShouldStartRequest(false); + } + }, [shouldStartRequest, regionId, filters]); + + const [ + pending, + requestComplete, + projectList, + ] = React.useMemo(() => [ + projectsResponse.fetching, + projectsResponse.fetched, + getResultsFromResponse(projectsResponse), + ], [projectsResponse]); + + React.useEffect(() => { + if (!pending && requestComplete && projectList.length > 0) { + const resolveToValues = (headers, data) => { + const resolvedValues = []; + headers.forEach(header => { + const el = header.modifier ? header.modifier(data) || '' : data[header.key] || ''; + resolvedValues.push(el); + }); + return resolvedValues; + }; + + const csvHeaders = exportHeaders.map(d => d.title); + const resolvedValueList = projectList.map(project => ( + resolveToValues(exportHeaders, project) + )); + + const csv = convertJsonToCsv([ + csvHeaders, + ...resolvedValueList, + ]); + + const blob = new Blob([csv], { type: 'text/csv' }); + const timestamp = (new Date()).getTime(); + const fileName = `projects-export-${timestamp}.csv`; + + saveAs(blob, fileName); + } + }, [pending, requestComplete, projectList]); + + const handleClick = React.useCallback(() => { + setShouldStartRequest(true); + }, [setShouldStartRequest]); + + return ( + + ); +} + +const mapStateToProps = (state, props) => ({ + projectsResponse: regionalProjectsSelector(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + getRegionalProjects: (...args) => dispatch(getRegionalProjectsAction(...args)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ExportButton); diff --git a/app/assets/scripts/views/RegionalThreeW/index.js b/app/assets/scripts/views/RegionalThreeW/index.js new file mode 100644 index 000000000..1aead7698 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/index.js @@ -0,0 +1,305 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { + ResponsiveContainer, + Sankey, + Tooltip, + Layer, + Label, + Rectangle, +} from 'recharts'; +import _cs from 'classnames'; + +import BlockLoading from '../../components/block-loading'; +import { + getRegionalMovementActivities as getRegionalMovementActivitiesAction, + getRegionalProjectsOverview as getRegionalProjectsOverviewAction, + getNationalSocietyActivities as getNationalSocietyActivitiesAction, + getNationalSocietyActivitiesWoFilters as getNationalSocietyActivitiesWoFiltersAction, +} from '../../actions'; + +import { + regionalMovementActivitiesSelector, + regionalProjectsOverviewSelector, + nationalSocietyActivitiesSelector, + nationalSocietyActivitiesWoFiltersSelector, +} from '../../selectors'; + +import { statuses } from '../../utils/constants'; +import { getDataFromResponse } from '../../utils/request'; + +import StatusOverview from './status-overview'; +import BudgetOverview from './budget-overview'; +import CountryTable from './country-table'; +import PeopleOverview from './people-overview'; +import MovementActivitiesFilters from './movement-activities-filters'; +import NSActivitiesFilters from './ns-activities-filters'; +import ExportButton from './export-button'; +import Map from './map'; + +const emptyList = []; + +function SankeyNode (p) { + const { x, y, width, height, index, payload } = p; + if (payload.value === 0) { + return null; + } + + const isOut = (x + width) > window.innerWidth / 2; + + return ( + + + + {payload.name} ({payload.value}) + + + ); +} + +function SankeyLink (p) { + const { + sourceX, + targetX, + sourceY, + targetY, + sourceControlX, + targetControlX, + linkWidth, + index, + } = p; + + const isLeft = targetX < window.innerWidth / 2; + + return ( + + + + ); +} + +function RegionalThreeW (p) { + const { + regionId, + regionalMovementActivitiesResponse, + regionalProjectsOverviewResponse, + nationalSocietyActivitiesResponse, + nationalSocietyActivitiesWoFiltersResponse, + getRegionalMovementActivities, + getRegionalProjectsOverview, + getNationalSocietyActivities, + getNationalSocietyActivitiesWoFilters, + } = p; + + const [activeCountryId, setActiveCountryId] = React.useState(undefined); + const [movementActivityFilters, setMovementActivityFilters] = React.useState({ + operation_type: undefined, + programme_type: undefined, + primary_sector: undefined, + status: undefined, + }); + const [nsActivityFilters, setNSActivityFilters] = React.useState({ + reporting_ns: [], + primary_sector: [], + project_country: [], + }); + + React.useEffect(() => { + getRegionalMovementActivities(regionId, movementActivityFilters); + }, [ + regionId, + getRegionalMovementActivities, + movementActivityFilters, + ]); + + React.useEffect(() => { + getRegionalProjectsOverview(regionId); + }, [regionId, getRegionalProjectsOverview]); + + React.useEffect(() => { + getNationalSocietyActivitiesWoFilters(regionId); + }, [regionId, getNationalSocietyActivitiesWoFilters]); + + React.useEffect(() => { + getNationalSocietyActivities(regionId, nsActivityFilters); + }, [regionId, nsActivityFilters, getNationalSocietyActivities]); + + const [ + movementActivityList, + movementActivityListPending, + ] = React.useMemo(() => ([ + (getDataFromResponse(regionalMovementActivitiesResponse).countries_count || emptyList) + .filter(d => d.projects_count > 0), + regionalMovementActivitiesResponse.fetching, + ]), [regionalMovementActivitiesResponse]); + + const [ + projectsOverview, + projectsOverviewPending, + ] = React.useMemo(() => [ + getDataFromResponse(regionalProjectsOverviewResponse), + regionalProjectsOverviewResponse.fetching, + ], [regionalProjectsOverviewResponse]); + + const [ + nationalSocietyActivities, + nationalSocietyActivitiesPending, + ] = React.useMemo(() => ([ + getDataFromResponse(nationalSocietyActivitiesResponse), + nationalSocietyActivitiesResponse.fetching, + ]), [nationalSocietyActivitiesResponse]); + + const [ + nationalSocietyActivitiesWoFilters, + nationalSocietyActivitiesWoFiltersPending, + ] = React.useMemo(() => ([ + getDataFromResponse(nationalSocietyActivitiesWoFiltersResponse), + nationalSocietyActivitiesWoFiltersResponse.fetching, + ]), [nationalSocietyActivitiesWoFiltersResponse]); + + const sectorActivityData = React.useMemo(() => ( + (projectsOverview.projects_by_status || emptyList).map( + d => ({ label: statuses[d.status], value: d.count }), + ).sort((a, b) => a.label.localeCompare(b.label)) + ), [projectsOverview]); + + return ( +
+ { projectsOverviewPending ? ( + + ) : ( +
+ + + +
+ )} +
+ { movementActivityListPending && } +
+
+

+ Movement activities +

+ +
+ +
+
+ +
+ { movementActivityList.map(c => ( + + ))} +
+
+
+
+
+

+ National society activities +

+ +
+
+ { nationalSocietyActivitiesPending || nationalSocietyActivitiesWoFiltersPending ? ( + + ) : ( + (nationalSocietyActivities.links || emptyList).length === 0 ? ( +
+ Not enough data to show the chart +
+ ) : ( + + + + + + ) + )} +
+
+
+ ); +} + +const mapStateToProps = (state) => ({ + regionalMovementActivitiesResponse: regionalMovementActivitiesSelector(state), + regionalProjectsOverviewResponse: regionalProjectsOverviewSelector(state), + nationalSocietyActivitiesResponse: nationalSocietyActivitiesSelector(state), + nationalSocietyActivitiesWoFiltersResponse: nationalSocietyActivitiesWoFiltersSelector(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + getRegionalMovementActivities: (...args) => dispatch(getRegionalMovementActivitiesAction(...args)), + getRegionalProjectsOverview: (...args) => dispatch(getRegionalProjectsOverviewAction(...args)), + getNationalSocietyActivities: (...args) => dispatch(getNationalSocietyActivitiesAction(...args)), + getNationalSocietyActivitiesWoFilters: (...args) => dispatch(getNationalSocietyActivitiesWoFiltersAction(...args)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(React.memo(RegionalThreeW)); diff --git a/app/assets/scripts/views/RegionalThreeW/map.js b/app/assets/scripts/views/RegionalThreeW/map.js new file mode 100644 index 000000000..f027396ca --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/map.js @@ -0,0 +1,197 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from 'react-dom'; +import { BrowserRouter, Link } from 'react-router-dom'; +import mapboxgl from 'mapbox-gl'; + +import store from '../../utils/store'; +import newMap from '../../utils/get-new-map'; +import { getRegionBoundingBox } from '../../utils/region-bounding-box'; +import { getCentroidByCountryId } from '../../utils/country-centroids'; + +import ActivityDetails from './activity-details'; + +const emptyList = []; + +function getGeojsonFromMovementActivities (movementActivities = emptyList) { + const geojson = { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: movementActivities.map(d => ({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: getCentroidByCountryId(d.id), + }, + properties: { + ...d, + } + })), + } + }; + + return geojson; +} + +function ProgressBar (p) { + const { + value, + max, + } = p; + const width = 100 * value / max; + + return ( +
+
+
+ ); +} + +const MAX_SCALE_STOPS = 6; + +function Scale (p) { + const { max } = p; + const numbers = []; + + const diff = max / MAX_SCALE_STOPS; + + for (let i = 0; i <= max; i += diff) { + numbers.push(i); + } + + return ( +
+ { numbers.map(n =>
{n}
) } +
+ ); +} + +function Map (props) { + const { + regionId, + data, + } = props; + + const ref = React.useRef(); + const [map, setMap] = React.useState(); + const [mapLoaded, setMapLoaded] = React.useState(false); + + React.useEffect(() => { + const { current: mapContainer } = ref; + setMap(newMap(mapContainer)); + }, [setMap]); + + React.useEffect(() => { + if (!map) { + return; + } + + map.on('load', () => { + const bbox = getRegionBoundingBox(regionId); + map.fitBounds(bbox); + setMapLoaded(true); + }); + }, [map, setMapLoaded]); + + React.useEffect(() => { + if (!map || !mapLoaded) { + return; + } + + const geojson = getGeojsonFromMovementActivities(data); + + try { + if (map.getLayer('movement-activity-circles')) { + map.removeLayer('movement-activity-circles'); + } + map.removeSource('movement-activity-markers'); + } catch (err) { + // pass + } + + map.addSource('movement-activity-markers', geojson); + map.addLayer({ + id: 'movement-activity-circles', + source: 'movement-activity-markers', + type: 'circle', + paint: { + 'circle-radius': 7, + 'circle-color': '#f5333f', + 'circle-opacity': 0.7, + }, + }); + + map.on('click', 'movement-activity-circles', (e) => { + const properties = e.features[0].properties; + const popoverContent = document.createElement('div'); + + render( + ( + + + + + + ), + popoverContent, + ); + + new mapboxgl.Popup({ closeButton: false }) + .setLngLat(e.lngLat) + .setDOMContent(popoverContent.children[0]) + .addTo(map); + }); + }, [map, regionId, data, mapLoaded]); + + const [supportingNSList, maxProjects] = React.useMemo(() => { + const maxProjects = Math.max(...data.map(d => d.projects_count)); + const numBuckets = Math.ceil(maxProjects / MAX_SCALE_STOPS); + const max = numBuckets * MAX_SCALE_STOPS; + + return [ + data.map(d => ({ + id: d.id, + name: d.name, + value: d.projects_count, + })), + max, + ]; + }, [data]); + + return ( +
+
+
+ +
+ { supportingNSList.map(d => ( +
+
+
+ + { d.name } + +
+
+ {d.value} projects +
+
+ +
+ ))} +
+
+
+ ); +} + +export default React.memo(Map); diff --git a/app/assets/scripts/views/RegionalThreeW/movement-activities-filters.js b/app/assets/scripts/views/RegionalThreeW/movement-activities-filters.js new file mode 100644 index 000000000..d44f98226 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/movement-activities-filters.js @@ -0,0 +1,90 @@ +import React from 'react'; +import _cs from 'classnames'; +import Faram from '@togglecorp/faram'; + +import SelectInput from '../../components/form-elements/select-input'; +import { + statusList, + sectorList, + programmeTypeList, + operationTypeList, +} from '../../utils/constants'; + +const compareString = (a, b) => a.label.localeCompare(b.label); + +const programmeTypeOptions = programmeTypeList.map(p => ({ + value: p.key, + label: p.title, +})).sort(compareString); + +const sectorsOfActivityOptions = sectorList.map(p => ({ + value: p.inputValue, + label: p.title, +})).sort(compareString); + +const statusOptions = statusList.map(p => ({ + value: p.key, + label: p.title, +})).sort(compareString); + +const operationTypeOptions = operationTypeList.map(o => ({ + value: o.value, + label: o.label, +})).sort(compareString); + +const filterSchema = { + fields: { + operation_type: [], + programme_type: [], + primary_sector: [], + status: [] + } +}; + +function MovementActivitiesFilters (p) { + const { + className, + value, + onChange, + } = p; + + return ( + + + + + + + ); +} + +export default MovementActivitiesFilters; diff --git a/app/assets/scripts/views/RegionalThreeW/ns-activities-filters.js b/app/assets/scripts/views/RegionalThreeW/ns-activities-filters.js new file mode 100644 index 000000000..345146a3e --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/ns-activities-filters.js @@ -0,0 +1,79 @@ +import React from 'react'; +import Faram from '@togglecorp/faram'; + +import SelectInput from '../../components/form-elements/select-input'; + +const filterSchema = { + fields: { + reporting_ns: [], + project_country: [], + primary_sector: [], + } +}; + +const emptyData = { + nodes: [], +}; + +function toLabelValue (d) { + return { + label: d.name, + value: d.id, + }; +} + +const compareString = (a, b) => a.label.localeCompare(b.label); + +function NSActivitiesFilters (p) { + const { + value, + onChange, + data = emptyData, + } = p; + + const [ + supportingNSOptions, + activityOptions, + receivingNSOptions, + ] = React.useMemo(() => [ + (data.nodes || []).filter(d => d.type === 'supporting_ns').map(toLabelValue).sort(compareString), + (data.nodes || []).filter(d => d.type === 'sector').map(toLabelValue).sort(compareString), + (data.nodes || []).filter(d => d.type === 'receiving_ns').map(toLabelValue).sort(compareString), + ], [data]); + + return ( + + + + + + ); +} + +export default NSActivitiesFilters; diff --git a/app/assets/scripts/views/RegionalThreeW/people-overview.js b/app/assets/scripts/views/RegionalThreeW/people-overview.js new file mode 100644 index 000000000..2360751c3 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/people-overview.js @@ -0,0 +1,54 @@ +import React from 'react'; +import _cs from 'classnames'; + +import FormattedNumber from '../../components/formatted-number'; + +function PeopleOverview (props) { + const { + targeted = 0, + reached = 0, + className, + } = props; + + const barStyle = React.useMemo(() => { + let progress = 0; + + if (targeted && targeted !== 0) { + progress = Math.min(100, 100 * reached / targeted); + } + + return { width: `${progress}%` }; + }, [targeted, reached]); + + return ( +
+

+ Total number of people reached +

+
+ +
+
+ Targeted +
+ +
+
+
+
+
+
+ ); +} + +export default PeopleOverview; diff --git a/app/assets/scripts/views/RegionalThreeW/status-overview.js b/app/assets/scripts/views/RegionalThreeW/status-overview.js new file mode 100644 index 000000000..92f700a82 --- /dev/null +++ b/app/assets/scripts/views/RegionalThreeW/status-overview.js @@ -0,0 +1,78 @@ +import React from 'react'; +import _cs from 'classnames'; + +import { + ResponsiveContainer, + PieChart, + Pie, + Legend, + Tooltip, + Cell, +} from 'recharts'; + +import FormattedNumber from '../../components/formatted-number'; + +const colors = { + 'Completed': '#f5333f', + 'Ongoing': '#f7969c', + 'Planned': '#f9e5e6', +}; + +function StatusOverview (p) { + const { + data, + total, + className, + } = p; + + return ( +
+

+ Total activities by status +

+
+
+ +
+ Total activities +
+
+
+ + + + { data.map((entry, index) => { + return ( + + ); + })} + + + + + +
+
+
+ ); +} + +export default StatusOverview; diff --git a/app/assets/scripts/views/ThreeW/export-headers.js b/app/assets/scripts/views/ThreeW/export-headers.js index 51ac830ef..0715ffba3 100644 --- a/app/assets/scripts/views/ThreeW/export-headers.js +++ b/app/assets/scripts/views/ThreeW/export-headers.js @@ -28,12 +28,12 @@ const exportHeaders = [ modifier: r => operationTypes[r.operation_type], }, { - title: 'Programme Type', + title: 'Programme type', key: 'programme_type', modifier: r => programmeTypes[r.programme_type], }, { - title: 'Project Name', + title: 'Activity name', key: 'name', }, { diff --git a/app/assets/scripts/views/ThreeW/filter.js b/app/assets/scripts/views/ThreeW/filter.js index ca6d6be61..1ac714597 100644 --- a/app/assets/scripts/views/ThreeW/filter.js +++ b/app/assets/scripts/views/ThreeW/filter.js @@ -12,20 +12,22 @@ import { programmeTypeList, } from '../../utils/constants'; +const compareString = (a, b) => a.label.localeCompare(b.label); + const programmeTypeOptions = programmeTypeList.map(p => ({ value: p.title, label: p.title, -})); +})).sort(compareString); const sectorsOfActivityOptions = sectorList.map(p => ({ value: p.inputValue, label: p.title, -})); +})).sort(compareString); const statusOptions = statusList.map(p => ({ value: p.title, label: p.title, -})); +})).sort(compareString); export default class ThreeWFilter extends React.PureComponent { constructor (props) { diff --git a/app/assets/scripts/views/ThreeW/index.js b/app/assets/scripts/views/ThreeW/index.js index 0b2b8dd15..ba9e725e6 100644 --- a/app/assets/scripts/views/ThreeW/index.js +++ b/app/assets/scripts/views/ThreeW/index.js @@ -3,9 +3,9 @@ import React from 'react'; import _cs from 'classnames'; import memoize from 'memoize-one'; import { saveAs } from 'file-saver'; -import { isNotDefined } from '@togglecorp/fujs'; import { getDataFromResponse } from '../../utils/request'; +import { convertJsonToCsv } from '../../utils/utils'; import Summary from './stats/summary'; import SectorActivity from './stats/sector-activity'; @@ -16,29 +16,6 @@ import Table from './table'; import Map from './map'; import exportHeaders from './export-headers'; -const convertJsonToCsv = (data, columnDelimiter = ',', lineDelimiter = '\n', emptyValue = '') => { - if (!data || data.length <= 0) { - return undefined; - } - - let result = ''; - - data.forEach((items) => { - result += items.map((str) => { - if (isNotDefined(str)) { - return emptyValue; - } - const val = String(str); - if (val.includes(columnDelimiter)) { - return `"${val}"`; - } - return val; - }).join(columnDelimiter); - result += lineDelimiter; - }); - - return result; -}; export default class ThreeW extends React.PureComponent { getIsCountryAdmin = memoize((user, countryId) => { diff --git a/app/assets/scripts/views/ThreeW/map.js b/app/assets/scripts/views/ThreeW/map.js index 7d409ee9f..7e63f66d8 100644 --- a/app/assets/scripts/views/ThreeW/map.js +++ b/app/assets/scripts/views/ThreeW/map.js @@ -179,7 +179,7 @@ class ThreeWMap extends React.PureComponent { this.resetBounds(countryId); const groupedProjects = listToGroupList( - projectList, + projectList.filter(d => d.project_district), project => project.project_district, project => project, ); diff --git a/app/assets/scripts/views/ThreeW/project-details.js b/app/assets/scripts/views/ThreeW/project-details.js index f5c4a8439..9e65ab632 100644 --- a/app/assets/scripts/views/ThreeW/project-details.js +++ b/app/assets/scripts/views/ThreeW/project-details.js @@ -93,7 +93,7 @@ class ProjectDetails extends React.PureComponent {
@@ -109,7 +109,7 @@ class ProjectDetails extends React.PureComponent {
@@ -137,10 +137,12 @@ class ProjectDetails extends React.PureComponent {
@@ -152,10 +154,13 @@ class ProjectDetails extends React.PureComponent { label='Primary sector' value={sectors[primary_sector]} /> - secondarySectors[d])).join(', ')} - /> + {secondary_sectors.length > 0 && ( + secondarySectors[d])).join(', ')} + hideEmptyValue + /> + )}
@@ -165,18 +170,26 @@ class ProjectDetails extends React.PureComponent {
@@ -188,18 +201,26 @@ class ProjectDetails extends React.PureComponent {
diff --git a/app/assets/scripts/views/ThreeW/project-form-modal.js b/app/assets/scripts/views/ThreeW/project-form-modal.js index 4e58702d0..f9e0a86c8 100644 --- a/app/assets/scripts/views/ThreeW/project-form-modal.js +++ b/app/assets/scripts/views/ThreeW/project-form-modal.js @@ -4,13 +4,13 @@ import _cs from 'classnames'; import ProjectForm from './project-form'; -function ProjectFormModal (props) { +function ProjectFormModal (p) { const { projectData, countryId, onCloseButtonClick, pending, - } = props; + } = p; return ( diff --git a/app/assets/scripts/views/ThreeW/project-form.js b/app/assets/scripts/views/ThreeW/project-form.js index 8b781cb95..94167bbe8 100644 --- a/app/assets/scripts/views/ThreeW/project-form.js +++ b/app/assets/scripts/views/ThreeW/project-form.js @@ -16,6 +16,7 @@ import TextInput from '../../components/form-elements/text-input'; import NumberInput from '../../components/form-elements/number-input'; import DateInput from '../../components/form-elements/date-input'; import Checkbox from '../../components/form-elements/faram-checkbox'; +import TextOutput from '../../components/text-output'; import { getCountries, @@ -29,16 +30,13 @@ import { } from '../../utils/field-report-constants'; import { - statusList, + // statusList, statuses, sectorList, secondarySectorInputValues, secondarySectorList, - sectorInputValues, programmeTypeList, - programmeTypes, operationTypeList, - operationTypes, projectVisibilityList, } from '../../utils/constants'; @@ -52,10 +50,12 @@ const positiveIntegerCondition = (value) => { const compareString = (a, b) => a.label.localeCompare(b.label); +/* const statusOptions = statusList.map(p => ({ - value: p.title, + value: p.key, label: p.title, })).sort(compareString); +*/ const sectorOptions = sectorList.map(p => ({ value: p.inputValue, @@ -68,7 +68,7 @@ const secondarySectorOptions = secondarySectorList.map(p => ({ })).sort(compareString); const programmeTypeOptions = programmeTypeList.map(p => ({ - value: p.title, + value: p.key, label: p.title, })).sort(compareString); @@ -170,9 +170,9 @@ class ProjectForm extends React.PureComponent { dtype: projectData.dtype, project_district: projectData.project_district ? projectData.project_district : 'all', name: projectData.name, - operation_type: operationTypes[projectData.operation_type], - primary_sector: sectorInputValues[projectData.primary_sector], - programme_type: programmeTypes[projectData.programme_type], + operation_type: projectData.operation_type, + primary_sector: projectData.primary_sector, + programme_type: projectData.programme_type, end_date: projectData.end_date, start_date: projectData.start_date, reached_other: projectData.reached_other || undefined, @@ -182,7 +182,7 @@ class ProjectForm extends React.PureComponent { reporting_ns: projectData.reporting_ns, secondary_sectors: projectData.secondary_sectors ? projectData.secondary_sectors.map(d => secondarySectorInputValues[d]) : [], is_project_completed: projectData.status === 2, - status: statuses[projectData.status], + status: projectData.status, target_other: projectData.target_other || undefined, target_female: projectData.target_female || undefined, target_male: projectData.target_male || undefined, @@ -280,7 +280,7 @@ class ProjectForm extends React.PureComponent { })); const operationToDisasterMap = {}; - currentOperationList.forEach(d => { operationToDisasterMap[d.id] = d.dtype.id; }); + currentOperationList.forEach(d => { operationToDisasterMap[d.id] = (d.dtype || {}).id; }); const currentEmergencyOperationOptions = currentOperationList .filter(d => d.auto_generated_source === 'New field report') @@ -298,21 +298,21 @@ class ProjectForm extends React.PureComponent { getProjectStatusFaramValue = memoize((start, isCompleted) => { if (isCompleted) { - return { status: 'Completed' }; + return { status: '2' }; } if (!start) { - return { status: 'Planned' }; + return { status: '0' }; } const startDate = new Date(start); const today = new Date(); if (startDate.getTime() <= today.getTime()) { - return { status: 'Ongoing' }; + return { status: '1' }; } - return { status: 'Planned' }; + return { status: '0' }; }) getTargetedTotalFaramValue = memoize((male, female, other) => { @@ -452,6 +452,7 @@ class ProjectForm extends React.PureComponent { const fetchingCountries = countries && countries.fetching; const shouldDisableCountryInput = fetchingCountries; + const fetchingDistricts = districts && districts[faramValues.project_country] && districts[faramValues.project_country].fetching; const shouldDisableDistrictInput = fetchingCountries || fetchingDistricts; const fetchingEvents = eventList && eventList.fetching; @@ -584,9 +585,23 @@ class ProjectForm extends React.PureComponent { /> )} + { shouldShowCurrentEmergencyOperation && ( + + + + )}
- +
@@ -698,7 +710,7 @@ class ProjectForm extends React.PureComponent { /> { +const ProgressBar = (p) => { + const { + value, + max, + } = p; const width = 100 * value / max; return ( @@ -21,9 +22,15 @@ const ProgressBar = ({ ); }; -const Scale = ({ max }) => { +const MAX_SCALE_STOPS = 5; + +function Scale (p) { + const { max } = p; const numbers = []; - for (let i = 0; i <= max; i++) { + + const diff = max / MAX_SCALE_STOPS; + + for (let i = 0; i <= max; i += diff) { numbers.push(i); } @@ -32,7 +39,7 @@ const Scale = ({ max }) => { { numbers.map(n =>
{n}
) } ); -}; +} export default class RegionOverview extends React.PureComponent { render () { @@ -49,6 +56,8 @@ export default class RegionOverview extends React.PureComponent { const projectDistrictList = Object.keys(groupedProjectList); const maxProjects = Math.max(...projectDistrictList.map(d => groupedProjectList[d].length)); + const numBuckets = Math.ceil(maxProjects / MAX_SCALE_STOPS); + const max = numBuckets * MAX_SCALE_STOPS; return (
@@ -56,7 +65,7 @@ export default class RegionOverview extends React.PureComponent { Regions
{ projectDistrictList.map(d => { @@ -69,11 +78,11 @@ export default class RegionOverview extends React.PureComponent { className='three-w-region-district' >
- {regionName} ({p.length} projects) + {regionName} ({p.length} activities)
); diff --git a/app/assets/scripts/views/ThreeW/stats/sector-activity.js b/app/assets/scripts/views/ThreeW/stats/sector-activity.js index 49815008f..dacee9cfd 100644 --- a/app/assets/scripts/views/ThreeW/stats/sector-activity.js +++ b/app/assets/scripts/views/ThreeW/stats/sector-activity.js @@ -11,6 +11,7 @@ import { Bar, XAxis, YAxis, + Tooltip, } from 'recharts'; import { sectorList } from '../../../utils/constants'; @@ -51,7 +52,7 @@ export default class SectorActivity extends React.PureComponent { return (

- Projects by sector of activity + Activities by sector

@@ -72,6 +73,7 @@ export default class SectorActivity extends React.PureComponent { fill='#c1cdd1' dataKey='value' /> +
diff --git a/app/assets/scripts/views/ThreeW/stats/status-overview.js b/app/assets/scripts/views/ThreeW/stats/status-overview.js index 8bed1a71e..98f85b52d 100644 --- a/app/assets/scripts/views/ThreeW/stats/status-overview.js +++ b/app/assets/scripts/views/ThreeW/stats/status-overview.js @@ -54,7 +54,7 @@ export default class StatusOverview extends React.PureComponent { return (

- Project status overview + Activity status overview

diff --git a/app/assets/scripts/views/ThreeW/table.js b/app/assets/scripts/views/ThreeW/table.js index 898e68f97..f2d91ba57 100644 --- a/app/assets/scripts/views/ThreeW/table.js +++ b/app/assets/scripts/views/ThreeW/table.js @@ -6,6 +6,14 @@ import { } from '@togglecorp/fujs'; import memoize from 'memoize-one'; import url from 'url'; +import { + MdContentCopy, + MdSearch, + MdEdit, + MdDeleteForever, + MdHistory, + MdMoreHoriz, +} from 'react-icons/md'; import { programmeTypes, @@ -34,7 +42,7 @@ export default class ProjectListTable extends React.PureComponent { }, { key: 'name', - label: 'Project name', + label: 'Activity name', }, { key: 'reporting_ns', @@ -83,13 +91,13 @@ export default class ProjectListTable extends React.PureComponent { } + label={} >