8 changed files with 134 additions and 8 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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)); |
||||
|
} |
||||
|
} |
@ -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') |
Loading…
Reference in new issue