diff --git a/app/controllers/client.js b/app/controllers/client.js index 8955269..7c741b2 100644 --- a/app/controllers/client.js +++ b/app/controllers/client.js @@ -15,7 +15,6 @@ export default class ClientController extends SiteController { constructor (dtp) { super(dtp, ClientController.slug); - this.dtp = dtp; } async start ( ) { diff --git a/app/controllers/home.js b/app/controllers/home.js index bcf181a..ac8b1e5 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -28,7 +28,7 @@ export default class HomeController extends SiteController { } async getHome (req, res, next) { - const { client: clientService, task: taskService } = this.dtp.services; + const { client: clientService, report: reportService, task: taskService } = this.dtp.services; try { if (!req.user) { return res.redirect('/welcome'); @@ -41,6 +41,7 @@ export default class HomeController extends SiteController { 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.render('home'); } catch (error) { diff --git a/app/controllers/report.js b/app/controllers/report.js new file mode 100644 index 0000000..4707252 --- /dev/null +++ b/app/controllers/report.js @@ -0,0 +1,40 @@ +// report.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import express from 'express'; + +import { SiteController } from '../../lib/site-lib.js'; + +export default class ReportController extends SiteController { + + static get name ( ) { return 'ReportController'; } + static get slug ( ) { return 'report'; } + + constructor (dtp) { + super(dtp, ReportController.slug); + } + + async start ( ) { + // const { limiter: limiterService } = dtp.services; + // const limiterConfig = limiterService.config.report; + + const router = express.Router(); + this.dtp.app.use('/report', router); + + router.get('/', this.getDashboard.bind(this)); + } + + async getDashboard (req, res, next) { + const { report: reportService } = this.dtp.services; + try { + res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(req.user); + res.render('report/dashboard'); + } catch (error) { + this.log.error('failed to present report dashboard', { error }); + return next(error); + } + } +} \ No newline at end of file diff --git a/app/services/report.js b/app/services/report.js new file mode 100644 index 0000000..8241723 --- /dev/null +++ b/app/services/report.js @@ -0,0 +1,60 @@ +// report.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import mongoose from 'mongoose'; +const TaskSession = mongoose.model('TaskSession'); + +import dayjs from 'dayjs'; + +import { SiteService } from '../../lib/site-lib.js'; + +export default class ReportService extends SiteService { + + static get name ( ) { return 'ReportService'; } + static get slug () { return 'report'; } + + constructor (dtp) { + super(dtp, ReportService); + } + + async getWeeklyEarnings (user) { + const NOW = new Date(); + const dateStart = this.startOfWeek(NOW); + const dateEnd = dayjs(dateStart).add(1, 'week').toDate(); + + this.log.debug('computing weekly earnings', { dateStart, dateEnd }); + + /* + * I'm sure there's some beautiful way to do this using aggregation but I + * don't care at all (and won't) until aggregation becomes a usable API. + */ + const response = { sessionCount: 0, duration: 0, billable: 0 }; + await TaskSession + .find({ + user: user._id, + $and: [ + { created: { $gte: dateStart } }, + { finished: { $lt: dateEnd } }, + ], + }) + .cursor() + .eachAsync(async (session) => { + response.sessionCount += 1; + response.duration += session.duration; + response.billable += session.hourlyRate * (session.duration / 60 / 60); + }); + + return response; + } + + startOfWeek (date) { + date = date || new Date(); + date.setHours(0,0,0,0); + + var diff = date.getDate() - date.getDay() + (date.getDay() === 0 ? -6 : 1); + return new Date(date.setDate(diff)); + } +} \ No newline at end of file diff --git a/app/services/task.js b/app/services/task.js index 51dcfd7..d764caa 100644 --- a/app/services/task.js +++ b/app/services/task.js @@ -285,10 +285,4 @@ export default class TaskService extends SiteService { await this.closeTaskSession(session); }); } - - startOfWeek (date) { - date = date || new Date(); - var diff = date.getDate() - date.getDay() + (date.getDay() === 0 ? -6 : 1); - return new Date(date.setDate(diff)); - } } \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index 5473224..dda215c 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -13,6 +13,9 @@ nav(style="background: #000000;").uk-navbar-container.uk-light li a(href="/client", aria-label="Manage your clients").uk-navbar-item .uk-text-bold.no-select Clients + li + a(href="/report", aria-label="View your work reports").uk-navbar-item + .uk-text-bold.no-select Reports .uk-navbar-right if !user diff --git a/app/views/home.pug b/app/views/home.pug index 113453c..741dfc0 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -3,6 +3,19 @@ block view-content include task/components/grid + section.uk-section.uk-section-secondary.uk-section-small + .uk-container + div(uk-grid).uk-flex-between + .uk-width-auto + div sessions + .uk-text-large.uk-text-bold= formatCount(weeklyEarnings.sessionCount) + .uk-width-auto + div time worked + .uk-text-large.uk-text-bold= numeral(weeklyEarnings.duration).format('0:00:00') + .uk-width-auto + div billable + .uk-text-large.uk-text-bold= numeral(weeklyEarnings.billable).format('$0,0.00') + section.uk-section.uk-section-default .uk-container +renderTaskGrid( diff --git a/app/views/report/dashboard.pug b/app/views/report/dashboard.pug new file mode 100644 index 0000000..8f909f1 --- /dev/null +++ b/app/views/report/dashboard.pug @@ -0,0 +1,16 @@ +extends ../layout/main +block view-content + + section.uk-section.uk-section-default.uk-section + .uk-container + + div(uk-grid).uk-flex-between + .uk-width-auto + .uk-text-bold.uk-text-small Sessions + div= formatCount(weeklyEarnings.sessionCount) + .uk-width-auto + .uk-text-bold.uk-text-small Hours + div= numeral(weeklyEarnings.duration).format('0:00:00') + .uk-width-auto + .uk-text-bold.uk-text-small Billable + div= numeral(weeklyEarnings.billable).format('$0,0.00') \ No newline at end of file