Browse Source

manager views

develop
Rob Colbert 12 months ago
parent
commit
5215767eca
  1. 7
      app/controllers/home.js
  2. 8
      app/controllers/task.js
  3. 51
      app/services/client.js
  4. 2
      app/views/admin/user/components/list-table.pug
  5. 2
      app/views/client/editor.pug
  6. 13
      app/views/client/project/editor.pug
  7. 19
      app/views/home.pug
  8. 13
      app/views/task/view.pug

7
app/controllers/home.js

@ -37,12 +37,15 @@ export default class HomeController extends SiteController {
res.locals.currentView = 'home'; res.locals.currentView = 'home';
res.locals.pageDescription = 'DTP Time Tracker'; res.locals.pageDescription = 'DTP Time Tracker';
res.locals.clients = await clientService.getClientsForUser(req.user);
res.locals.projects = await clientService.getProjectsForUser(req.user); res.locals.projects = await clientService.getProjectsForUser(req.user);
res.locals.taskGrid = await taskService.getTaskGridForUser(req.user); res.locals.taskGrid = await taskService.getTaskGridForUser(req.user);
res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(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'); res.render('home');
} catch (error) { } catch (error) {
this.log.error('failed to present the home view', { error }); this.log.error('failed to present the home view', { error });

8
app/controllers/task.js

@ -44,14 +44,16 @@ export default class TaskController extends SiteController {
}); });
async function checkTaskOwnership (req, res, next) { async function checkTaskOwnership (req, res, next) {
if (!res.locals.task.user._id.equals(req.user._id)) { res.locals.manager = res.locals.task.project.managers.find((manager) => manager._id.equals(req.user._id));
throw new SiteError(401, 'This is not your task'); 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(); return next();
} }
async function checkSessionOwnership (req, res, 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'); throw new SiteError(401, 'This is not your session');
} }
return next(); return next();

51
app/services/client.js

@ -8,7 +8,7 @@ import mongoose from 'mongoose';
const Client = mongoose.model('Client'); const Client = mongoose.model('Client');
const ClientProject = mongoose.model('ClientProject'); 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 { export default class ClientService extends SiteService {
@ -38,6 +38,10 @@ export default class ClientService extends SiteService {
path: 'client', path: 'client',
populate: this.populateClient, 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); project.description = textService.filter(projectDefinition.description);
} }
if (projectDefinition.managers && (projectDefinition.managers.length > 0)) {
project.managers = await this.parseManagerNames(projectDefinition.managers);
}
project.hourlyRate = projectDefinition.hourlyRate; project.hourlyRate = projectDefinition.hourlyRate;
await project.save(); await project.save();
@ -135,11 +143,19 @@ export default class ClientService extends SiteService {
update.$set.name = textService.filter(projectDefinition.name); update.$set.name = textService.filter(projectDefinition.name);
changed = true; changed = true;
} }
if (projectDefinition.description !== project.description) { if (projectDefinition.description !== project.description) {
update.$set.description = textService.filter(projectDefinition.description); update.$set.description = textService.filter(projectDefinition.description);
changed = true; 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) { if (projectDefinition.hourlyRate !== project.hourlyRate) {
update.$set.hourlyRate = projectDefinition.hourlyRate; update.$set.hourlyRate = projectDefinition.hourlyRate;
changed = true; changed = true;
@ -152,6 +168,22 @@ export default class ClientService extends SiteService {
await ClientProject.updateOne({ _id: project._id }, update); 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) { async getProjectById (projectId) {
const project = ClientProject const project = ClientProject
.findOne({ _id: projectId }) .findOne({ _id: projectId })
@ -161,18 +193,27 @@ export default class ClientService extends SiteService {
} }
async getProjectsForUser (user) { async getProjectsForUser (user) {
const projects = ClientProject const projects = await ClientProject
.find({ user: user._id }) .find({ user: user._id })
.sort({ created: -1 }) .sort({ name: 1 })
.populate(this.populateClientProject) .populate(this.populateClientProject)
.lean(); .lean();
return projects; return projects;
} }
async getProjectsForClient (client) { async getProjectsForClient (client) {
const projects = ClientProject const projects = await ClientProject
.find({ client: client._id }) .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) .populate(this.populateClientProject)
.lean(); .lean();
return projects; return projects;

2
app/views/admin/user/components/list-table.pug

@ -1,6 +1,6 @@
mixin renderAdminUserTable (users) mixin renderAdminUserTable (users)
.uk-overflow-auto .uk-overflow-auto
table.uk-table.uk-table-small.uk-table-justify table.uk-table.uk-table-small
thead thead
tr tr
th Username th Username

2
app/views/client/editor.pug

@ -21,6 +21,7 @@ block view-content
value= !!client ? client.name : undefined, value= !!client ? client.name : undefined,
placeholder="Enter client name", placeholder="Enter client name",
).uk-input ).uk-input
.uk-margin .uk-margin
label(for="description").uk-form-label Client description label(for="description").uk-form-label Client description
textarea( textarea(
@ -29,6 +30,7 @@ block view-content
rows=4, rows=4,
placeholder="Enter client description", placeholder="Enter client description",
).uk-textarea.uk-resize-vertical= !!client ? client.description : undefined ).uk-textarea.uk-resize-vertical= !!client ? client.description : undefined
.uk-margin .uk-margin
label(for="hours-limit").uk-form-label Max. hours per week label(for="hours-limit").uk-form-label Max. hours per week
input( input(

13
app/views/client/project/editor.pug

@ -25,6 +25,7 @@ block view-content
value= !!project ? project.name : undefined, value= !!project ? project.name : undefined,
placeholder="Enter project name", placeholder="Enter project name",
).uk-input ).uk-input
.uk-margin .uk-margin
label(for="description").uk-form-label Project description label(for="description").uk-form-label Project description
textarea( textarea(
@ -33,6 +34,18 @@ block view-content
rows=4, rows=4,
placeholder="Enter project description" placeholder="Enter project description"
).uk-textarea.uk-resize-vertical= !!project ? 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 .uk-margin
label(for="hourly-rate").uk-form-label Hourly rate label(for="hourly-rate").uk-form-label Hourly rate
input( input(

19
app/views/home.pug

@ -4,8 +4,10 @@ block view-content
include task/components/grid include task/components/grid
include report/components/weekly-summary include report/components/weekly-summary
if Array.isArray(projects) && (projects.length > 0)
section.uk-section.uk-section-muted.uk-section-small section.uk-section.uk-section-muted.uk-section-small
.uk-container .uk-container
h1 Your Projects
if weeklyEarnings.length > 0 if weeklyEarnings.length > 0
+renderWeeklySummaryReport(weeklyEarnings) +renderWeeklySummaryReport(weeklyEarnings)
else else
@ -18,3 +20,20 @@ block view-content
taskGrid.activeTasks, taskGrid.activeTasks,
taskGrid.finishedTasks, 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,
)

13
app/views/task/view.pug

@ -15,6 +15,7 @@ block view-content
+renderProfilePicture(user) +renderProfilePicture(user)
.uk-width-expand .uk-width-expand
div(style="line-height: 1;").uk-text-lead.uk-text-truncated.uk-margin-small= task.note div(style="line-height: 1;").uk-text-lead.uk-text-truncated.uk-margin-small= task.note
if !manager
case task.status case task.status
when 'pending' when 'pending'
.uk-margin-small .uk-margin-small
@ -26,8 +27,10 @@ block view-content
button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Finish Task button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Finish Task
when 'finished' when 'finished'
.uk-text-success Finished task .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 .uk-width-auto.uk-text-right
.pretty.p-switch.p-slim .pretty.p-switch.p-slim
input( input(
@ -81,15 +84,15 @@ block view-content
else else
div No work sessions div No work sessions
if task.status === 'active' if !manager && (task.status === 'active')
div(class="uk-width-1-1 uk-width-large@m") div(class="uk-width-1-1 uk-flex-first uk-width-large@m uk-flex-last@m")
.uk-margin .uk-margin
video( video(
id="capture-preview", id="capture-preview",
poster="/img/default-poster.svg", poster="/img/default-poster.svg",
playsinline, muted, playsinline, muted,
).dtp-video ).dtp-video.no-select
.uk-margin.uk-text-small.uk-text-muted 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 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. p When you start a work session, you will select the screen, application, or browser tab to share.

Loading…
Cancel
Save