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. 45
      app/views/home.pug
  8. 35
      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.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 });

8
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();

51
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;

2
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

2
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(

13
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(

45
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,
)
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,
)

35
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.

Loading…
Cancel
Save