diff --git a/server/migrations/0000_initial.ts b/server/migrations/0000_initial.ts index 583107b61c019fdb4ce60bb4f1ff7c917a267f03..5bbfc84585e6fe110969d91daf1733e0d036aeef 100644 --- a/server/migrations/0000_initial.ts +++ b/server/migrations/0000_initial.ts @@ -29,6 +29,7 @@ export async function up(database: Knex): Promise<void> { .createTable('projects', table => { table.uuid('id').notNullable().primary(); table.text('name').notNullable(); + table.text('text').notNullable(); table.string('color').notNullable(); table.enum('status', [ 'open', 'closed', 'suspended' ]).notNullable(); }) diff --git a/server/src/v1/project.ts b/server/src/v1/project.ts index 3766830e92f1f8b30ebd1040ae53c26b84cb68d2..370eef80ba61f7c44fdf6b95349fee1417e5971f 100644 --- a/server/src/v1/project.ts +++ b/server/src/v1/project.ts @@ -48,6 +48,8 @@ project.get('/:uuid', async (req, res) => { .select({ id: 'projects.id', name: 'projects.name', + text: 'projects.text', + color: 'projects.color', status: 'projects.status', team_id: 'tms.team_id', }) @@ -134,15 +136,96 @@ project.get('/:uuid/tasks', async (req, res) => { } }); +project.get('/:uuid/assigned', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const users = await database('team_members') + .innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .innerJoin('task_assignees', 'tasks.id', 'task_assignees.task_id') + .innerJoin('users', 'task_assignees.user_id', 'users.id') + .select({ + id: 'users.id', + username: 'users.user_name', + email: 'users.email', + realname: 'users.real_name', + }) + .sum({ time: 'task_assignees.time' }) + .where({ + 'team_members.user_id': req.body.token.id, + 'team_projects.project_id': id, + }) + .groupBy('users.id'); + res.status(200).json({ + status: 'success', + tasks: users, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + console.log(e); + res.status(400).json({ + status: 'error', + message: 'failed to get assignees', + }); + } +}); + +project.get('/:uuid/work', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const work = await database({ ut: 'team_members' }) + .innerJoin('team_projects', 'ut.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .innerJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + id: 'workhours.id', + task: 'workhours.task_id', + user: 'workhours.user_id', + started: 'workhours.started', + finished: 'workhours.finished', + }) + .where({ + 'ut.user_id': req.body.token.id, + 'team_projects.project_id': id, + }) + .andWhere('workhours.started', '>=', since) + .groupBy('workhours.id'); + res.status(200).json({ + status: 'success', + work: work, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get work', + }); + } +}); + interface AddProjectBody { teams: Array<string>; name: string; + text: string; color: string; token: Token; } project.post('/', async (req, res) => { - if (isOfType<AddProjectBody>(req.body, [['teams', 'object'], ['name', 'string'], ['color', 'string']])) { + if (isOfType<AddProjectBody>(req.body, [['teams', 'object'], ['name', 'string'], ['text', 'string'], ['color', 'string']])) { try { const team_ids = req.body.teams; for (const team_id of team_ids) { @@ -173,6 +256,8 @@ project.post('/', async (req, res) => { await transaction('projects').insert({ id: project_id, name: req.body.name, + text: req.body.text, + color: req.body.color, status: 'open', }); await transaction('team_projects').insert( @@ -211,6 +296,7 @@ interface UpdateProjectBody { remove_teams?: Array<string>; add_teams?: Array<string>; name?: string; + text?: string; color?: string; status?: string; token: Token; @@ -245,6 +331,7 @@ project.put('/:uuid', async (req, res) => { await transaction('projects') .update({ name: req.body.name, + text: req.body.text, color: req.body.color, status: req.body.status, }).where({ diff --git a/server/src/v1/task.ts b/server/src/v1/task.ts index 0779a282a722cdec2a5f65d11870e30974edc041..89d3f948cafb5577b08a48b05f0b31109c1f433b 100644 --- a/server/src/v1/task.ts +++ b/server/src/v1/task.ts @@ -257,6 +257,46 @@ task.get('/:uuid/comments', async (req, res) => { } }); +task.get('/:uuid/work', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const work = await database({ ut: 'team_members' }) + .innerJoin('team_projects', 'ut.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .innerJoin('workhours', 'tasks.id', 'workhours.task_id') + .select({ + id: 'workhours.id', + task: 'workhours.task_id', + user: 'workhours.user_id', + started: 'workhours.started', + finished: 'workhours.finished', + }) + .where({ + 'ut.user_id': req.body.token.id, + 'tasks.id': id, + }) + .andWhere('workhours.started', '>=', since) + .groupBy('workhours.id'); + res.status(200).json({ + status: 'success', + work: work, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get work', + }); + } +}); + task.get('/:uuid', async (req, res) => { try { const id = req.params.uuid; diff --git a/server/src/v1/team.ts b/server/src/v1/team.ts index c97fe686942ef44d54841e7bfe3f290e5c71c897..4fa91366611ebca56c96bfaf5b38618ba96cd06a 100644 --- a/server/src/v1/team.ts +++ b/server/src/v1/team.ts @@ -260,6 +260,45 @@ team.get('/:uuid/projects', async (req, res) => { } }); +team.get('/:uuid/work', async (req, res) => { + try { + const id = req.params.uuid; + if (validate(id)) { + const since = (req.query.since ?? 0) as number; + const work = 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({ + id: 'workhours.id', + task: 'workhours.task_id', + user: 'workhours.user_id', + started: 'workhours.started', + finished: 'workhours.finished', + }) + .where({ + 'ut.user_id': req.body.token.id, + 'ut.team_id': id, + }) + .andWhere('workhours.started', '>=', since) + .groupBy('workhours.id'); + res.status(200).json({ + status: 'success', + work: work, + }); + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get work', + }); + } +}); + interface AddRoleBody { name: string; token: Token; diff --git a/server/src/v1/user.ts b/server/src/v1/user.ts index 7fb8c66f921945608df0c35d29c1d31f4c8ad298..4136116ed5cf1253a6b1169ba42ae362a6b9472c 100644 --- a/server/src/v1/user.ts +++ b/server/src/v1/user.ts @@ -108,6 +108,34 @@ user.get('/tasks', async (req, res) => { } }); +user.get('/work', async (req, res) => { + try { + const since = (req.query.since ?? 0) as number; + const work = await database('workhours') + .select({ + id: 'workhours.id', + task: 'workhours.task_id', + user: 'workhours.user_id', + started: 'workhours.started', + finished: 'workhours.finished', + }) + .where({ + 'workhours.user_id': req.body.token.id, + }) + .andWhere('workhours.started', '>=', since) + .groupBy('workhours.id'); + res.status(200).json({ + status: 'success', + work: work, + }); + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed get work', + }); + } +}); + interface UserUpdateBody { token: Token; realname?: string; diff --git a/server/src/v1/work.ts b/server/src/v1/work.ts index 9868e7c2b4dd823f81d8c93b2d785a33540d2f2b..e12e49047dca30a4660b5b50ea94ed1a1dc92dcb 100644 --- a/server/src/v1/work.ts +++ b/server/src/v1/work.ts @@ -1,13 +1,110 @@ import express from 'express'; +import { v4 as uuid, validate } from 'uuid'; -import { requireVerification } from './auth'; +import database from '../database'; +import { requireVerification, Token } from './auth'; +import { isOfType } from '../util'; const work = express(); work.use(requireVerification); +interface AddWorkBody { + task: string; + token: Token; +} +work.post('/start', async (req, res) => { + if (isOfType<AddWorkBody>(req.body, [['task', 'string']])) { + try { + const task_id = req.body.task; + if (validate(task_id)) { + const task = await database('team_members') + .innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id') + .innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id') + .select({ id: 'tasks.id' }) + .where({ + 'team_members.user_id': req.body.token.id, + 'tasks.id': task_id, + }); + if (task.length >= 1) { + const work_id = uuid(); + await database.transaction(async transaction => { + await transaction('workhours') + .update({ + finished: new Date(), + }) + .where({ + user_id: req.body.token.id, + finished: null, + }); + await transaction('workhours') + .insert({ + id: work_id, + task_id: task_id, + user_id: req.body.token.id, + started: new Date(), + finished: null, + }); + }); + res.status(200).json({ + status: 'success', + id: work_id, + }); + } else { + res.status(404).json({ + status: 'error', + message: 'task not found', + }); + } + } else { + res.status(400).json({ + status: 'error', + message: 'malformed uuid', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed to create work', + }); + } + } else { + res.status(400).json({ + status: 'error', + message: 'missing request fields', + }); + } +}); + +work.put('/finish', async (req, res) => { + try { + const work = await database('workhours') + .update({ + finished: new Date(), + }) + .where({ + user_id: req.body.token.id, + finished: null, + }); + if (work >= 1) { + res.status(200).json({ + status: 'success', + }); + } else { + res.status(200).json({ + status: 'error', + message: 'no work to finish', + }); + } + } catch (e) { + res.status(400).json({ + status: 'error', + message: 'failed to finish work', + }); + } +}); export default work;