Skip to content

Commit

Permalink
feat(datatrakWeb): RN-1374: Task Metrics (#5860)
Browse files Browse the repository at this point in the history
* task metrics

* types update

* Update useCreateTask.ts

* review changes

* review updates

* update to MUI breakpoints

* Review updates

* Update TaskCompletionHandler.js
  • Loading branch information
hrazasalman authored Aug 29, 2024
1 parent 1ee66fb commit 12a9021
Show file tree
Hide file tree
Showing 22 changed files with 234 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export class TaskCompletionHandler extends ChangeHandler {
dataTime: surveyResponse.data_time,
})),
);

return this.models.task.find({
[QUERY_CONJUNCTIONS.AND]: {
status: 'to_do',
Expand All @@ -54,7 +53,7 @@ export class TaskCompletionHandler extends ChangeHandler {
},
[QUERY_CONJUNCTIONS.RAW]: {
sql: `${surveyIdAndEntityIdPairs
.map(() => `(survey_id = ? AND entity_id = ? AND created_at <= ?)`)
.map(() => `(task.survey_id = ? AND task.entity_id = ? AND created_at <= ?)`)
.join(' OR ')}`,
parameters: surveyIdAndEntityIdPairs.flatMap(({ surveyId, entityId, dataTime }) => [
surveyId,
Expand Down
8 changes: 7 additions & 1 deletion packages/database/src/modelClasses/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ export class TaskRecord extends DatabaseRecord {
{
joinWith: RECORDS.SURVEY,
joinCondition: ['survey_id', `${RECORDS.SURVEY}.id`],
fields: { name: 'survey_name', code: 'survey_code' },
fields: { name: 'survey_name', code: 'survey_code', project_id: 'project_id' },
},
{
joinWith: RECORDS.SURVEY_RESPONSE,
joinType: JOIN_TYPES.LEFT,
joinCondition: ['survey_response_id', `${RECORDS.SURVEY_RESPONSE}.id`],
fields: { data_time: 'data_time', timezone: 'timezone' },
},
];

Expand Down
3 changes: 3 additions & 0 deletions packages/datatrak-web-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
SurveysRoute,
SurveyUsersRequest,
SurveyUsersRoute,
TaskMetricsRequest,
TaskMetricsRoute,
TaskRequest,
TaskRoute,
TasksRequest,
Expand Down Expand Up @@ -95,6 +97,7 @@ export async function createApp() {
.get<ProjectRequest>('project/:projectCode', handleWith(ProjectRoute))
.get<RecentSurveysRequest>('recentSurveys', handleWith(RecentSurveysRoute))
.get<ActivityFeedRequest>('activityFeed', handleWith(ActivityFeedRoute))
.get<TaskMetricsRequest>('taskMetrics/:projectId', handleWith(TaskMetricsRoute))
.get<TasksRequest>('tasks', handleWith(TasksRoute))
.get<TaskRequest>('tasks/:taskId', handleWith(TaskRoute))
.get<SingleSurveyResponseRequest>('surveyResponse/:id', handleWith(SingleSurveyResponseRoute))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const handleTaskCompletion = async (
},
},
[QUERY_CONJUNCTIONS.RAW]: {
sql: `(survey_id = ? AND entity_id = ? AND created_at <= ?)`,
sql: `(task.survey_id = ? AND task.entity_id = ? AND task.created_at <= ?)`,
parameters: [surveyId, entityId, dataTime],
},
},
Expand Down
79 changes: 79 additions & 0 deletions packages/datatrak-web-server/src/routes/TaskMetricsRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import { getOffsetForTimezone } from '@tupaia/utils';
import { DatatrakWebTaskMetricsRequest, TaskStatus } from '@tupaia/types';
import { QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database';

export type TaskMetricsRequest = Request<
DatatrakWebTaskMetricsRequest.Params,
DatatrakWebTaskMetricsRequest.ResBody,
DatatrakWebTaskMetricsRequest.ReqBody,
DatatrakWebTaskMetricsRequest.ReqQuery
>;

export class TaskMetricsRoute extends Route<TaskMetricsRequest> {
public async buildResponse() {
const { params, models } = this.req;
const { projectId } = params;
const baseQuery = { 'survey.project_id': projectId };
const baseJoin = { joinWith: RECORDS.SURVEY, joinCondition: ['survey.id', 'task.survey_id'] };

const unassignedTasks = await models.task.count(
{
...baseQuery,
[QUERY_CONJUNCTIONS.RAW]: {
sql: `assignee_id IS NULL`,
},
},
baseJoin,
);

const overdueTasks = await models.task.count(
{
...baseQuery,
due_date: {
comparator: '<=',
comparisonValue: new Date().getTime(),
},
},
baseJoin,
);

const completedTasks = await models.task.find(
// @ts-ignore
{
...baseQuery,
status: TaskStatus.completed,
[QUERY_CONJUNCTIONS.RAW]: {
sql: `repeat_schedule IS NULL`,
},
},
{
columns: ['due_date', 'data_time', 'timezone', 'project_id'],
},
);

const onTimeCompletedTasks = completedTasks.filter(record => {
if (!record.due_date || !record.data_time) {
return false;
}
const { data_time: dataTime, timezone } = record;
const offset = getOffsetForTimezone(timezone, new Date(dataTime));
const formattedDate = `${dataTime.toString().replace(' ', 'T')}${offset}`;
return new Date(formattedDate).getTime() <= record.due_date;
});

const onTimeCompletionRate = (completedTasks.length / onTimeCompletedTasks.length) * 100 || 0;

return {
unassignedTasks,
overdueTasks,
onTimeCompletionRate,
};
}
}
1 change: 1 addition & 0 deletions packages/datatrak-web-server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { LeaderboardRequest, LeaderboardRoute } from './LeaderboardRoute';
export { ActivityFeedRequest, ActivityFeedRoute } from './ActivityFeedRoute';
export { EntitiesRequest, EntitiesRoute } from './EntitiesRoute';
export { GenerateLoginTokenRequest, GenerateLoginTokenRoute } from './GenerateLoginTokenRoute';
export { TaskMetricsRequest, TaskMetricsRoute } from './TaskMetricsRoute';
export { TasksRequest, TasksRoute } from './TasksRoute';
export { TaskRequest, TaskRoute } from './TaskRoute';
export { SurveyUsersRequest, SurveyUsersRoute } from './SurveyUsersRoute';
Expand Down
3 changes: 3 additions & 0 deletions packages/datatrak-web/src/api/mutations/useCreateTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { useMutation, useQueryClient } from 'react-query';
import { DatatrakWebTaskChangeRequest } from '@tupaia/types';
import { post } from '../api';
import { successToast } from '../../utils';
import { useCurrentUserContext } from '../CurrentUserContext';

export const useCreateTask = (onSuccess?: () => void) => {
const queryClient = useQueryClient();
const { projectId } = useCurrentUserContext();
return useMutation<any, Error, DatatrakWebTaskChangeRequest.ReqBody, unknown>(
(data: DatatrakWebTaskChangeRequest.ReqBody) => {
return post('tasks', {
Expand All @@ -19,6 +21,7 @@ export const useCreateTask = (onSuccess?: () => void) => {
{
onSuccess: () => {
queryClient.invalidateQueries('tasks');
queryClient.invalidateQueries(['taskMetric', projectId]);
successToast('Task successfully created');
if (onSuccess) onSuccess();
},
Expand Down
3 changes: 3 additions & 0 deletions packages/datatrak-web/src/api/mutations/useEditTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useMutation, useQueryClient } from 'react-query';
import { Task } from '@tupaia/types';
import { put } from '../api';
import { successToast } from '../../utils';
import { useCurrentUserContext } from '../CurrentUserContext';

type PartialTask = Partial<Task>;

export const useEditTask = (taskId?: Task['id'], onSuccess?: () => void) => {
const queryClient = useQueryClient();
const { projectId } = useCurrentUserContext();
return useMutation<any, Error, PartialTask, unknown>(
(task: PartialTask) => {
return put(`tasks/${taskId}`, {
Expand All @@ -22,6 +24,7 @@ export const useEditTask = (taskId?: Task['id'], onSuccess?: () => void) => {
onSuccess: () => {
queryClient.invalidateQueries('tasks');
queryClient.invalidateQueries(['tasks', taskId]);
queryClient.invalidateQueries(['taskMetric', projectId]);
successToast('Task updated successfully');
if (onSuccess) onSuccess();
},
Expand Down
1 change: 1 addition & 0 deletions packages/datatrak-web/src/api/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export { useUserRewards } from './useUserRewards';
export { useActivityFeed, useCurrentProjectActivityFeed } from './useActivityFeed';
export { useProjectSurveys } from './useProjectSurveys';
export { useEntities } from './useEntities';
export { useTaskMetrics } from './useTaskMetrics';
export { useTasks } from './useTasks';
export { useTask } from './useTask';
export { useSurveyUsers } from './useSurveyUsers';
Expand Down
18 changes: 18 additions & 0 deletions packages/datatrak-web/src/api/queries/useTaskMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { useQuery } from 'react-query';
import { DatatrakWebTaskMetricsRequest } from '@tupaia/types';
import { get } from '../api';

export const useTaskMetrics = (projectId?: string) => {
return useQuery(
['taskMetric', projectId],
(): Promise<DatatrakWebTaskMetricsRequest.ResBody> => get(`taskMetrics/${projectId}`),
{
enabled: !!projectId,
},
);
};
51 changes: 51 additions & 0 deletions packages/datatrak-web/src/components/TaskMetrics/TaskMetric.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { SpinningLoader } from '@tupaia/ui-components';
import React from 'react';
import styled from 'styled-components';

const MetricWrapper = styled.div`
display: flex;
border: 1px solid;
border-radius: 3px;
margin-inline: 0.5rem;
margin-block-end: auto;
${({ theme }) => theme.breakpoints.down('xs')} {
width: inherit;
margin-block-start: 0.5rem;
margin-inline: 0;
}
`;

const MetricNumber = styled.p`
font-size: 0.875rem;
line-height: 1.75;
padding: 0.5rem 1.75rem;
border-right: 1px solid ${({ theme }) => theme.palette.divider};
min-width: 3rem;
padding-inline: 0.9rem;
align-content: center;
text-align: center;
font-weight: 500;
margin: 0;
`;

const MetricText = styled.p`
line-height: 1.75;
letter-spacing: 0;
padding: 0.5rem 1.75rem;
padding-inline-end: 1.2rem;
padding-inline-start: 0.9rem;
font-weight: 500;
margin: 0;
${({ theme }) => theme.breakpoints.up('lg')} {
min-width: 12rem;
}
`;

export const TaskMetric = ({ number, text, isLoading }) => {
return (
<MetricWrapper>
<MetricNumber>{isLoading ? <SpinningLoader spinnerSize={14} /> : number}</MetricNumber>
<MetricText>{text}</MetricText>
</MetricWrapper>
);
};
33 changes: 33 additions & 0 deletions packages/datatrak-web/src/components/TaskMetrics/TaskMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import styled from 'styled-components';
import { TaskMetric } from './TaskMetric';
import { useCurrentUserContext, useTaskMetrics } from '../../api';

const TaskMetricsContainer = styled.div`
margin-block-end: 0;
gap: 0.5rem;
${({ theme }) => theme.breakpoints.up('xs')} {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
${({ theme }) => theme.breakpoints.down('xs')} {
width: inherit;
}
`;

export const TaskMetrics = () => {
const { projectId } = useCurrentUserContext();
const { data: metrics, isLoading } = useTaskMetrics(projectId);
return (
<TaskMetricsContainer>
<TaskMetric text="Unassigned tasks" number={metrics?.unassignedTasks} isLoading={isLoading} />
<TaskMetric text="Overdue tasks" number={metrics?.overdueTasks} isLoading={isLoading} />
<TaskMetric
text="On-time completion rate"
number={`${metrics?.onTimeCompletionRate}%`}
isLoading={isLoading}
/>
</TaskMetricsContainer>
);
};
6 changes: 6 additions & 0 deletions packages/datatrak-web/src/components/TaskMetrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

export { TaskMetrics } from './TaskMetrics';
1 change: 1 addition & 0 deletions packages/datatrak-web/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { TextInput } from './TextInput';
export { Tile, LoadingTile } from './Tile';
export { Toast } from './Toast';
export { TopProgressBar } from './TopProgressBar';
export { TaskMetrics } from './TaskMetrics';
6 changes: 3 additions & 3 deletions packages/datatrak-web/src/features/Tasks/TaskPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ const BackButton = styled(Button)`
const Wrapper = styled.div`
padding-block: 0.7rem;
display: flex;
align-items: center;
align-items: self-start;
padding-inline-end: 2.7rem;
${({ theme }) => theme.breakpoints.down('xs')} {
flex-direction: column;
align-items: flex-start;
Expand Down Expand Up @@ -63,7 +62,8 @@ const ContentWrapper = styled.div`
justify-content: flex-end;
width: 100%;
${({ theme }) => theme.breakpoints.down('xs')} {
padding-inline-start: 1.5rem;
padding-inline-start: 1rem;
flex-direction: column;
padding-inline-end: 0.6rem;
}
`;
Expand Down
6 changes: 6 additions & 0 deletions packages/datatrak-web/src/views/Tasks/TasksDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import { Add } from '@material-ui/icons';
import { Button } from '../../components';
import { CreateTaskModal, TaskPageHeader, TasksTable } from '../../features';
import { TasksContentWrapper } from '../../layout';
import { TaskMetrics } from '../../components/TaskMetrics';

const ButtonContainer = styled.div`
padding-block-end: 0.5rem;
margin-block-start: 1rem;
${({ theme }) => theme.breakpoints.up('sm')} {
margin-inline-start: auto;
margin-block-start: 0;
padding-block-end: 0;
}
${({ theme }) => theme.breakpoints.down('xs')} {
align-self: self-end;
}
`;

Expand Down Expand Up @@ -44,6 +49,7 @@ export const TasksDashboardPage = () => {
return (
<>
<TaskPageHeader title="Tasks" backTo="/">
<TaskMetrics />
<ButtonContainer>
<CreateButton onClick={toggleCreateModal}>
<AddIcon /> Create task
Expand Down
6 changes: 5 additions & 1 deletion packages/server-boilerplate/src/models/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { TaskRecord as BaseTaskRecord, TaskModel as BaseTaskModel } from '@tupai
import { Task } from '@tupaia/types';
import { Model } from './types';

export interface TaskRecord extends Task, BaseTaskRecord {}
export interface TaskRecord extends Task, BaseTaskRecord {
project_id?: string | null;
data_time?: Date | null;
timezone?: string | null;
}

export interface TaskModel extends Model<BaseTaskModel, Task, TaskRecord> {}
1 change: 1 addition & 0 deletions packages/server-boilerplate/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type QueryOptions = {
sort?: string[];
rawSort?: string;
joinWith?: string;
columns?: string[];
joinCondition?: [string, string];
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Params = Record<string, never>;
export interface ResBody {
unassignedTasks: number;
overdueTasks: number;
onTimeCompletionRate: number;
}
export type ReqBody = Record<string, never>;
export type ReqQuery = Record<string, never>;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * as DatatrakWebLeaderboardRequest from './LeaderboardRequest';
export * as DatatrakWebActivityFeedRequest from './ActivityFeedRequest';
export * as DatatrakWebGenerateLoginTokenRequest from './GenerateLoginTokenRequest';
export * as DatatrakWebEntityDescendantsRequest from './EntityDescendantsRequest';
export * as DatatrakWebTaskMetricsRequest from './TaskMetricsRequest';
export * as DatatrakWebTasksRequest from './TasksRequest';
export * as DatatrakWebTaskRequest from './TaskRequest';
export * as DatatrakWebUsersRequest from './UsersRequest';
Expand Down
Loading

0 comments on commit 12a9021

Please sign in to comment.