diff --git a/LICENSE b/LICENSE index 8942413..4e735f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,2 +1,2 @@ -DTP Time Tracker Copyright (C) 2024 DTP Technologies, LLC -All Rights Reserved +DTP Base Copyright (C) 2024 DTP Technologies, LLC +All Rights Reserved \ No newline at end of file diff --git a/README.md b/README.md index 5d9870e..849dba2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# DTP Chat +# DTP Base -A no-nonsense/no-frills communications platform. +A production-ready Node.js application development framework for use in-house at DTP Technologies, LLC. ## System Requirements @@ -82,4 +82,4 @@ Edit `.bashrc` and set `NODE_ENV=production` ## Emoji Picker -Chat currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. \ No newline at end of file +Base currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. \ No newline at end of file diff --git a/app/controllers/client.js b/app/controllers/client.js deleted file mode 100644 index a24b33b..0000000 --- a/app/controllers/client.js +++ /dev/null @@ -1,216 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController, SiteError } from '../../lib/site-lib.js'; -import { populateClientId, populateProjectId } from './lib/populators.js'; - -export default class ClientController extends SiteController { - - static get name ( ) { return 'ClientController'; } - static get slug ( ) { return 'client'; } - - constructor (dtp) { - super(dtp, ClientController.slug); - } - - async start ( ) { - const { dtp } = this; - const { - limiter: limiterService, - session: sessionService, - } = dtp.services; - - const limiterConfig = limiterService.config.client; - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - function checkClientOwnership (req, res, next) { - if (!res.locals.client.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } - - function checkProjectOwnership (req, res, next) { - if (!res.locals.project.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } - - const router = express.Router(); - dtp.app.use('/client', authCheck, router); - - router.param('clientId', populateClientId(this)); - router.param('projectId', populateProjectId(this)); - - router.post( - '/:clientId/project/:projectId', - limiterService.create(limiterConfig.postProjectUpdate), - checkClientOwnership, - this.postProjectUpdate.bind(this), - ); - - router.post( - '/:clientId/project', - limiterService.create(limiterConfig.postProjectCreate), - checkClientOwnership, - this.postProjectCreate.bind(this), - ); - - router.post( - '/:clientId', - limiterService.create(limiterConfig.postClientUpdate), - checkClientOwnership, - this.postClientUpdate.bind(this), - ); - - router.post( - '/', - limiterService.create(limiterConfig.postClientCreate), - this.postClientCreate.bind(this), - ); - - router.get( - '/:clientId/project/create', - limiterService.create(limiterConfig.getProjectCreate), - checkClientOwnership, - this.getProjectCreate.bind(this), - ); - - router.get( - '/:clientId/project/:projectId/edit', - limiterService.create(limiterConfig.getProjectEditor), - checkProjectOwnership, - this.getProjectEditor.bind(this), - ); - - router.get( - '/:clientId/project/:projectId', - limiterService.create(limiterConfig.getProjectView), - checkProjectOwnership, - this.getProjectView.bind(this), - ); - - router.get( - '/create', - limiterService.create(limiterConfig.getClientEditor), - this.getClientEditor.bind(this), - ); - - router.get( - '/:clientId/edit', - limiterService.create(limiterConfig.getClientEditor), - checkClientOwnership, - this.getClientEditor.bind(this), - ); - - router.get( - '/:clientId', - limiterService.create(limiterConfig.getClientView), - checkClientOwnership, - this.getClientView.bind(this), - ); - - router.get( - '/', - limiterService.create(limiterConfig.getHome), - this.getHome.bind(this), - ); - - return router; - } - - async postProjectUpdate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - await clientService.updateProject(res.locals.project, req.body); - res.redirect(`/client/${res.locals.client._id}/project/${res.locals.project._id}`); - } catch (error) { - this.log.error('failed to update client project', { error }); - return next(error); - } - } - - async postProjectCreate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.project = await clientService.createProject(res.locals.client, req.body); - res.redirect(`/client/${res.locals.client._id}/project/${res.locals.project._id}`); - } catch (error) { - this.log.error('failed to create client project', { error }); - return next(error); - } - } - - async postClientUpdate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - await clientService.updateClient(res.locals.client, req.body); - res.redirect(`/client/${res.locals.client._id}`); - } catch (error) { - this.log.error('failed to create client', { error }); - return next(error); - } - } - - async postClientCreate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.client = await clientService.createClient(req.user, req.body); - res.redirect(`/client/${res.locals.client._id}`); - } catch (error) { - this.log.error('failed to create client', { error }); - return next(error); - } - } - - async getProjectCreate (req, res) { - res.render('client/project/editor'); - } - - async getProjectEditor (req, res) { - res.render('client/project/editor'); - } - - async getProjectView (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - res.locals.taskGrid = await taskService.getTaskGridForProject(res.locals.project); - res.render('client/project/view'); - } catch (error) { - this.log.error('failed to present project view', { error }); - return next(error); - } - } - - async getClientEditor (req, res) { - res.render('client/editor'); - } - - async getClientView (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.projects = await clientService.getProjectsForClient(res.locals.client); - res.render('client/view'); - } catch (error) { - this.log.error('failed to present client home view', { error }); - return next(error); - } - } - - async getHome (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.clients = await clientService.getClientsForUser(req.user); - res.render('client/dashboard'); - } catch (error) { - this.log.error('failed to present client home view', { error }); - return next(error); - } - } -} \ No newline at end of file diff --git a/app/controllers/home.js b/app/controllers/home.js index 736900a..a1e4bd1 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -28,23 +28,13 @@ export default class HomeController extends SiteController { } async getHome (req, res, next) { - const { client: clientService, report: reportService, task: taskService } = this.dtp.services; try { if (!req.user) { return res.redirect('/welcome'); } res.locals.currentView = 'home'; - res.locals.pageDescription = 'DTP Time Tracker'; - - 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.locals.pageDescription = 'DTP Base'; res.render('home'); } catch (error) { diff --git a/app/controllers/lib/populators.js b/app/controllers/lib/populators.js index 58d95ca..84eb21d 100644 --- a/app/controllers/lib/populators.js +++ b/app/controllers/lib/populators.js @@ -2,71 +2,22 @@ import { SiteError } from '../../../lib/site-lib.js'; -export function populateClientId (controller) { - return async function (req, res, next, clientId) { - const { client: clientService } = controller.dtp.services; +/* + * This is a sample populator. It doesn't run. There is no sample service, etc. + * This is simply the pattern you follow to declare a new ExpressJS parameter + * populator and export it from your populators library. + */ +export function populateSampleParameter (controller) { + return async function (req, res, next, sampleParameter) { + const { sample: sampleService } = controller.dtp.services; try { - res.locals.client = await clientService.getClientById(clientId); - if (!res.locals.client) { - throw new SiteError(404, 'Client not found'); - } - if (!res.locals.client.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate client', { error }); - return next(error); - } - }; -} - -export function populateProjectId (controller) { - return async function (req, res, next, projectId) { - const { client: clientService } = controller.dtp.services; - try { - res.locals.project = await clientService.getProjectById(projectId); - if (!res.locals.project) { - throw new SiteError(404, 'Project not found'); - } - if (!res.locals.project.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your project'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate project', { error }); - return next(error); - } - }; -} - -export function populateTaskId (controller) { - return async function (req, res, next, taskId) { - const { task: taskService } = controller.dtp.services; - try { - res.locals.task = await taskService.getTaskById(taskId); - if (!res.locals.task) { - throw new SiteError(404, 'Task not found'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate taskId', { taskId, error }); - return next(error); - } - }; -} - -export function populateSessionId (controller) { - return async function (req, res, next, sessionId) { - const { task: taskService } = controller.dtp.services; - try { - res.locals.session = await taskService.getTaskSessionById(sessionId); - if (!res.locals.session) { - throw new SiteError(404, 'Task session not found'); + res.locals.sample = await sampleService.getSample(sampleParameter); + if (!res.locals.sample) { + throw new SiteError(404, 'Sample not found'); } return next(); } catch (error) { - controller.log.error('failed to populate sessionId', { sessionId, error }); + controller.log.error('failed to populate sampleParameter', { sampleParamater, error }); return next(error); } }; diff --git a/app/controllers/manager.js b/app/controllers/manager.js deleted file mode 100644 index a9bf7c6..0000000 --- a/app/controllers/manager.js +++ /dev/null @@ -1,43 +0,0 @@ -// manager.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController } from '../../lib/site-lib.js'; -import { populateClientId, populateProjectId } from './lib/populators.js'; - -export default class ManagerController extends SiteController { - - static get name ( ) { return 'ManagerController'; } - static get slug ( ) { return 'manager'; } - - constructor (dtp) { - super(dtp, ManagerController.slug); - } - - async start ( ) { - const { dtp } = this; - const { - // limiter: limiterService, - session: sessionService, - } = dtp.services; - - // const limiterConfig = limiterService.config.manager; - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - dtp.app.use('/manager', authCheck, router); - - router.param('clientId', populateClientId(this)); - router.param('projectId', populateProjectId(this)); - - router.get('/', this.getDashboard.bind(this)); - } - - async getDashboard (req, res) { - res.render('manager/dashboard'); - } -} \ No newline at end of file diff --git a/app/controllers/report.js b/app/controllers/report.js deleted file mode 100644 index 77a31f8..0000000 --- a/app/controllers/report.js +++ /dev/null @@ -1,52 +0,0 @@ -// 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, - session: sessionService, - } = this.dtp.services; - - // const limiterConfig = limiterService.config.report; - - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - this.dtp.app.use('/report', authCheck, router); - - router.get('/', this.getDashboard.bind(this)); - } - - async getDashboard (req, res, next) { - const { report: reportService } = this.dtp.services; - try { - res.locals.pageTitle = 'Weekly Summary Report'; - res.locals.pageDescription = 'A breakdown of project and contractor performance for the current week.'; - - res.locals.weekStartDate = reportService.startOfWeek(); - res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(req.user); - res.locals.dailyTimeWorked = await reportService.getDailyHoursWorkedForUser(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/controllers/task.js b/app/controllers/task.js deleted file mode 100644 index 51dcb6a..0000000 --- a/app/controllers/task.js +++ /dev/null @@ -1,257 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController, SiteError } from '../../lib/site-lib.js'; -import { populateTaskId, populateSessionId } from './lib/populators.js'; - -export default class TaskController extends SiteController { - - static get name ( ) { return 'TaskController'; } - static get slug ( ) { return 'task'; } - - constructor (dtp) { - super(dtp, TaskController); - } - - async start ( ) { - const { dtp } = this; - - const { - limiter: limiterService, - session: sessionService, - } = dtp.services; - const limiterConfig = limiterService.config.task; - - const multer = this.createMulter(TaskController.slug, { - limits: { - fileSize: 1024 * 1000 * 5, - }, - }); - - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - dtp.app.use('/task', authCheck, router); - - router.use(async (req, res, next) => { - res.locals.currentView = TaskController.name; - return next(); - }); - - async function checkTaskOwnership (req, res, next) { - if (Array.isArray(res.locals.task.project.managers) && (res.locals.task.project.managers.length > 0)) { - 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 (Array.isArray(res.locals.task.project.managers) && (res.locals.task.project.managers.length > 0)) { - 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(); - } - - router.param('taskId', populateTaskId(this)); - router.param('sessionId', populateSessionId(this)); - - router.post( - '/:taskId/session/start', - limiterService.create(limiterConfig.postStartTaskSession), - this.postStartTaskSession.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/screenshot', - checkSessionOwnership, - limiterService.create(limiterConfig.postTaskSessionScreenshot), - multer.single('image'), - this.postTaskSessionScreenshot.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/status', - limiterService.create(limiterConfig.postTaskSessionStatus), - checkSessionOwnership, - this.postTaskSessionStatus.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/close', - limiterService.create(limiterConfig.postCloseTaskSession), - checkSessionOwnership, - this.postCloseTaskSession.bind(this), - ); - - router.post( - '/:taskId/start', - limiterService.create(limiterConfig.postStartTask), - checkTaskOwnership, - this.postStartTask.bind(this), - ); - router.post( - '/:taskId/close', - limiterService.create(limiterConfig.postCloseTask), - checkTaskOwnership, - this.postCloseTask.bind(this), - ); - - router.post( - '/', - limiterService.create(limiterConfig.postCreateTask), - this.postCreateTask.bind(this), - ); - - router.get( - '/:taskId/session/:sessionId', - limiterService.create(limiterConfig.getTaskSessionView), - checkSessionOwnership, - this.getTaskSessionView.bind(this), - ); - - router.get( - '/:taskId', - limiterService.create(limiterConfig.getTaskView), - checkTaskOwnership, - this.getTaskView.bind(this), - ); - } - - async postStartTaskSession (req, res) { - const { task: taskService } = this.dtp.services; - try { - res.locals.session = await taskService.createTaskSession(res.locals.task); - res.status(200).json({ - success: true, - session: res.locals.session, - }); - } catch (error) { - this.log.error('failed to create task session', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postTaskSessionScreenshot (req, res) { - const { task: taskService } = this.dtp.services; - try { - await taskService.addTaskSessionScreenshot(res.locals.session, req.file); - res.status(200).json({ success: true }); - } catch (error) { - this.log.error('failed to add task session screenshot', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postTaskSessionStatus (req, res) { - const { task: taskService } = this.dtp.services; - try { - this.log.debug('updating task session status', { - sessionId: res.locals.session._id, - params: req.body, - }); - await taskService.setTaskSessionStatus(res.locals.session, req.body.status); - - const displayList = this.createDisplayList('set-session-status'); - displayList.showNotification( - 'Session status updated', - 'success', - 'bottom-center', - 5000, - ); - - res.status(200).json({ success: true, displayList }); - } catch (error) { - this.log.error('failed to update task session status', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postCloseTaskSession (req, res) { - const { task: taskService } = this.dtp.services; - try { - await taskService.closeTaskSession(res.locals.session); - res.status(200).json({ success: true }); - } catch (error) { - this.log.error('failed to close task session', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postStartTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - await taskService.startTask(res.locals.task); - res.redirect(`/task/${res.locals.task._id}`); - } catch (error) { - this.log.error('failed to close task session', { error }); - return next(error); - } - } - - async postCloseTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - await taskService.closeTask(res.locals.task); - res.redirect('/'); - } catch (error) { - this.log.error('failed to close task', { error }); - return next(error); - } - } - - async postCreateTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - res.locals.task = await taskService.createTask(req.user, req.body); - res.redirect(`/task/${res.locals.task._id}`); - } catch (error) { - this.log.error('failed to create new task', { error }); - return next(error); - } - } - - async getTaskSessionView (req, res) { - res.render('task/session/view'); - } - - async getTaskView (req, res, next) { - const { report: reportService, task: taskService } = this.dtp.services; - try { - res.locals.pagination = this.getPaginationParameters(req, 50); - res.locals.sessions = await taskService.getSessionsForTask( - res.locals.task, - res.locals.pagination, - ); - - res.locals.weekStartDate = reportService.startOfWeek(); - - res.render('task/view'); - } catch (error) { - this.log.error('failed to present the Task view', { error }); - return next(error); - } - } -} \ No newline at end of file diff --git a/app/models/client-project.js b/app/models/client-project.js deleted file mode 100644 index 7d29733..0000000 --- a/app/models/client-project.js +++ /dev/null @@ -1,20 +0,0 @@ -// project.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const ClientProjectSchema = new Schema({ - created: { type: Date, default: Date.now, required: true, index: 1 }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - managers: { type: [Schema.ObjectId], ref: 'User' }, - name: { type: String, required: true }, - description: { type: String }, - hourlyRate: { type: Number, required: true }, -}); - -export default mongoose.model('ClientProject', ClientProjectSchema); \ No newline at end of file diff --git a/app/models/client.js b/app/models/client.js deleted file mode 100644 index eb6e868..0000000 --- a/app/models/client.js +++ /dev/null @@ -1,23 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const MAX_HOURS_PER_WEEK = 7 * 24; - -const ClientSchema = new Schema({ - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - name: { type: String, required: true }, - description: { type: String }, - hoursLimit: { type: Number, default: 0, max: MAX_HOURS_PER_WEEK, required: true }, - weeklyTotals: { - timeWorked: { type: Number, default: 0, required: true }, - billable: { type: Number, default: 0, required: true }, - }, -}); - -export default mongoose.model('Client', ClientSchema); \ No newline at end of file diff --git a/app/models/lib/constants.js b/app/models/lib/constants.js index 8f19e0d..bbe28ef 100644 --- a/app/models/lib/constants.js +++ b/app/models/lib/constants.js @@ -1,4 +1,5 @@ 'use strict'; -export const MIN_ROOM_CAPACITY = 4; -export const MAX_ROOM_CAPACITY = 25; \ No newline at end of file +/* + * Declare model constants here and import them into whatever needs them. + */ \ No newline at end of file diff --git a/app/models/lib/media.js b/app/models/lib/media.js index 29d004b..25a9657 100644 --- a/app/models/lib/media.js +++ b/app/models/lib/media.js @@ -7,15 +7,6 @@ import mongoose from 'mongoose'; const Schema = mongoose.Schema; -const EPISODE_STATUS_LIST = [ - 'starting', // the stream is connecting - 'live', // the stream is live - 'ending', // the stream has ended, queued for processing - 'processing', // the stream is being processed for DVR - 'replay', // the stream is available on the DVR - 'expired', // the stream is expired (removed) -]; - const VIDEO_STATUS_LIST = [ 'new', // the video (original) is on storage / queued 'processing', // the video is being processed for distribution @@ -23,15 +14,7 @@ const VIDEO_STATUS_LIST = [ 'removed', // the video has been removed ]; -const ROOM_STATUS_LIST = [ - 'starting', // the room is starting a call - 'live', // the room has a live call - 'shutdown', // the room is closing it's live call - 'expired', // the room has failed to check in - 'crashed', // the room's worker or server has crashed -]; - -const MediaMetadataSchema = new Schema({ +const VideoMetadataSchema = new Schema({ type: { type: String }, size: { type: Number }, bitRate: { type: Number }, @@ -58,9 +41,7 @@ const AudioMetadataSchema = new Schema({ }); export { - EPISODE_STATUS_LIST, VIDEO_STATUS_LIST, - ROOM_STATUS_LIST, - MediaMetadataSchema, + VideoMetadataSchema, AudioMetadataSchema, }; \ No newline at end of file diff --git a/app/models/task-session.js b/app/models/task-session.js deleted file mode 100644 index 02bb3e2..0000000 --- a/app/models/task-session.js +++ /dev/null @@ -1,37 +0,0 @@ -// task-session.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const STATUS_LIST = [ - 'active', - 'reconnecting', - 'finished', - 'expired', -]; - -const ScreenshotSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - image: { type: Schema.ObjectId, required: true, ref: 'Image' }, -}); - -const TaskSessionSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - lastUpdated: { type: Date, required: true, default: Date.now, index: 1 }, - finished: { type: Date, index: 1 }, - duration: { type: Number, default: 0, required: true }, - hourlyRate: { type: Number, required: true }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - project: { type: Schema.ObjectId, required: true, index: 1, ref: 'ClientProject' }, - task: { type: Schema.ObjectId, required: true, index: 1, ref: 'Task' }, - status: { type: String, enum: STATUS_LIST, default: 'active', required: true }, - notes: { type: [String], default: [ ] }, - screenshots: { type: [ScreenshotSchema] }, -}); - -export default mongoose.model('TaskSession', TaskSessionSchema); \ No newline at end of file diff --git a/app/models/task.js b/app/models/task.js deleted file mode 100644 index df23a88..0000000 --- a/app/models/task.js +++ /dev/null @@ -1,23 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const STATUS_LIST = ['pending', 'active', 'finished']; - -const TaskSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - finished: { type: Date, index: 1 }, - duration: { type: Number, default: 0, required: true }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - project: { type: Schema.ObjectId, required: true, index: 1, ref: 'ClientProject' }, - status: { type: String, enum: STATUS_LIST, default: 'pending', required: true }, - note: { type: String, required: true }, -}); - -export default mongoose.model('Task', TaskSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 2571460..03cdd8c 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -45,7 +45,7 @@ const UserSchema = new Schema({ }, bio: { type: String }, ui: { - theme: { type: String, default: 'tracker-light', required: true }, + theme: { type: String, default: 'dtp-light', required: true }, }, flags: { type: UserFlagsSchema, default: { }, required: true, select: false }, permissions: { type: UserPermissionsSchema, default: { }, required: true, select: false }, diff --git a/app/models/video.js b/app/models/video.js index cf3f2d0..61b5bb1 100644 --- a/app/models/video.js +++ b/app/models/video.js @@ -9,7 +9,7 @@ const Schema = mongoose.Schema; import { VIDEO_STATUS_LIST, - MediaMetadataSchema, + VideoMetadataSchema, } from './lib/media.js'; const VideoSchema = new Schema({ @@ -24,7 +24,7 @@ const VideoSchema = new Schema({ media: { bucket: { type: String, required: true }, key: { type: String, required: true }, - metadata: { type: MediaMetadataSchema }, + metadata: { type: VideoMetadataSchema }, }, }); diff --git a/app/services/client.js b/app/services/client.js deleted file mode 100644 index 05cd485..0000000 --- a/app/services/client.js +++ /dev/null @@ -1,293 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Client = mongoose.model('Client'); -const ClientProject = mongoose.model('ClientProject'); - -import { SiteService, SiteError } from '../../lib/site-lib.js'; - -export default class ClientService extends SiteService { - - static get name ( ) { return 'ClientService'; } - static get slug () { return 'client'; } - - constructor (dtp) { - super(dtp, ClientService); - } - - async start ( ) { - const { user: userService } = this.dtp.services; - - this.populateClient = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - ]; - - this.populateClientProject = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: this.populateClient, - }, - { - path: 'managers', - select: userService.USER_SELECT, - }, - ]; - } - - async createClient (user, clientDefinition) { - const { text: textService } = this.dtp.services; - - const client = new Client(); - client.user = user._id; - client.name = textService.filter(clientDefinition.name); - if (clientDefinition.description && clientDefinition.description.length > 0) { - client.description = textService.filter(clientDefinition.description); - } - client.hoursLimit = clientDefinition.hoursLimit; - - await client.save(); - - return client.toObject(); - } - - async updateClient (client, clientDefinition) { - const { text: textService } = this.dtp.services; - const update = { $set: { }, $unset: { } }; - - let changed = false; - - if (clientDefinition.name !== client.name) { - update.$set.name = textService.filter(clientDefinition.name); - changed = true; - } - - if (clientDefinition.description && (clientDefinition.description.length > 0)) { - if (clientDefinition.description !== client.description) { - update.$set.description = textService.filter(clientDefinition.description); - changed = true; - } - } else { - update.$unset.description = 1; - changed = true; - } - - if (clientDefinition.hoursLimit !== client.hoursLimit) { - update.$set.hoursLimit = clientDefinition.hoursLimit; - changed = true; - } - - if (!changed) { - return; - } - - await Client.updateOne({ _id: client._id }, update); - } - - async getClientById (clientId) { - const client = Client - .findOne({ _id: clientId }) - .populate(this.populateClient) - .lean(); - return client; - } - - async getClientsForUser (user) { - const clients = Client - .find({ user: user._id }) - .sort({ name: 1 }) - .populate(this.populateClient) - .lean(); - return clients; - } - - async createProject (client, projectDefinition) { - const { text: textService } = this.dtp.services; - - const project = new ClientProject(); - project.user = client.user._id; - project.client = client._id; - - project.name = textService.filter(projectDefinition.name); - if (projectDefinition.description && (projectDefinition.description.length > 0)) { - 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(); - - return project.toObject(); - } - - async updateProject (project, projectDefinition) { - const { text: textService } = this.dtp.services; - const update = { $set: { }, $unset: { } }; - let changed = false; - - if (projectDefinition.name !== project.name) { - 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; - } - - if (!changed) { - return; - } - - 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 }) - .populate(this.populateClientProject) - .lean(); - return project; - } - - async getProjectsForUser (user) { - const projects = await ClientProject - .find({ user: user._id }) - .sort({ name: 1 }) - .populate(this.populateClientProject) - .lean(); - return projects; - } - - async getProjectsForClient (client) { - const projects = await ClientProject - .find({ client: client._id }) - .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; - } - - async addTimeWorked (client, duration) { - const { task: taskService } = this.dtp.services; - - const durationLimit = client.hoursLimit * 3600; - if ((client.weeklyTotals.timeWorked + duration) > durationLimit) { - this.log.alert('clamping time worked to weekly hours limit', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - }, - }); - await Client.updateOne( - { _id: client._id }, - { - $set: { 'weeklyTotals.timeWorked': durationLimit }, - }, - ); - - this.log.alert('Ending work sessions for user due to hours limit reached', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - }, - }); - await taskService.closeTaskSessionForUser(client.user); - - const displayList = this.createDisplayList('session-control'); - displayList.showNotification( - 'Hours limit reached. The task work session has been closed.', - 'danger', - 'bottom-center', - 5000, - ); - - this.dtp.emitter - .to(client.user._id.toString()) - .emit('session-control', { - cmd: 'end-session', - displayList, - }); - - return; - } - - this.log.info('adding time worked to client weekly totals', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - currentHours: client.weeklyTotals.timeWorked / 3600, - }, - hours: duration / 3600, - }); - await Client.updateOne( - { _id: client._id }, - { - $inc: { 'weeklyTotals.timeWorked': duration }, - }, - ); - } -} \ No newline at end of file diff --git a/app/services/report.js b/app/services/report.js deleted file mode 100644 index a1b3c4f..0000000 --- a/app/services/report.js +++ /dev/null @@ -1,141 +0,0 @@ -// 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 { client: clientService } = this.dtp.services; - const NOW = new Date(); - - const dateStart = this.startOfWeek(NOW); - const dateEnd = dayjs(dateStart).add(1, 'week').toDate(); - - const data = await TaskSession.aggregate([ - { - $match: { - user: user._id, - $and: [ - { created: { $gte: dateStart } }, - { finished: { $lt: dateEnd } }, - ], - }, - }, - { - $group: { - _id: { project: '$project' }, - sessionCount: { $sum: 1 }, - duration: { $sum: '$duration' }, - billable: { - $sum: { - $multiply: [ - '$hourlyRate', - { $divide: ['$duration', 3600 ] }, - ], - }, - }, - }, - }, - { - $project: { - _id: 0, - project: '$_id.project', - sessionCount: '$sessionCount', - duration: '$duration', - billable: '$billable', - }, - }, - ]); - - const response = await TaskSession.populate(data, [ - { - path: 'project', - populate: clientService.populateClientProject, - }, - ]); - - return response; - } - - async getDailyHoursWorkedForUser (user) { - const NOW = new Date(); - - const dateStart = this.startOfWeek(NOW); - const dateEnd = dayjs(dateStart).add(1, 'week').toDate(); - - const response = await TaskSession.aggregate([ - { - $match: { - $and: [ - { user: user._id }, - { finished: { $gt: dateStart } }, - { created: { $lt: dateEnd } }, - ], - } - }, - { - $group: { - _id: { - year: { $year: '$created' }, - month: { $month: '$created' }, - day: { $dayOfMonth: '$created' }, - }, - workSessionCount: { $sum: 1 }, - timeWorked: { $sum: '$duration' }, - }, - }, - { - $project: { - _id: false, - date: { - $dateFromParts: { - year: '$_id.year', - month: '$_id.month', - day: '$_id.day', - }, - }, - workSessionCount: '$workSessionCount', - hoursWorked: { $divide: ['$timeWorked', 3600] }, - }, - }, - ]); - - if (response.length < 7) { - let currentDay = dayjs(dateStart).add(response.length, 'day'); - while (response.length < 7) { - response.push({ - date: currentDay, - workSessionCount: 0, - hoursWorked: 0, - }); - currentDay = dayjs(currentDay).add(1, 'day'); - } - } - - 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 deleted file mode 100644 index 7199f4d..0000000 --- a/app/services/task.js +++ /dev/null @@ -1,329 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Task = mongoose.model('Task'); -const TaskSession = mongoose.model('TaskSession'); - -import dayjs from 'dayjs'; - -import { SiteService, SiteError } from '../../lib/site-lib.js'; - -export default class TaskService extends SiteService { - - static get name ( ) { return 'TaskService'; } - static get slug () { return 'task'; } - - constructor (dtp) { - super(dtp, TaskService); - } - - async start ( ) { - const { client: clientService, user: userService } = this.dtp.services; - - // const { jobQueue: jobQueueService } = this.dtp.services; - // this.linksQueue = jobQueueService.getJobQueue('links', this.dtp.config.jobQueues.links); - - this.populateTask = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: clientService.populateClient, - }, - { - path: 'project', - populate: clientService.populateClientProject, - }, - ]; - - this.populateTaskSession = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: clientService.populateClient, - }, - { - path: 'project', - populate: clientService.populateClientProject, - }, - { - path: 'task', - populate: this.populateTask, - }, - { - path: 'screenshots.image', - } - ]; - } - - async createTask (user, taskDefinition) { - const { text: textService } = this.dtp.services; - const NOW = new Date(); - - const task = new Task(); - task.created = NOW; - task.user = user._id; - task.client = mongoose.Types.ObjectId.createFromHexString(taskDefinition.clientId); - task.project = mongoose.Types.ObjectId.createFromHexString(taskDefinition.projectId); - task.status = 'pending'; - task.note = textService.filter(taskDefinition.note); - - await task.save(); - - return task.toObject(); - } - - async startTask (task) { - if (task.status !== 'pending') { - throw new SiteError(400, 'The task is not in the pending state'); - } - await Task.updateOne( - { _id: task._id }, - { $set: { status: 'active' } }, - ); - } - - async closeTask (task) { - await TaskSession - .find({ task: task._id, status: 'active' }) - .cursor() - .eachAsync(async (session) => { - await this.closeTaskSession(session); - }); - - await Task.updateOne( - { _id: task._id }, - { $set: { status: 'finished' } }, - ); - } - - async getTasksForUser (user, options, pagination) { - const search = { user: user._id }; - - options = options || { }; - if (options.pending) { - search.status = 'pending'; - } else if (options.active) { - search.status = 'active'; - } else if (options.finished) { - search.status = 'finished'; - } - - const tasks = await Task - .find(search) - .sort({ created: -1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTask) - .lean(); - - return tasks.reverse(); - } - - async getTasksForProject (project, options, pagination) { - const search = { project: project._id }; - - options = options || { }; - if (options.pending) { - search.status = 'pending'; - } else if (options.active) { - search.status = 'active'; - } else if (options.finished) { - search.status = 'finished'; - } - - const tasks = await Task - .find(search) - .sort({ created: -1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTask) - .lean(); - - return tasks.reverse(); - } - - async getTaskGridForUser (user) { - const pagination = { skip: 0, cpp: 10 }; - const pendingTasks = await this.getTasksForUser(user, { pending: true }, pagination); - const activeTasks = await this.getTasksForUser(user, { active: true }, pagination); - const finishedTasks = await this.getTasksForUser(user, { finished: true }, pagination); - return { pendingTasks, activeTasks, finishedTasks }; - } - - async getTaskGridForProject (project) { - const pagination = { skip: 0, cpp: 10 }; - const pendingTasks = await this.getTasksForProject(project, { pending: true }, pagination); - const activeTasks = await this.getTasksForProject(project, { active: true }, pagination); - const finishedTasks = await this.getTasksForProject(project, { finished: true }, pagination); - return { pendingTasks, activeTasks, finishedTasks }; - } - - async getTaskById (taskId) { - const task = await Task - .findOne({ _id: taskId }) - .populate(this.populateTask) - .lean(); - return task; - } - - async createTaskSession (task) { - const NOW = new Date(); - - if (await TaskSession.findOne({ user: task.user._id, status: 'active' })) { - throw new SiteError(401, "Can't start new session with a currently active session."); - } - - let session = new TaskSession(); - session.created = NOW; - session.lastUpdated = NOW; - session.hourlyRate = task.project.hourlyRate; - session.user = task.user._id; - session.client = task.client._id; - session.project = task.project._id; - session.task = task._id; - session.status = 'active'; - - await session.save(); - session = await TaskSession.populate(session, this.populateTaskSession); - - this.log.info('task session created', { - user: { - _id: task.user._id, - username: task.user.username, - }, - session: { - _id: session._id, - }, - }); - - return session.toObject(); - } - - async addTaskSessionScreenshot (session, file) { - const NOW = new Date(); - const { image: imageService } = this.dtp.services; - const image = await imageService.create(session.user._id, { }, file); - await TaskSession.updateOne( - { _id: session._id }, - { - $push: { - screenshots: { - created: NOW, - image: image._id, - }, - }, - }, - ); - - const displayList = this.createDisplayList('screenshot-accepted'); - displayList.showNotification( - 'Screenshot accepted', - 'success', - 'bottom-center', - 3000, - ); - - this.dtp.emitter - .to(session.task._id.toString()) - .emit('session-control', { - displayList, - }); - } - - async setTaskSessionStatus (session, status) { - if (status === session.status) { - return; // do nothing - } - if (!['active', 'reconnecting'].includes(status)) { - throw new SiteError(400, 'Can only set status to active or reconnecting'); - } - - this.log.info('updating task session status', { - user: { - _id: session.user._id, - username: session.user.username, - }, - session: { _id: session._id }, - status, - }); - - await TaskSession.updateOne({ _id: session._id }, { $set: { status } }); - } - - async closeTaskSession (session, status = 'finished') { - const { client: clientService } = this.dtp.services; - const NOW = new Date(); - - if (session.status !== 'active') { - throw new SiteError(400, 'The session is not currently active'); - } - - const duration = dayjs(NOW).diff(session.created, 'second'); - await TaskSession.updateOne( - { _id: session._id }, - { - $set: { - finished: NOW, - status, - duration, - }, - }, - ); - - await Task.updateOne( - { _id: session.task._id }, - { - $inc: { duration }, - }, - ); - - await clientService.addTimeWorked(session.client, duration); - - this.log.info('task session closed', { - user: { - _id: session.user._id, - username: session.user.username, - }, - duration, - }); - } - - async getSessionsForTask (task, pagination) { - const sessions = TaskSession - .find({ task: task._id }) - .sort({ created: 1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTaskSession) - .lean(); - return sessions; - } - - async getTaskSessionById (sessionId) { - const session = await TaskSession - .findOne({ _id: sessionId }) - .populate(this.populateTaskSession) - .lean(); - return session; - } - - async closeTaskSessionForUser (user) { - await TaskSession - .find({ user: user._id, status: 'active' }) - .populate(this.populateTaskSession) - .cursor() - .eachAsync(async (session) => { - await this.closeTaskSession(session); - }); - } -} \ No newline at end of file diff --git a/app/services/text.js b/app/services/text.js index 95e737c..8315b6c 100644 --- a/app/services/text.js +++ b/app/services/text.js @@ -10,12 +10,15 @@ const User = mongoose.model('User'); const Link = mongoose.model('Link'); import striptags from 'striptags'; -import unzalgo from 'unzalgo'; import shoetest from 'shoetest'; import diacritics from 'diacritics'; import DtpTextFilter from './lib/edit-with-vi.js'; -import { SiteService, SiteError } from '../../lib/site-lib.js'; +import { + SiteService, + SiteError, + SiteUnzalgo, +} from '../../lib/site-lib.js'; export default class TextService extends SiteService { @@ -37,7 +40,7 @@ export default class TextService extends SiteService { * @returns The cleaned text */ clean (text) { - text = unzalgo.clean(text); + text = SiteUnzalgo.clean(text); text = striptags(text.trim()); return text; } diff --git a/app/views/client/dashboard.pug b/app/views/client/dashboard.pug deleted file mode 100644 index 1e6e7fb..0000000 --- a/app/views/client/dashboard.pug +++ /dev/null @@ -1,24 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid) - .uk-width-expand - h1 Clients - .uk-width-auto - a(href="/client/create").uk-button.uk-button-default.uk-border-rounded Create Client - - .uk-margin - if Array.isArray(clients) && (clients.length > 0) - ul.uk-list.uk-list-divider - each client in clients - li - a(href=`/client/${client._id}`).uk-link-reset.uk-display-block - .uk-text-lead= client.name - div= client.description || 'No description' - - else - div There are no clients \ No newline at end of file diff --git a/app/views/client/editor.pug b/app/views/client/editor.pug deleted file mode 100644 index 984f44d..0000000 --- a/app/views/client/editor.pug +++ /dev/null @@ -1,54 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - - var actionUrl = !!client ? `/client/${client._id}` : "/client"; - form(method="POST", action= actionUrl).uk-form - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h1.uk-card-title #[span= !!client ? "Edit" : "New"] Client - - .uk-card-body - .uk-margin - label(for="name").uk-form-label Client name - input( - id="name", - name="name", - type="text", - value= !!client ? client.name : undefined, - placeholder="Enter client name", - ).uk-input - - .uk-margin - label(for="description").uk-form-label Client description - textarea( - id="name", - name="description", - 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( - id="hours-limit", - name="hoursLimit", - type="number", - value= !!client ? (client.hoursLimit || 40) : 40, - placeholder="Enter max. hours/week (0=unlimited)", - ).uk-input - .uk-text-small.uk-text-muted Use zero or 168 (7 x 24) for unlimited hours per week. - - .uk-card-footer - div(uk-grid).uk-grid-small.uk-flex-right - .uk-width-auto - a(href=`/client/${client._id}`).uk-button.uk-button-default.uk-border-rounded - i.fa-solid.fa-chevron-left - span.uk-margin-small-left Back - .uk-width-auto - button(type="submit").uk-button.uk-button-default.uk-border-rounded - i.fa-solid.fa-save - span.uk-margin-small-left #{!!client ? "Update" : "Create"} Client \ No newline at end of file diff --git a/app/views/client/project/editor.pug b/app/views/client/project/editor.pug deleted file mode 100644 index 0320246..0000000 --- a/app/views/client/project/editor.pug +++ /dev/null @@ -1,60 +0,0 @@ -extends ../../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - - var actionUrl = !!project ? `/client/${client._id}/project/${project._id}` : `/client/${client._id}/project`; - form(method="POST", action= actionUrl).uk-form - .uk-card.uk-card-default.uk-card-small - .uk-card-header - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand - h1.uk-card-title #[span= !!project ? "Edit" : "New"] Project - .uk-width-auto - div #{client.name} - - .uk-card-body - .uk-margin - label(for="name").uk-form-label Project name - input( - id="name", - name="name", - type="text", - value= !!project ? project.name : undefined, - placeholder="Enter project name", - ).uk-input - - .uk-margin - label(for="description").uk-form-label Project description - textarea( - id="name", - name="description", - 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( - id="hourly-rate", - name="hourlyRate", - type="number", - value= !!project ? project.hourlyRate : undefined, - placeholder="Enter hourly rate", - ).uk-input - - .uk-card-footer.uk-flex.uk-flex-right - button(type="submit").uk-button.uk-button-default.uk-border-rounded #[span= !!project ? "Update" : "Create"] Project \ No newline at end of file diff --git a/app/views/client/project/view.pug b/app/views/client/project/view.pug deleted file mode 100644 index 24c3968..0000000 --- a/app/views/client/project/view.pug +++ /dev/null @@ -1,39 +0,0 @@ -extends ../../layout/main -block view-content - - include ../../task/components/grid - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand - h1.uk-margin-remove= project.name - .uk-width-auto - a(href=`/client/${client._id}/project/${project._id}/edit`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-cog - span.uk-margin-small-left Settings - .uk-width-auto - .uk-text-bold - a(href=`/client/${client._id}`).uk-link-reset= client.name - .uk-text-small #{numeral(project.hourlyRate).format('$0,0.00')}/hr - - if project.description - div= project.description - - .uk-margin-medium - form(method="POST", action="/task").uk-form - input(type="hidden", name="clientId", value= client._id) - input(type="hidden", name="projectId", value= project._id) - .uk-card.uk-card-secondary.uk-card-small.uk-border-rounded - .uk-card-body.uk-border-rounded - label(for="note").uk-form-label.sr-only Task note - div(uk-grid).uk-grid-small - .uk-width-expand - input(id="note", name="note", placeholder="What will you be working on?").uk-input.uk-border-rounded - .uk-width-auto - button(type="submit").uk-button.uk-button-default.uk-border-rounded Create Task - - .uk-margin-medium - +renderTaskGrid(taskGrid.pendingTasks, taskGrid.activeTasks, taskGrid.finishedTasks) \ No newline at end of file diff --git a/app/views/client/view.pug b/app/views/client/view.pug deleted file mode 100644 index 00695ce..0000000 --- a/app/views/client/view.pug +++ /dev/null @@ -1,32 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-expand - h1.uk-margin-remove= client.name - if client.description - div= client.description - .uk-width-auto - a(href=`/client/${client._id}/edit`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-cog - span.uk-margin-small-left Settings - .uk-width-auto - a(href=`/client/${client._id}/project/create`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-plus - span.uk-margin-small-left Add Project - - .uk-margin - h2 Projects - if Array.isArray(projects) && (projects.length > 0) - ul.uk-list.uk-list-divided - each project in projects - li - a(href=`/client/${client._id}/project/${project._id}`).uk-link-reset.uk-display-block - .uk-text-bold= project.name - .uk-text-small= project.description || 'No description' - else - div There are no projects \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index dda215c..13ae90f 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -9,13 +9,6 @@ nav(style="background: #000000;").uk-navbar-container.uk-light li a(href="/", aria-label="Back to Home").uk-navbar-item .uk-text-bold.no-select Home - if user - 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 d099865..5970310 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -1,39 +1,8 @@ extends layout/main block view-content - include task/components/grid - include report/components/weekly-summary - - 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, - ) - - 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 + section.uk-section.uk-section-default.uk-section-small + .uk-container + + h1= site.name + p Welcome to the authenticated home page. You are logged in. This app does nothing, so get to work building your features and ship often! \ No newline at end of file diff --git a/app/views/layout/main.pug b/app/views/layout/main.pug index 9a0df72..e16e740 100644 --- a/app/views/layout/main.pug +++ b/app/views/layout/main.pug @@ -22,7 +22,7 @@ html(lang='en', data-obs-widget= obsWidget) block vendorcss - link(rel='stylesheet', href=`/dist/${(user) ? user.ui.theme : 'tracker-light'}.css?v=${pkg.version}`) + link(rel='stylesheet', href=`/dist/${(user) ? user.ui.theme : 'dtp-light'}.css?v=${pkg.version}`) link(rel='stylesheet', href=`/pretty-checkbox/pretty-checkbox.min.css?v=${pkg.version}`) block viewcss diff --git a/app/views/manager/dashboard.pug b/app/views/manager/dashboard.pug deleted file mode 100644 index 152726e..0000000 --- a/app/views/manager/dashboard.pug +++ /dev/null @@ -1,12 +0,0 @@ -extends ../layout/main -block view-content - - include ../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - h1 Projects You Manage - if Array.isArray(projects) && (projects.length > 0) - else - div You don't manage any projects at this time. \ No newline at end of file diff --git a/app/views/report/components/weekly-summary.pug b/app/views/report/components/weekly-summary.pug deleted file mode 100644 index d9158c9..0000000 --- a/app/views/report/components/weekly-summary.pug +++ /dev/null @@ -1,20 +0,0 @@ -mixin renderWeeklySummaryReport (data) - .uk-overflow-auto - table.uk-table.uk-table-small.no-select - thead - tr - th Project - th Client - th Sessions - th Time Worked - th Billable - tbody - each row in data - tr - td.uk-table-expand - a(href=`/client/${row.project.client._id}/project/${row.project._id}`)=row.project.name - td - a(href=`/client/${row.project.client._id}`)= row.project.client.name - td= formatCount(row.sessionCount) - td= numeral(row.duration).format('0:00:00') - td= numeral(row.billable).format('$0,0.00') \ No newline at end of file diff --git a/app/views/report/dashboard.pug b/app/views/report/dashboard.pug deleted file mode 100644 index f7eaf96..0000000 --- a/app/views/report/dashboard.pug +++ /dev/null @@ -1,41 +0,0 @@ -extends ../layout/main -block view-content - - include components/weekly-summary - - section.uk-section.uk-section-default.uk-section - .uk-container - h1 Week of #{dayjs(weekStartDate).format('MMMM DD')} - +renderWeeklySummaryReport(weeklyEarnings) - - .uk-margin-medium - h2 Daily Hours - .uk-overflow-auto - table.uk-table.uk-table-small.uk-table-divider - thead - tr - th.uk-table-expand Day of Week - th.uk-text-nowrap.uk-text-right.uk-table-shrink Session Count - th.uk-text-nowrap.uk-text-right.uk-table-shrink Hours Worked - th.uk-text-nowrap.uk-text-right.uk-table-shrink Time Worked - tbody - - - var totalHoursWorked = 0; - var totalSessionCount = 0; - - each day in dailyTimeWorked - - - totalHoursWorked += day.hoursWorked; - totalSessionCount += day.workSessionCount; - - tr - td.uk-table-expand= dayjs(day.date).format('dddd') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.workSessionCount > 0) ? formatCount(day.workSessionCount) : '---' - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.hoursWorked > 0) ? numeral(day.hoursWorked).format('0.00') : '---' - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.hoursWorked > 0) ? numeral(day.hoursWorked * 3600).format('0:00:00') : '---' - tfoot - tr - td.uk-table-expand TOTALS - td.uk-text-right.uk-text-nowrap.uk-table-shrink= formatCount(totalSessionCount) - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalHoursWorked).format('0.00') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalHoursWorked * 3600).format('0:00:00') \ No newline at end of file diff --git a/app/views/task/components/grid.pug b/app/views/task/components/grid.pug deleted file mode 100644 index 25c44b8..0000000 --- a/app/views/task/components/grid.pug +++ /dev/null @@ -1,23 +0,0 @@ -include list -mixin renderTaskGrid (pendingTasks, activeTasks, finishedTasks) - div(uk-grid).uk-grid-divider - div(class="uk-width1-1 uk-width-1-3@m") - h3 Pending Tasks - if Array.isArray(pendingTasks) && (pendingTasks.length > 0) - +renderTaskList(pendingTasks) - else - div No pending tasks - - div(class="uk-width1-1 uk-width-1-3@m") - h3 Active Tasks - if Array.isArray(activeTasks) && (activeTasks.length > 0) - +renderTaskList(activeTasks) - else - div No active tasks - - div(class="uk-width1-1 uk-width-1-3@m") - h3 Finished Tasks - if Array.isArray(finishedTasks) && (finishedTasks.length > 0) - +renderTaskList(finishedTasks) - else - div No finished tasks \ No newline at end of file diff --git a/app/views/task/components/list.pug b/app/views/task/components/list.pug deleted file mode 100644 index db41828..0000000 --- a/app/views/task/components/list.pug +++ /dev/null @@ -1,11 +0,0 @@ -mixin renderTaskList (tasks) - ul.uk-list.uk-list-divider - each task in tasks - li - a(href=`/task/${task._id}`).uk-display-block.uk-link-reset - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand= task.note - .uk-width-auto - .uk-text-small= numeral(task.duration).format('00:00:00') - .uk-text-small.uk-text-muted - span #{task.project.name}, #{task.client.name} diff --git a/app/views/task/session/view.pug b/app/views/task/session/view.pug deleted file mode 100644 index 4964a6d..0000000 --- a/app/views/task/session/view.pug +++ /dev/null @@ -1,55 +0,0 @@ -extends ../../layout/main -block view-content - - include ../../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-auto - +renderProfilePicture(user) - .uk-width-expand - .uk-text-large= user.displayName || user.username - div Work session #[code #{session._id}] for task "#{task.note}" - .uk-width-auto.uk-text-right - div= numeral(session.duration).format('HH:MM:SS') - .uk-text-small #{numeral(session.hourlyRate * (session.duration / 60 / 60)).format('$0,0.00')} - - .uk-margin-medium - .uk-flex.uk-flex-between.uk-flex-wrap - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Created - div= dayjs(session.created).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Last Updated - div= dayjs(session.lastUpdated).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Finished - div= dayjs(session.finished).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Project - div= session.project.name - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Client - div= session.client.name - - .uk-margin-medium - if Array.isArray(session.screenshots) && (session.screenshots.length > 0) - h3 Screenshots - div(class="uk-child-width-1-2 uk-child-width-1-3@s uk-child-width-1-4@m uk-child-width-1-5@l uk-child-width-1-6@xl", uk-grid, uk-lightbox="animation: slide").uk-grid-small - each screenshot in session.screenshots - a(href=`/image/${screenshot.image._id}`, - data-type="image", - data-caption= `${dayjs(screenshot.created).format('MMM DD [at] h:mm a')} | ${screenshot.image.metadata.format.toUpperCase()} | ${numeral(screenshot.image.size).format('0,0.0b')}`, - ).uk-link-reset - img(src=`/image/${screenshot.image._id}`, width= screenshot.image.metadata.width, height= screenshot.image.metadata.height, alt="Image attachment") - .uk-text-small.uk-text-muted= dayjs(screenshot.created).format('MMM DD [at] h:mm a') - else - div No screenshots were filed by the session. \ No newline at end of file diff --git a/app/views/task/view.pug b/app/views/task/view.pug deleted file mode 100644 index e520290..0000000 --- a/app/views/task/view.pug +++ /dev/null @@ -1,102 +0,0 @@ -extends ../layout/main -block view-content - - include ../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid) - div(class="uk-width-1-1 uk-width-expand@m") - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-auto - +renderProfilePicture(user) - .uk-width-expand - div(style="line-height: 1;").uk-text-lead.uk-text-truncated.uk-margin-small= task.note - 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' && !manager - .uk-width-auto.uk-text-right - .pretty.p-switch.p-slim - input( - id="active-toggle", - type="checkbox", - data-task-id= task._id, - onchange="return dtp.app.taskActivityToggle(event);", - ) - .state.p-success - label(for="active-toggle") - span.sr-only Active - #current-session-duration.uk-text-small.no-select= numeral(0).format('HH:MM:SS') - - - var timeRemaining = task.client.hoursLimit - (task.client.weeklyTotals.timeWorked / 3600); - .uk-text-small.no-select avail: #[span#time-remaining= numeral(timeRemaining).format('0,0.00')] - - .uk-margin-medium - - .uk-margin - h3(style="line-height: 1em;").uk-padding-remove.uk-margin-remove Work Sessions - small.uk-text-muted Week of #{dayjs(weekStartDate).format('MMMM DD')} - - if Array.isArray(sessions) && (sessions.length > 0) - - - var totalTimeWorked = 0; - var totalBillable = 0; - - table.uk-table.uk-table-small.uk-table-divider - thead - tr - th.uk-table-expand Start Time - th.uk-text-nowrap.uk-table-shrink Tracked - th.uk-text-nowrap.uk-table-shrink Billable - tbody - each session in sessions - - - totalTimeWorked += session.duration; - totalBillable += session.hourlyRate * (session.duration / 3600); - tr - td.uk-table-expand - a(href=`/task/${task._id}/session/${session._id}`, - onclick="return dtp.app.performSessionNavigation(event);", - ).uk-link-reset.uk-display-block= dayjs(session.created).format('dddd [at] h:mm a') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(session.duration).format('HH:MM:SS') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(session.hourlyRate * (session.duration / 3600)).format('$0,00.00') - tfoot - tr - td.uk-table-expand TOTALS - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalTimeWorked).format('HH:MM:SS') - td.uk-text-right.uk-text-nowrap.uk-table-shrink #{numeral(totalBillable).format('$0,0.00')} - else - div No work sessions - - 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.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. - -block viewjs - script. - window.dtp = window.dtp || { }; - window.dtp.task = !{JSON.stringify(task)}; \ No newline at end of file diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 7d2dc25..f318bee 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -68,8 +68,8 @@ block view-content .uk-margin label(for="ui-theme").uk-form-label UI Theme select(id="ui-theme", name="uiTheme").uk-select - option(value="tracker-light", selected= (user.ui.theme === 'tracker-light')) Light - option(value="tracker-dark", selected= (user.ui.theme === 'tracker-dark')) Dark + option(value="dtp-light", selected= (user.ui.theme === 'dtp-light')) Light + option(value="dtp-dark", selected= (user.ui.theme === 'dtp-dark')) Dark li .uk-margin diff --git a/app/views/welcome/home.pug b/app/views/welcome/home.pug index 908d9dc..87bf5ee 100644 --- a/app/views/welcome/home.pug +++ b/app/views/welcome/home.pug @@ -24,9 +24,6 @@ block view-content h2 About #{site.name} - p #{site.name} is a real-time communications tool with high quality audio and video, extremely low latency, instant messaging, voicemail, and an easy-to-use interface. It removes and avoids everything unnecessary while focusing on being excellent at making calls and helping people stay in touch. + p #{site.name} provides an application development harness for building secure and scalable production-ready web applications. - p There is no app to download from a store. #{site.name} is a Progressive Web App (PWA) that runs in your browser. It can be installed as a desktop or mobile app if you like, and provides an even sleeker interface if you do. It can do a better job delivering notifications when installed as an app. - - .uk-margin - .uk-text-small.uk-text-muted Anonymous use is not supported. A user account in good standing is required to use the app. #{site.name} is not free for hosting group and conference calls. Free members can make and receive calls, but can't create group/conference calls and don't have voicemail services. \ No newline at end of file + p There is no app to download from a store. #{site.name} is a Progressive Web App (PWA) that runs in your browser. It can be installed as a desktop or mobile app if you like, and provides a more native interface. #{site.name} can do a better job delivering notifications when installed as an app. \ No newline at end of file diff --git a/app/workers/tracker-monitor.js b/app/workers/tracker-monitor.js deleted file mode 100644 index 9cdc996..0000000 --- a/app/workers/tracker-monitor.js +++ /dev/null @@ -1,107 +0,0 @@ -// tracker-monitor.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import 'dotenv/config'; - -import path, { dirname } from 'path'; -import dayjs from 'dayjs'; - -import { SiteRuntime } from '../../lib/site-lib.js'; - -import { CronJob } from 'cron'; -const CRON_TIMEZONE = 'America/New_York'; - -class TrackerMonitorWorker extends SiteRuntime { - - static get name ( ) { return 'TrackerMonitorWorker'; } - static get slug ( ) { return 'trackerMonitor'; } - - constructor (rootPath) { - super(TrackerMonitorWorker, rootPath); - } - - async start ( ) { - await super.start(); - - const mongoose = await import('mongoose'); - this.TaskSession = mongoose.model('TaskSession'); - - this.viewModel = { }; - await this.populateViewModel(this.viewModel); - - /* - * Cron jobs - */ - - const sessionExpireSchedule = '* */5 * * * *'; // Every 5 minutes - this.cronJob = new CronJob( - sessionExpireSchedule, - this.expireTaskSessions.bind(this), - null, - true, - CRON_TIMEZONE, - ); - await this.expireTaskSessions(); - - /* - * Bull Queue job processors - */ - - // this.log.info('registering queue job processor', { config: this.config.jobQueues.links }); - // this.linksProcessingQueue = this.services.jobQueue.getJobQueue('links', this.config.jobQueues.links); - // this.linksProcessingQueue.process('link-ingest', 1, this.ingestLink.bind(this)); - - this.log.info('Tracker Monitor online'); - } - - async shutdown ( ) { - this.log.alert('ChatLinksWorker shutting down'); - await super.shutdown(); - } - - async expireTaskSessions ( ) { - const { task: taskService } = this.services; - - const NOW = new Date(); - const oldestDate = dayjs(NOW).subtract(10, 'minute'); - - this.log.debug('scanning for defunct sessions'); - await this.TaskSession - .find({ - $and: [ - { status: { $in: ['active', 'reconnecting'] } }, - { lastUpdated: { $lt: oldestDate } }, - ], - }) - .cursor() - .eachAsync(async (session) => { - this.log.info('expiring defunct task work session', { - session: { - _id: session._id, - created: session.created, - lastUpdated: session.lastUpdated, - }, - }); - taskService.closeTaskSession(session, 'expired'); - }); - } -} - -(async ( ) => { - - try { - const { fileURLToPath } = await import('node:url'); - const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line - - const worker = new TrackerMonitorWorker(path.resolve(__dirname, '..', '..')); - await worker.start(); - - } catch (error) { - console.error('failed to start tracker monitor worker', { error }); - process.exit(-1); - } - -})(); \ No newline at end of file diff --git a/assets/icon/dtp-chat.app-icon.png b/assets/icon/dtp-chat.app-icon.png deleted file mode 100644 index a174aff..0000000 Binary files a/assets/icon/dtp-chat.app-icon.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-114x114.png b/assets/icon/dtp-chat.app-icon/icon-114x114.png deleted file mode 100644 index a0183fd..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-114x114.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-120x120.png b/assets/icon/dtp-chat.app-icon/icon-120x120.png deleted file mode 100644 index 12465e2..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-120x120.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-144x144.png b/assets/icon/dtp-chat.app-icon/icon-144x144.png deleted file mode 100644 index a429832..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-144x144.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-150x150.png b/assets/icon/dtp-chat.app-icon/icon-150x150.png deleted file mode 100644 index ed9c2d4..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-150x150.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-152x152.png b/assets/icon/dtp-chat.app-icon/icon-152x152.png deleted file mode 100644 index 82be507..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-152x152.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-16x16.png b/assets/icon/dtp-chat.app-icon/icon-16x16.png deleted file mode 100644 index ee02676..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-16x16.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-180x180.png b/assets/icon/dtp-chat.app-icon/icon-180x180.png deleted file mode 100644 index ecf2e7f..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-180x180.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-192x192.png b/assets/icon/dtp-chat.app-icon/icon-192x192.png deleted file mode 100644 index a0d8f54..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-192x192.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-256x256.png b/assets/icon/dtp-chat.app-icon/icon-256x256.png deleted file mode 100644 index fbd23cd..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-256x256.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-310x310.png b/assets/icon/dtp-chat.app-icon/icon-310x310.png deleted file mode 100644 index 8b34bf7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-310x310.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-32x32.png b/assets/icon/dtp-chat.app-icon/icon-32x32.png deleted file mode 100644 index da88e0c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-32x32.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-36x36.png b/assets/icon/dtp-chat.app-icon/icon-36x36.png deleted file mode 100644 index f73b4c6..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-36x36.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-384x384.png b/assets/icon/dtp-chat.app-icon/icon-384x384.png deleted file mode 100644 index 704436c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-384x384.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-48x48.png b/assets/icon/dtp-chat.app-icon/icon-48x48.png deleted file mode 100644 index 02c8574..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-48x48.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-512x512.png b/assets/icon/dtp-chat.app-icon/icon-512x512.png deleted file mode 100644 index 9debbe7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-512x512.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-57x57.png b/assets/icon/dtp-chat.app-icon/icon-57x57.png deleted file mode 100644 index 349e689..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-57x57.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-60x60.png b/assets/icon/dtp-chat.app-icon/icon-60x60.png deleted file mode 100644 index 43b02c7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-60x60.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-70x70.png b/assets/icon/dtp-chat.app-icon/icon-70x70.png deleted file mode 100644 index 87e8506..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-70x70.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-72x72.png b/assets/icon/dtp-chat.app-icon/icon-72x72.png deleted file mode 100644 index ba5e38c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-72x72.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-76x76.png b/assets/icon/dtp-chat.app-icon/icon-76x76.png deleted file mode 100644 index cf2e3b9..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-76x76.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-96x96.png b/assets/icon/dtp-chat.app-icon/icon-96x96.png deleted file mode 100644 index aaac378..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-96x96.png and /dev/null differ diff --git a/client/img/app-icon.png b/client/img/app-icon.png index 1bb258e..2b11396 100644 Binary files a/client/img/app-icon.png and b/client/img/app-icon.png differ diff --git a/client/img/icon/icon-114x114.png b/client/img/icon/icon-114x114.png index dd8aba2..34e0c0a 100644 Binary files a/client/img/icon/icon-114x114.png and b/client/img/icon/icon-114x114.png differ diff --git a/client/img/icon/icon-120x120.png b/client/img/icon/icon-120x120.png index 5d6a5d4..ea70074 100644 Binary files a/client/img/icon/icon-120x120.png and b/client/img/icon/icon-120x120.png differ diff --git a/client/img/icon/icon-144x144.png b/client/img/icon/icon-144x144.png index 2f3ce51..0218fb6 100644 Binary files a/client/img/icon/icon-144x144.png and b/client/img/icon/icon-144x144.png differ diff --git a/client/img/icon/icon-150x150.png b/client/img/icon/icon-150x150.png index c80f261..b95c00d 100644 Binary files a/client/img/icon/icon-150x150.png and b/client/img/icon/icon-150x150.png differ diff --git a/client/img/icon/icon-152x152.png b/client/img/icon/icon-152x152.png index c2a418c..790db61 100644 Binary files a/client/img/icon/icon-152x152.png and b/client/img/icon/icon-152x152.png differ diff --git a/client/img/icon/icon-16x16.png b/client/img/icon/icon-16x16.png index f84c448..185998b 100644 Binary files a/client/img/icon/icon-16x16.png and b/client/img/icon/icon-16x16.png differ diff --git a/client/img/icon/icon-180x180.png b/client/img/icon/icon-180x180.png index 2b62c4e..725e24f 100644 Binary files a/client/img/icon/icon-180x180.png and b/client/img/icon/icon-180x180.png differ diff --git a/client/img/icon/icon-192x192.png b/client/img/icon/icon-192x192.png index 9ca324a..f0c698b 100644 Binary files a/client/img/icon/icon-192x192.png and b/client/img/icon/icon-192x192.png differ diff --git a/client/img/icon/icon-256x256.png b/client/img/icon/icon-256x256.png index c300431..646ea5a 100644 Binary files a/client/img/icon/icon-256x256.png and b/client/img/icon/icon-256x256.png differ diff --git a/client/img/icon/icon-310x310.png b/client/img/icon/icon-310x310.png index 78181ee..7ced692 100644 Binary files a/client/img/icon/icon-310x310.png and b/client/img/icon/icon-310x310.png differ diff --git a/client/img/icon/icon-32x32.png b/client/img/icon/icon-32x32.png index f43696e..7e8e035 100644 Binary files a/client/img/icon/icon-32x32.png and b/client/img/icon/icon-32x32.png differ diff --git a/client/img/icon/icon-36x36.png b/client/img/icon/icon-36x36.png index b90b1a9..9916eea 100644 Binary files a/client/img/icon/icon-36x36.png and b/client/img/icon/icon-36x36.png differ diff --git a/client/img/icon/icon-384x384.png b/client/img/icon/icon-384x384.png index 2635bc2..d8c032e 100644 Binary files a/client/img/icon/icon-384x384.png and b/client/img/icon/icon-384x384.png differ diff --git a/client/img/icon/icon-48x48.png b/client/img/icon/icon-48x48.png index c7a6d16..ff552dd 100644 Binary files a/client/img/icon/icon-48x48.png and b/client/img/icon/icon-48x48.png differ diff --git a/client/img/icon/icon-512x512.png b/client/img/icon/icon-512x512.png index 166fb65..ef625c9 100644 Binary files a/client/img/icon/icon-512x512.png and b/client/img/icon/icon-512x512.png differ diff --git a/client/img/icon/icon-57x57.png b/client/img/icon/icon-57x57.png index 971001f..4e1dd78 100644 Binary files a/client/img/icon/icon-57x57.png and b/client/img/icon/icon-57x57.png differ diff --git a/client/img/icon/icon-60x60.png b/client/img/icon/icon-60x60.png index 676d3cf..f6dfbb8 100644 Binary files a/client/img/icon/icon-60x60.png and b/client/img/icon/icon-60x60.png differ diff --git a/client/img/icon/icon-70x70.png b/client/img/icon/icon-70x70.png index 8251b9f..db32e6c 100644 Binary files a/client/img/icon/icon-70x70.png and b/client/img/icon/icon-70x70.png differ diff --git a/client/img/icon/icon-72x72.png b/client/img/icon/icon-72x72.png index ddf77de..bb9ba70 100644 Binary files a/client/img/icon/icon-72x72.png and b/client/img/icon/icon-72x72.png differ diff --git a/client/img/icon/icon-76x76.png b/client/img/icon/icon-76x76.png index 6422e45..153b45e 100644 Binary files a/client/img/icon/icon-76x76.png and b/client/img/icon/icon-76x76.png differ diff --git a/client/img/icon/icon-96x96.png b/client/img/icon/icon-96x96.png index a4cff5e..60dcce7 100644 Binary files a/client/img/icon/icon-96x96.png and b/client/img/icon/icon-96x96.png differ diff --git a/client/img/nav-icon.png b/client/img/nav-icon.png index 0c10246..83a94b4 100644 Binary files a/client/img/nav-icon.png and b/client/img/nav-icon.png differ diff --git a/client/js/time-tracker-audio.js b/client/js/base-audio.js similarity index 95% rename from client/js/time-tracker-audio.js rename to client/js/base-audio.js index cf13ce1..457f4dd 100644 --- a/client/js/time-tracker-audio.js +++ b/client/js/base-audio.js @@ -1,19 +1,17 @@ -// time-tracker-audio.js +// base-audio.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; -const DTP_COMPONENT_NAME = 'TimeTrackerAudio'; - import DtpLog from 'lib/dtp-log'; const AudioContext = window.AudioContext || window.webkitAudioContext; -export default class TimeTrackerAudio { +export default class BaseAudio { constructor ( ) { - this.log = new DtpLog(DTP_COMPONENT_NAME); + this.log = new DtpLog('BaseAudio'); } start ( ) { diff --git a/client/js/base-client.js b/client/js/base-client.js new file mode 100644 index 0000000..4a5e74e --- /dev/null +++ b/client/js/base-client.js @@ -0,0 +1,340 @@ +// base-client.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +const dtp = window.dtp = window.dtp || { }; + +import DtpApp from 'lib/dtp-app.js'; +import BaseAudio from './base-audio.js'; + +import QRCode from 'qrcode'; +import Cropper from 'cropperjs'; + +import dayjs from 'dayjs'; +import dayjsRelativeTime from 'dayjs/plugin/relativeTime.js'; +dayjs.extend(dayjsRelativeTime); + +export class BaseApp extends DtpApp { + + static get SFX_SAMPLE_SOUND ( ) { return 'sample-sound'; } + + constructor (user) { + super('BaseApp', user); + + this.loadSettings(); + + this.notificationPermission = 'default'; + this.haveFocus = true; // hard to load the app w/o also being the focused app + + window.addEventListener('dtp-load', this.onDtpLoad.bind(this)); + window.addEventListener('focus', this.onWindowFocus.bind(this)); + window.addEventListener('blur', this.onWindowBlur.bind(this)); + + /* + * Page Visibility API hooks + */ + window.addEventListener('pageshow', this.onWindowPageShow.bind(this)); + window.addEventListener('pagehide', this.onWindowPageHide.bind(this)); + window.addEventListener('freeze', this.onWindowFreeze.bind(this)); + window.addEventListener('resume', this.onWindowResume.bind(this)); + + this.updateTimestamps(); + + this.log.info('constructor', 'BaseApp client online'); + } + + async onDtpLoad ( ) { + this.log.info('onDtpLoad', 'dtp-load event received. Connecting to platform.'); + + await this.connect({ + mode: 'User', + onSocketConnect: this.onBaseSocketConnect.bind(this), + onSocketDisconnect: this.onBaseSocketDisconnect.bind(this), + }); + } + + async onWindowFocus (event) { + this.log.debug('onWindowFocus', 'window has received focus', { event }); + this.haveFocus = true; + } + + async onWindowBlur (event) { + this.log.debug('onWindowBlur', 'window has lost focus', { event }); + this.haveFocus = false; + } + + async onWindowPageShow (event) { + this.log.debug('onWindowPageShow', 'the page is being shown', { event }); + } + + async onWindowPageHide (event) { + this.log.debug('onWindowPageHide', 'the page is being hidden', { event }); + if (!event.persisted) { + await this.socket.disconnect(); + } + } + + async onWindowFreeze (event) { + this.log.debug('onWindowFreeze', 'the page is being frozen', { event }); + } + + async onWindowResume (event) { + this.log.debug('onWindowResume', 'the page is being resumed', { event }); + } + + async startAudio ( ) { + this.log.info('startAudio', 'starting audio'); + this.audio = new BaseAudio(); + this.audio.start(); + try { + await Promise.all([ + this.audio.loadSound(BaseApp.SFX_SAMPLE_SOUND, '/static/sfx/sample-sound.mp3'), + ]); + } catch (error) { + this.log.error('startAudio', 'failed to load sound', { error }); + // fall through + } + } + + async onBaseSocketConnect (socket) { + this.log.debug('onSocketConnect', 'attaching socket events'); + + this.systemMessageHandler = this.onSystemMessage.bind(this); + socket.on('system-message', this.systemMessageHandler); + } + + async onBaseSocketDisconnect (socket) { + this.log.debug('onSocketDisconnect', 'detaching socket events'); + + socket.off('system-message', this.systemMessageHandler); + delete this.systemMessageHandler; + + if (this.taskSession) { + await this.setTaskSessionStatus('reconnecting'); + } + } + + async onSystemMessage (message) { + if (message.displayList) { + this.displayEngine.executeDisplayList(message.displayList); + } + } + + async confirmNavigation (event) { + const target = event.currentTarget || event.target; + event.preventDefault(); + event.stopPropagation(); + + const href = target.getAttribute('href'); + const hrefTarget = target.getAttribute('target'); + const text = target.textContent; + const whitelist = [ + 'digitaltelepresence.com', + 'www.digitaltelepresence.com', + ]; + try { + const url = new URL(href); + if (!whitelist.includes(url.hostname)) { + await UIkit.modal.confirm(`
You are navigating to ${href}
, a link or button that was displayed as:
${text}
You are navigating to ${href}
, a link or button that was displayed as:
${text}