Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add organisation stats page #4103

Merged
merged 3 commits into from
Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@webscopeio/react-textarea-autocomplete": "^4.7.3",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"date-fns": "^2.16.1",
"dompurify": "^2.2.6",
"downshift-hooks": "^0.8.1",
"final-form": "^4.20.1",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Welcome } from './views/welcome';
import { Settings } from './views/settings';
import { ManagementPageIndex, ManagementSection } from './views/management';
import { ListOrganisations, CreateOrganisation, EditOrganisation } from './views/organisations';
import { OrganisationStats } from './views/organisationStats';
import { MyTeams, ManageTeams, CreateTeam, EditTeam, TeamDetail } from './views/teams';
import { ListCampaigns, CreateCampaign, EditCampaign } from './views/campaigns';
import { ListInterests, CreateInterest, EditInterest } from './views/interests';
Expand Down Expand Up @@ -78,9 +79,10 @@ let App = (props) => {
</ProjectsPage>
<ProjectDetailPage path="projects/:id" />
<SelectTask path="projects/:id/tasks" />
<ProjectStats path="projects/:id/stats" />
<MapTask path="projects/:id/map" />
<ValidateTask path="projects/:id/validate" />
<ProjectStats path="projects/:id/stats" />
<OrganisationStats path="organisations/:id/stats/" />
<LearnPage path="learn" />
<QuickstartPage path="learn/quickstart" />
<AboutPage path="about" />
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/components/progressBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';

import { useHover } from '../hooks/UseHover';

export const ProgressBar = ({ className, height, firstBarValue, secondBarValue = 0, children }) => {
const [hoverRef, isHovered, position] = useHover();

/* tooltip component credit: https://codepen.io/syndicatefx/pen/QVPbJg */
return (
<div className={`cf db ${className || ''}`}>
<div className="relative" ref={hoverRef}>
<div
className={`absolute bg-blue-grey br-pill h${height} hide-child`}
style={{ width: `${firstBarValue > 100 ? 100 : firstBarValue}%` }}
role="progressbar"
aria-valuenow={firstBarValue}
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
className={`absolute bg-red br-pill h${height} hide-child`}
style={{ width: `${secondBarValue > 100 ? 100 : secondBarValue}%` }}
role="progressbar"
aria-valuenow={secondBarValue}
aria-valuemin="0"
aria-valuemax="100"
></div>
<div className={`bg-grey-light br-pill h${height} overflow-y-hidden`}></div>
{isHovered && (
<span
className={`db absolute z-1 dib bg-blue-dark ba br2 b--blue-dark pa2 shadow-5 top-${
height === 'half' ? '1' : height
}`}
style={{ left: position.x }}
>
{children}
<span className="absolute top-0 left-2 nt2 w1 h1 bg-blue-dark bl bt b--blue-dark rotate-45"></span>
</span>
)}
</div>
</div>
);
};
81 changes: 28 additions & 53 deletions frontend/src/components/projectCard/projectProgressBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,43 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';

import messages from './messages';
import { useHover } from '../../hooks/UseHover';
import { ProgressBar } from '../progressBar';

export default function ProjectProgressBar({
percentMapped,
percentValidated,
percentBadImagery,
className,
}: Object) {
const [hoverRef, isHovered, position] = useHover();

/* tooltip component credit: https://codepen.io/syndicatefx/pen/QVPbJg */
return (
<>
<div className={`cf db ${className || ''}`}>
<div className="relative" ref={hoverRef}>
<div
className="absolute bg-blue-grey br-pill hhalf hide-child"
style={{ width: `${percentMapped > 100 ? 100 : percentMapped}%` }}
role="progressbar"
aria-valuenow={percentMapped}
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
className="absolute bg-red br-pill hhalf hide-child"
style={{ width: `${percentValidated > 100 ? 100 : percentValidated}%` }}
role="progressbar"
aria-valuenow={percentValidated}
aria-valuemin="0"
aria-valuemax="100"
></div>
<div className={`bg-grey-light br-pill hhalf overflow-y-hidden`}></div>
{isHovered && (
<span
className="db absolute top-1 z-1 dib bg-blue-dark ba br2 b--blue-dark pa2 shadow-5"
style={{ left: position.x - 70 }}
>
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentMapped']}
values={{ n: <span className="fw8">{percentMapped}</span> }}
/>
</p>
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentValidated']}
values={{ n: <span className="fw8">{percentValidated}</span> }}
/>
</p>
{![null, undefined].includes(percentBadImagery) && (
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentBadImagery']}
values={{ n: <span className="fw8">{percentBadImagery}</span> }}
/>
</p>
)}
<span className="absolute top-0 left-2 nt2 w1 h1 bg-blue-dark bl bt b--blue-dark rotate-45"></span>
</span>
)}
</div>
</div>
<ProgressBar
className={className}
firstBarValue={percentMapped}
secondBarValue={percentValidated}
height="half"
>
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentMapped']}
values={{ n: <span className="fw8">{percentMapped}</span> }}
/>
</p>
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentValidated']}
values={{ n: <span className="fw8">{percentValidated}</span> }}
/>
</p>
{![null, undefined].includes(percentBadImagery) && (
<p className="f6 lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentBadImagery']}
values={{ n: <span className="fw8">{percentBadImagery}</span> }}
/>
</p>
)}
</ProgressBar>
</>
);
}
4 changes: 3 additions & 1 deletion frontend/src/components/projectDetail/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default function ProjectTimeline({ tasksByDay }: Object) {
data={formatTimelineData(tasksByDay, CHART_COLOURS.orange, CHART_COLOURS.red)}
options={{
legend: { position: 'top', align: 'end', labels: { boxWidth: 12 } },
tooltips: { callbacks: { label: (tooltip, data) => formatTimelineTooltip(tooltip, data) } },
tooltips: {
callbacks: { label: (tooltip, data) => formatTimelineTooltip(tooltip, data, true) },
},
scales: { xAxes: [{ type: 'time', time: { unit: unit } }] },
}}
/>
Expand Down
73 changes: 61 additions & 12 deletions frontend/src/components/projects/filterSelectFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@ import React from 'react';
import ReactPlaceholder from 'react-placeholder';
import 'react-placeholder/lib/reactPlaceholder.css';
import Select from 'react-select';
import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import { FormattedMessage, useIntl } from 'react-intl';

import { FormattedMessage } from 'react-intl';
import messages from './messages';
import { CalendarIcon } from '../svgIcons';

export const ProjectFilterSelect = (props) => {
const state = props.options;
const fieldsetTitle = <FormattedMessage {...messages[props.fieldsetName]} />;
const fieldsetTitlePlural = <FormattedMessage {...messages[`${props.fieldsetName}s`]} />;
export const ProjectFilterSelect = ({
fieldsetName,
fieldsetStyle,
titleStyle,
selectedTag,
setQueryForChild,
allQueryParamsForChild,
options,
}) => {
const state = options;
const fieldsetTitle = <FormattedMessage {...messages[fieldsetName]} />;
const fieldsetTitlePlural = <FormattedMessage {...messages[`${fieldsetName}s`]} />;

return (
<fieldset id={props.fieldsetName} className={props.fieldsetStyle}>
<legend className={props.titleStyle}>{fieldsetTitle}</legend>
<fieldset id={fieldsetName} className={fieldsetStyle}>
<legend className={titleStyle}>{fieldsetTitle}</legend>
{state.isError ? (
<div className="bg-tan pa4">
<FormattedMessage
Expand All @@ -29,17 +40,55 @@ export const ProjectFilterSelect = (props) => {
<TagFilterPickerAutocomplete
fieldsetTitle={fieldsetTitle}
defaultSelectedItem={fieldsetTitlePlural}
fieldsetName={props.fieldsetName}
queryParamSelectedItem={props.selectedTag || fieldsetTitle}
tagOptionsFromAPI={props.options}
setQuery={props.setQueryForChild}
allQueryParams={props.allQueryParamsForChild}
fieldsetName={fieldsetName}
queryParamSelectedItem={selectedTag || fieldsetTitle}
tagOptionsFromAPI={options}
setQuery={setQueryForChild}
allQueryParams={allQueryParamsForChild}
/>
</ReactPlaceholder>
</fieldset>
);
};

export const DateFilterPicker = ({
fieldsetName,
fieldsetStyle,
titleStyle,
selectedValue,
setQueryForChild,
allQueryParamsForChild,
}) => {
const intl = useIntl();
const dateFormat = 'yyyy-MM-dd';
return (
<fieldset id={fieldsetName} className={fieldsetStyle}>
<legend className={titleStyle}>
<FormattedMessage {...messages[fieldsetName]} />
</legend>
<CalendarIcon className="blue-grey dib w1 pr2 v-mid" />
<DatePicker
selected={selectedValue ? parse(selectedValue, dateFormat, new Date()) : null}
onChange={(date) =>
setQueryForChild(
{
...allQueryParamsForChild,
page: undefined,
[fieldsetName]: date ? format(date, dateFormat) : null,
},
'pushIn',
)
}
dateFormat={dateFormat}
className="w-auto pv2 ph1"
placeholderText={intl.formatMessage(messages[`${fieldsetName}Placeholder`])}
showYearDropdown
scrollableYearDropdown
/>
</fieldset>
);
};

/*
defaultSelectedItem gets appended to top of list as an option for reset
*/
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/projects/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export default defineMessages({
id: 'project.nav.campaign',
defaultMessage: 'Campaign',
},
startDate: {
id: 'navFilters.startDate',
defaultMessage: 'From',
},
startDatePlaceholder: {
id: 'navFilters.startDate.placeholder',
defaultMessage: 'Click to select a start date',
},
endDate: {
id: 'navFilters.endDate',
defaultMessage: 'To',
},
endDatePlaceholder: {
id: 'navFilters.endDatePlace.placeholder',
defaultMessage: 'Click to select an end date',
},
showMapToggle: {
id: 'project.nav.showMapToggle',
defaultMessage: 'Show map',
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/svgIcons/calendar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

// Icon produced by FontAwesome project: https://github.com/FortAwesome/Font-Awesome/
// License: CC-By 4.0
export class CalendarIcon extends React.PureComponent {
render() {
return (
<svg viewBox="0 0 448 512" {...this.props}>
<path
fill="currentColor"
d="M148 288h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm108-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 96v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96-260v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h48V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h128V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h48c26.5 0 48 21.5 48 48zm-48 346V160H48v298c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z"
></path>
</svg>
);
}
}
1 change: 1 addition & 0 deletions frontend/src/components/svgIcons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export { CircleIcon } from './circle';
export { FourCellsGridIcon, NineCellsGridIcon } from './grid';
export { CutIcon } from './cut';
export { FileImportIcon } from './fileImport';
export { CalendarIcon } from './calendar';
53 changes: 53 additions & 0 deletions frontend/src/components/teamsAndOrgs/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,59 @@ export default defineMessages({
id: 'management.organisations',
defaultMessage: 'Organizations',
},
retry: {
id: 'management.organisations.stats.retry',
defaultMessage: 'Try again',
},
errorLoadingStats: {
id: 'management.organisations.stats.error',
defaultMessage: 'An error ocurred while loading stats.',
},
toBeMapped: {
id: 'management.organisations.stats.to_be_mapped',
defaultMessage: 'Tasks to be mapped',
},
tasksMapped: {
id: 'management.organisations.stats.tasks_mapped',
defaultMessage: 'Tasks mapped',
},
readyForValidation: {
id: 'management.organisations.stats.ready_for_validation',
defaultMessage: 'Ready for validation',
},
tasksValidated: {
id: 'management.organisations.stats.tasks_validated',
defaultMessage: 'Tasks validated',
},
actionsNeeded: {
id: 'management.organisations.stats.actions_needed',
defaultMessage: 'Actions needed',
},
completedActions: {
id: 'management.organisations.stats.completed_actions',
defaultMessage: 'Completed actions',
},
actionsNeededHelp: {
id: 'management.organisations.stats.actions_needed.help',
defaultMessage:
'Action means a mapping or validation operation. As each task needs to be mapped and validated, this is the number of actions needed to finish all the published projects of that organization.',
},
levelTooltip: {
id: 'management.organisations.stats.level.tooltip',
defaultMessage: '{n} of {total} ({percent}%) completed to move to level {nextLevel}',
},
levelInfo: {
id: 'management.organisations.stats.level.description',
defaultMessage: '{org} is an organization level {level}.',
},
nextLevelInfo: {
id: 'management.organisations.stats.level.next',
defaultMessage: 'After completing more {n} actions, it will reach the level {nextLevel}.',
},
topLevelInfo: {
id: 'management.organisations.stats.level.top',
defaultMessage: 'It is the highest level an organization can be on Tasking Manager!',
},
orgInfo: {
id: 'management.titles.organisation_information',
defaultMessage: 'Organization information',
Expand Down
Loading