From 5215767eca3ea90df26d2cb924d2025bc05f4cd2 Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 1 May 2024 15:38:58 -0400 Subject: [PATCH] manager views --- app/controllers/home.js | 7 ++- app/controllers/task.js | 8 +-- app/services/client.js | 51 +++++++++++++++++-- .../admin/user/components/list-table.pug | 2 +- app/views/client/editor.pug | 2 + app/views/client/project/editor.pug | 13 +++++ app/views/home.pug | 45 +++++++++++----- app/views/task/view.pug | 35 +++++++------ 8 files changed, 123 insertions(+), 40 deletions(-) diff --git a/app/controllers/home.js b/app/controllers/home.js index ac8b1e5..736900a 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -37,12 +37,15 @@ export default class HomeController extends SiteController { res.locals.currentView = 'home'; res.locals.pageDescription = 'DTP Time Tracker'; - res.locals.clients = await clientService.getClientsForUser(req.user); res.locals.projects = await clientService.getProjectsForUser(req.user); - res.locals.taskGrid = await taskService.getTaskGridForUser(req.user); res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(req.user); + res.locals.managedProjects = await clientService.getProjectsForManager(req.user); + for (const project of res.locals.managedProjects) { + project.taskGrid = await taskService.getTaskGridForProject(project); + } + res.render('home'); } catch (error) { this.log.error('failed to present the home view', { error }); diff --git a/app/controllers/task.js b/app/controllers/task.js index abd030c..a3e0860 100644 --- a/app/controllers/task.js +++ b/app/controllers/task.js @@ -44,14 +44,16 @@ export default class TaskController extends SiteController { }); async function checkTaskOwnership (req, res, next) { - if (!res.locals.task.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your task'); + res.locals.manager = res.locals.task.project.managers.find((manager) => manager._id.equals(req.user._id)); + if (!res.locals.manager && !res.locals.task.user._id.equals(req.user._id)) { + return next(new SiteError(401, 'This is not your task')); } return next(); } async function checkSessionOwnership (req, res, next) { - if (!res.locals.session.user._id.equals(req.user._id)) { + res.locals.manager = res.locals.task.project.managers.find((manager) => manager._id.equals(req.user._id)); + if (!res.locals.manager && !res.locals.session.user._id.equals(req.user._id)) { throw new SiteError(401, 'This is not your session'); } return next(); diff --git a/app/services/client.js b/app/services/client.js index 18c688f..05cd485 100644 --- a/app/services/client.js +++ b/app/services/client.js @@ -8,7 +8,7 @@ import mongoose from 'mongoose'; const Client = mongoose.model('Client'); const ClientProject = mongoose.model('ClientProject'); -import { SiteService/*, SiteError*/ } from '../../lib/site-lib.js'; +import { SiteService, SiteError } from '../../lib/site-lib.js'; export default class ClientService extends SiteService { @@ -38,6 +38,10 @@ export default class ClientService extends SiteService { path: 'client', populate: this.populateClient, }, + { + path: 'managers', + select: userService.USER_SELECT, + }, ]; } @@ -119,6 +123,10 @@ export default class ClientService extends SiteService { project.description = textService.filter(projectDefinition.description); } + if (projectDefinition.managers && (projectDefinition.managers.length > 0)) { + project.managers = await this.parseManagerNames(projectDefinition.managers); + } + project.hourlyRate = projectDefinition.hourlyRate; await project.save(); @@ -135,11 +143,19 @@ export default class ClientService extends SiteService { update.$set.name = textService.filter(projectDefinition.name); changed = true; } + if (projectDefinition.description !== project.description) { update.$set.description = textService.filter(projectDefinition.description); changed = true; } + if (projectDefinition.managers && (projectDefinition.managers.length > 0)) { + update.$set.managers = await this.parseManagerNames(projectDefinition.managers); + changed = true; + } else { + update.$unset.managers = 1; + } + if (projectDefinition.hourlyRate !== project.hourlyRate) { update.$set.hourlyRate = projectDefinition.hourlyRate; changed = true; @@ -152,6 +168,22 @@ export default class ClientService extends SiteService { await ClientProject.updateOne({ _id: project._id }, update); } + async parseManagerNames (managerNames) { + const { user: userService } = this.dtp.services; + const managers = [ ]; + + managerNames = managerNames.split(',').map((m) => m.trim().toLowerCase()); + for (const name of managerNames) { + const user = await userService.getByUsername(name); + if (!user) { + throw new SiteError(400, `Trying to set unknown user "${name}" as project manager`); + } + managers.push(user._id); + } + + return managers; + } + async getProjectById (projectId) { const project = ClientProject .findOne({ _id: projectId }) @@ -161,18 +193,27 @@ export default class ClientService extends SiteService { } async getProjectsForUser (user) { - const projects = ClientProject + const projects = await ClientProject .find({ user: user._id }) - .sort({ created: -1 }) + .sort({ name: 1 }) .populate(this.populateClientProject) .lean(); return projects; } async getProjectsForClient (client) { - const projects = ClientProject + const projects = await ClientProject .find({ client: client._id }) - .sort({ created: -1 }) + .sort({ name: 1 }) + .populate(this.populateClientProject) + .lean(); + return projects; + } + + async getProjectsForManager (manager) { + const projects = await ClientProject + .find({ managers: manager._id }) + .sort({ name: 1 }) .populate(this.populateClientProject) .lean(); return projects; diff --git a/app/views/admin/user/components/list-table.pug b/app/views/admin/user/components/list-table.pug index 75d4dc2..d2c8efa 100644 --- a/app/views/admin/user/components/list-table.pug +++ b/app/views/admin/user/components/list-table.pug @@ -1,6 +1,6 @@ mixin renderAdminUserTable (users) .uk-overflow-auto - table.uk-table.uk-table-small.uk-table-justify + table.uk-table.uk-table-small thead tr th Username diff --git a/app/views/client/editor.pug b/app/views/client/editor.pug index af02e0c..d782ee6 100644 --- a/app/views/client/editor.pug +++ b/app/views/client/editor.pug @@ -21,6 +21,7 @@ block view-content value= !!client ? client.name : undefined, placeholder="Enter client name", ).uk-input + .uk-margin label(for="description").uk-form-label Client description textarea( @@ -29,6 +30,7 @@ block view-content rows=4, placeholder="Enter client description", ).uk-textarea.uk-resize-vertical= !!client ? client.description : undefined + .uk-margin label(for="hours-limit").uk-form-label Max. hours per week input( diff --git a/app/views/client/project/editor.pug b/app/views/client/project/editor.pug index 9fe99fc..0320246 100644 --- a/app/views/client/project/editor.pug +++ b/app/views/client/project/editor.pug @@ -25,6 +25,7 @@ block view-content value= !!project ? project.name : undefined, placeholder="Enter project name", ).uk-input + .uk-margin label(for="description").uk-form-label Project description textarea( @@ -33,6 +34,18 @@ block view-content rows=4, placeholder="Enter project description" ).uk-textarea.uk-resize-vertical= !!project ? project.description : "" + + .uk-margin + label(for="managers").uk-form-label Managers + input( + id="managers", + name="managers", + type="text", + value= !!project ? (project.managers || [ ]).map((m) => m.username).join(',') : undefined, + placeholder="Enter comma-separated list of usernames", + ).uk-input + .uk-text-small.uk-text-muted Managers can review work sessions and access billing information. + .uk-margin label(for="hourly-rate").uk-form-label Hourly rate input( diff --git a/app/views/home.pug b/app/views/home.pug index ca69af4..d099865 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -4,17 +4,36 @@ block view-content include task/components/grid include report/components/weekly-summary - section.uk-section.uk-section-muted.uk-section-small - .uk-container - if weeklyEarnings.length > 0 - +renderWeeklySummaryReport(weeklyEarnings) - else - div No time worked this week. + if Array.isArray(projects) && (projects.length > 0) + section.uk-section.uk-section-muted.uk-section-small + .uk-container + h1 Your Projects + if weeklyEarnings.length > 0 + +renderWeeklySummaryReport(weeklyEarnings) + else + div No time worked this week. - section.uk-section.uk-section-default.uk-section-small - .uk-container - +renderTaskGrid( - taskGrid.pendingTasks, - taskGrid.activeTasks, - taskGrid.finishedTasks, - ) \ No newline at end of file + section.uk-section.uk-section-default.uk-section-small + .uk-container + +renderTaskGrid( + taskGrid.pendingTasks, + taskGrid.activeTasks, + taskGrid.finishedTasks, + ) + + if Array.isArray(managedProjects) && (managedProjects.length > 0) + section.uk-section.uk-section-muted.uk-section-small + .uk-container + h1 Projects You Manage + + section.uk-section.uk-section-default.uk-section-small + .uk-container + ul.uk-list.uk-list-divider + each project in managedProjects + li + h2= project.name + +renderTaskGrid( + project.taskGrid.pendingTasks, + project.taskGrid.activeTasks, + project.taskGrid.finishedTasks, + ) \ No newline at end of file diff --git a/app/views/task/view.pug b/app/views/task/view.pug index 5d6d785..e520290 100644 --- a/app/views/task/view.pug +++ b/app/views/task/view.pug @@ -15,19 +15,22 @@ block view-content +renderProfilePicture(user) .uk-width-expand div(style="line-height: 1;").uk-text-lead.uk-text-truncated.uk-margin-small= task.note - case task.status - when 'pending' - .uk-margin-small - form(method="POST", action=`/task/${task._id}/start`).uk-form - button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Start Task - when 'active' - .uk-margin-small - form(method="POST", action=`/task/${task._id}/close`).uk-form - button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Finish Task - when 'finished' - .uk-text-success Finished task + if !manager + case task.status + when 'pending' + .uk-margin-small + form(method="POST", action=`/task/${task._id}/start`).uk-form + button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Start Task + when 'active' + .uk-margin-small + form(method="POST", action=`/task/${task._id}/close`).uk-form + button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Finish Task + when 'finished' + .uk-text-success Finished task + else + div Task status: #{task.status} - if task.status === 'active' + if task.status === 'active' && !manager .uk-width-auto.uk-text-right .pretty.p-switch.p-slim input( @@ -81,15 +84,15 @@ block view-content else div No work sessions - if task.status === 'active' - div(class="uk-width-1-1 uk-width-large@m") + if !manager && (task.status === 'active') + div(class="uk-width-1-1 uk-flex-first uk-width-large@m uk-flex-last@m") .uk-margin video( id="capture-preview", poster="/img/default-poster.svg", playsinline, muted, - ).dtp-video - .uk-margin.uk-text-small.uk-text-muted + ).dtp-video.no-select + div(class="uk-visible@m").uk-margin.uk-text-small.uk-text-muted p One image will be captured from this live preview every 10 minutes. It will be uploaded and stored in the work session with a timestamp. p When you start a work session, you will select the screen, application, or browser tab to share.