diff --git a/client/src/adapters/project.ts b/client/src/adapters/project.ts index f20e849f4bf020c72537f36e75e984134a738e59..84beae4b2f84e3d6194e6046dd530596b0071acf 100644 --- a/client/src/adapters/project.ts +++ b/client/src/adapters/project.ts @@ -3,6 +3,7 @@ import { executeApiGet, executeApiPost, executeApiPut } from './util'; import { Task } from './task'; import { AssignedUser } from './user'; import { Work } from './work'; +import { Activity, Completion } from './util'; export interface Project { id: string; @@ -50,6 +51,20 @@ export function getProjectWork(uuid: string): Promise<Work[]> { })), "Failed to get project work"); } +export function getProjectActivity(uuid: string, from: Date = new Date(0), to: Date = new Date()): Promise<Activity[]> { + return executeApiGet( + `project/${uuid}/activity?since=${from.getTime()}&to=${to.getTime()}`, + ({ activity }) => activity, "Failed to get project activity" + ); +} + +export function getProjectCompletion(uuid: string, from: Date = new Date(0), to: Date = new Date()): Promise<Completion> { + return executeApiGet( + `project/${uuid}/completion?since=${from.getTime()}&to=${to.getTime()}`, + ({ completion }) => completion, "Failed to get project completion" + ); +} + interface NewTeamData { teams: Array<string>; name: string; diff --git a/client/src/adapters/team.ts b/client/src/adapters/team.ts index 257b2573b5e385a2ef3c86bb233e922917d25a11..d9b4c609eb1ecaea5e7c9f8ed8c59dd7c2e2ed47 100644 --- a/client/src/adapters/team.ts +++ b/client/src/adapters/team.ts @@ -3,6 +3,7 @@ import { executeApiDelete, executeApiGet, executeApiPost, executeApiPut } from ' import { User } from './user'; import { ReducedProject } from './project'; import { Work } from './work'; +import { Activity, Completion } from './util'; export interface Team { id: string; @@ -50,6 +51,20 @@ export function getTeamWork(uuid: string): Promise<Work[]> { })), "Failed to get team work"); } +export function getTeamActivity(uuid: string, from: Date = new Date(0), to: Date = new Date()): Promise<Activity[]> { + return executeApiGet( + `team/${uuid}/activity?since=${from.getTime()}&to=${to.getTime()}`, + ({ activity }) => activity, "Failed to get team activity" + ); +} + +export function getTeamCompletion(uuid: string, from: Date = new Date(0), to: Date = new Date()): Promise<Completion> { + return executeApiGet( + `team/${uuid}/completion?since=${from.getTime()}&to=${to.getTime()}`, + ({ completion }) => completion, "Failed to get team completion" + ); +} + export function createTeam(name: string): Promise<string> { return executeApiPost(`team`, { name: name }, ({ id }) => id, "Failed to create team"); } diff --git a/client/src/adapters/user.ts b/client/src/adapters/user.ts index 8f02b26fef56d8a6bf1f34b69536f29b97a2f7dc..4b61bbf211e5f6da6be661ef5de0c13a2154e9d6 100644 --- a/client/src/adapters/user.ts +++ b/client/src/adapters/user.ts @@ -4,6 +4,7 @@ import { apiRoot } from 'config'; import { executeApiGet, executeApiPut } from './util'; import { Task } from './task'; import { Work } from './work'; +import { Activity, Completion } from './util'; export interface User { id: string; @@ -46,6 +47,20 @@ export function getUserWork(): Promise<Work> { })), "Failed to get user work"); } +export function getUserActivity(from: Date = new Date(0), to: Date = new Date()): Promise<Activity[]> { + return executeApiGet( + `user/activity?since=${from.getTime()}&to=${to.getTime()}`, + ({ activity }) => activity, "Failed to get user activity" + ); +} + +export function getUserCompletion(from: Date = new Date(0), to: Date = new Date()): Promise<Completion> { + return executeApiGet( + `user/completion?since=${from.getTime()}&to=${to.getTime()}`, + ({ completion }) => completion, "Failed to get user completion" + ); +} + export function getUser(uuid: string): Promise<User> { return executeApiGet(`user/${uuid}`, ({ user }) => user, "Failed to get user"); } diff --git a/client/src/adapters/util.ts b/client/src/adapters/util.ts index 08a6b61442e3f8ad4da0a73079f4ef465f98b1dd..db9ea4afb1f9c3b7e39a3847782c6c7bc08cf4ea 100644 --- a/client/src/adapters/util.ts +++ b/client/src/adapters/util.ts @@ -3,6 +3,18 @@ import { apiRoot } from 'config'; import { getAuthHeader } from './auth'; +export interface Activity { + day: string; + time: number; +} + +export interface Completion { + open: number, + closed: number, + suspended: number, + overdue: number, +} + async function executeApiRequest<T>(path: string, method: string, body: any, onSuccess: (data: any) => T, errorMessage: string): Promise<T> { try { const response = await fetch(`${apiRoot}/${path}`, { diff --git a/server/src/v1/project.ts b/server/src/v1/project.ts index 0e25838d6a308b250bc5e41759fe0dcd5a1729ae..d987bfb16b4d0f13e6f9831723a0ecb39710471e 100644 --- a/server/src/v1/project.ts +++ b/server/src/v1/project.ts @@ -223,6 +223,107 @@ project.get('/:uuid/work', async (req, res) => { } }); +project.get('/:uuid/activity', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const activity = await database( + database('team_members') + .innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .innerJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + started: 'workhours.started', + finished: 'workhours.finished', + }) + .where({ + 'team_members.user_id': req.body.token.id, + 'team_projects.project_id': id, + }) + .andWhereNot({ 'workhours.finished': null }) + .andWhere('workhours.started', '>=', since) + .andWhere('workhours.started', '<=', to) + .groupBy('workhours.id') + ) + .select({ + day: database.raw('Date(`started` / 1000, \'unixepoch\')'), + }) + .sum({ time: database.raw('`finished` - `started`') }) + .groupBy('day'); + res.status(200).json({ + status: 'success', + activity: activity, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + console.error(e); + res.status(400).json({ + status: 'error', + message: 'failed get activity', + }); + } +}); + +project.get('/:uuid/completion', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const completion = await database( + database('team_members') + .innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .leftJoin('task_requirements', 'tasks.id', 'task_requirements.task_id') + .leftJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + id: 'tasks.id', + status: database.raw( + 'Case When `tasks`.`status` = \'open\' ' + + 'And Sum(`task_requirements`.`time` * 60 * 1000) < Sum(`workhours`.`finished` - `workhours`.`started`) ' + + 'Then \'overdue\' Else `tasks`.`status` End'), + }) + .where({ + 'team_members.user_id': req.body.token.id, + 'team_projects.project_id': id, + }) + .andWhere('tasks.edited', '>=', since) + .andWhere('tasks.created', '<=', to) + .groupBy('tasks.id') + ) + .select({ + status: 'status', + }) + .count({ count: 'id' }) + .groupBy('status') as any[]; + res.status(200).json({ + status: 'success', + completion: completion.reduce((object, { status, count }) => ({ + ...object, + [status]: count, + }), { open: 0, closed: 0, suspended: 0, overdue: 0 }), + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get completion', + }); + } +}); + interface AddProjectBody { teams: Array<string>; name: string; diff --git a/server/src/v1/team.ts b/server/src/v1/team.ts index 7df69d29a5eb14e45cda33ec1df9c9fe0fb6d482..4c229de0a400ad821287cf10257aae7a10a6851b 100644 --- a/server/src/v1/team.ts +++ b/server/src/v1/team.ts @@ -349,6 +349,98 @@ team.get('/:uuid/work', async (req, res) => { } }); +team.get('/:uuid/activity', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const activity = await database({ ut: 'team_members' }) + .innerJoin('team_members', 'ut.team_id', 'team_members.team_id') + .innerJoin('workhours', 'team_members.user_id', 'workhours.user_id') + .select({ + day: database.raw('Date(`workhours`.`started` / 1000, \'unixepoch\')'), + }) + .sum({ time: database.raw('`workhours`.`finished` - `workhours`.`started`') }) + .where({ + 'ut.user_id': req.body.token.id, + 'ut.team_id': id, + }) + .andWhereNot({ 'workhours.finished': null }) + .andWhere('workhours.started', '>=', since) + .andWhere('workhours.started', '<=', to) + .groupBy('day'); + res.status(200).json({ + status: 'success', + activity: activity, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get activity', + }); + } +}); + +team.get('/:uuid/completion', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const completion = await database( + database('team_members') + .innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .leftJoin('task_requirements', 'tasks.id', 'task_requirements.task_id') + .leftJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + id: 'tasks.id', + status: database.raw( + 'Case When `tasks`.`status` = \'open\' ' + + 'And Sum(`task_requirements`.`time` * 60 * 1000) < Sum(`workhours`.`finished` - `workhours`.`started`) ' + + 'Then \'overdue\' Else `tasks`.`status` End'), + }) + .where({ + 'team_members.user_id': req.body.token.id, + 'team_members.team_id': id, + }) + .andWhere('tasks.edited', '>=', since) + .andWhere('tasks.created', '<=', to) + .groupBy('tasks.id') + ) + .select({ + status: 'status', + }) + .count({ count: 'id' }) + .groupBy('status') as any[]; + res.status(200).json({ + status: 'success', + completion: completion.reduce((object, { status, count }) => ({ + ...object, + [status]: count, + }), { open: 0, closed: 0, suspended: 0, overdue: 0 }), + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get completion', + }); + } +}); + interface AddRoleBody { name: string; token: Token; diff --git a/server/src/v1/user.ts b/server/src/v1/user.ts index e670ca667e1562fd2f9fc5fef0fdb98bb81ed3b8..8d1db7eec2e0114ca7006d0762814a42de048fcc 100644 --- a/server/src/v1/user.ts +++ b/server/src/v1/user.ts @@ -185,6 +185,78 @@ user.get('/work', async (req, res) => { } }); +user.get('/activity', async (req, res) => { + try { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const activity = await database('workhours') + .select({ + day: database.raw('Date(`started` / 1000, \'unixepoch\')'), + }) + .sum({ time: database.raw('`finished` - `started`') }) + .where({ + 'workhours.user_id': req.body.token.id, + }) + .andWhereNot({ 'workhours.finished': null }) + .andWhere('workhours.started', '>=', since) + .andWhere('workhours.started', '<=', to) + .groupBy('day'); + res.status(200).json({ + status: 'success', + activity: activity, + }); + } catch (e) { + console.error(e); + res.status(400).json({ + status: 'error', + message: 'failed get activity', + }); + } +}); + +user.get('/completion', async (req, res) => { + try { + const since = (req.query.since ?? 0) as number; + const to = (req.query.to ?? Date.now()) as number; + const completion = await database( + database('task_assignees') + .leftJoin('tasks', 'task_assignees.task_id', 'tasks.id') + .leftJoin('task_requirements', 'tasks.id', 'task_requirements.task_id') + .leftJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + id: 'tasks.id', + status: database.raw( + 'Case When `tasks`.`status` = \'open\' ' + + 'And Sum(`task_requirements`.`time` * 60 * 1000) < Sum(`workhours`.`finished` - `workhours`.`started`) ' + + 'Then \'overdue\' Else `tasks`.`status` End'), + }) + .where({ + 'task_assignees.user_id': req.body.token.id, + }) + .andWhere('tasks.edited', '>=', since) + .andWhere('tasks.created', '<=', to) + .groupBy('tasks.id') + ) + .select({ + status: 'status', + }) + .count({ count: 'id' }) + .groupBy('status') as any[]; + res.status(200).json({ + status: 'success', + completion: completion.reduce((object, { status, count }) => ({ + ...object, + [status]: count, + }), { open: 0, closed: 0, suspended: 0, overdue: 0 }), + }); + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get completion', + }); + } +}); + interface UserUpdateBody { token: Token; realname?: string;