UNPKG

workwatch

Version:

A Linux terminal program for honest worktime tracking and billing.

220 lines (213 loc) 8.44 kB
/** * This file is part of the WorkWatch, a Linux terminal program for honest * worktime tracking and billing. * * Copyright (C) 2020-2025 by Artur Rutkowski * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * WorkWatch is beeing developped and maintained by Artur (locust) Rutkwoski * <locust@mailbox.org> */ /** * This is a file-handling module representing the log file containing the * entire worklog. It has only one getter providing its content and few * methods for its manipulation. */ const WorkWatchFile = require("./file.js"); const TimeMeasurement = require("./time-measurement.js"); const utils = require("../utils.js"); function Log(jsonContent, pathToJSONFile = null) { // Extends the base object. WorkWatchFile.call(this, jsonContent, pathToJSONFile); Object.setPrototypeOf(Log.prototype, WorkWatchFile.prototype); } // It provides worklog content. There is no field like days in the worklog. Object.defineProperty(Log.prototype, "days", { get() { if (Array.isArray(this.fileContent)) { return this.fileContent; } else { const error = new Error("Your worklog is empty or it's format is invalid."); error.code = "ERR_FILE_LOG_EMPTY_OR_WRONG_FORMAT"; throw error; } }, enumerable: true, configurable: false }); // Verifies the worklog file. Object.defineProperty(Log.prototype, "verify", { value: function verify() { return new Promise((resolve, reject) => { // Forces the days getter validation. let {days} = this; days.forEach((day, index) => { // Check for extra fields in each day entry. There are no extra // fields allowed. if ( Object.keys(day).filter( key => !["day", "measurements"].includes(key) ).length > 0 ) { const error = new Error(`Your worklog file shouldn't contain any extra fields in any log entry (position number ${index + 1}).`); error.code = "ERR_FILE_LOG_EXTRA_PROPERTIES"; reject(error); } // Check a day field if it contains a correct date format (yyyy-mm-dd). if ( !"day" in day || !utils.verifyDateValue(day.day) ) { const error = new Error(`Your worklog file lacks the date of measurements on position number ${index + 1} or its format is invalid.`); error.code = "ERR_FILE_LOG_NO_DAY_PROPERTY_OR_WRONG_FORMAT"; reject(error); } // Check for correct measurements field in particular day entry. if ( !"measurements" in day || !Array.isArray(day.measurements) ) { const error = new Error(`Your worklog file has no measurements on ${day.day} or their format is invalid.`); error.code = "ERR_FILE_LOG_NO_MEASUREMENTS_PROPERTY_OR_INVALID"; reject(error); } let duplicatedDayIndex = days.findIndex( (duplicatedDay, dayIndex) => duplicatedDay.day === day.day && dayIndex !== index ); if (duplicatedDayIndex !== -1) { const error = new Error(`Your worklog file has duplicated entries (with the same date) at position number ${duplicatedDayIndex}.`); error.code = "ERR_FILE_LOG_DUPLICATED_DAY_ENTRY"; reject(error); } day.measurements.forEach((measurement, measurementIndex) => { // Check for hoursRange fields correct order between measurements. if ( measurementIndex < day.measurements.length - 1 && utils.timeToMinutesNumber( measurement.hoursRange[measurement.hoursRange.length - 1].to ) > utils.timeToMinutesNumber( day.measurements[measurementIndex + 1].hoursRange[0].from ) ) { const error = new Error(`Your worklog file should have an incremental order of measurements on ${day.day} entry.`); error.code = "ERR_FILE_LOG_NESTED_MEASUREMENTS_OR_COMMON_RANGE"; reject(error); } // Check particular measurement with its module verification. const verificationTimeMeasurement = new TimeMeasurement( JSON.stringify(measurement) ); verificationTimeMeasurement.verify() .then(verifySuccess => {}) .catch(verifyError => reject(verifyError)); }); // Check for correct measurements' enumeration (number field). let wrongEnumeratedIndex = day.measurements.findIndex( (measurement, measurementIndex) => measurement.number !== measurementIndex + 1 ); if (wrongEnumeratedIndex !== -1) { const error = new Error(`Your worklog file has wrong enumeration of measurements at ${day.day} entry.`); error.code = "ERR_FILE_LOG_WRONG_ENUMERATION"; reject(error); } // Check for correct sum of all measurements in particular day. // The maximum time allowed is 23 hours and 59 minutes. if ( day.measurements.reduce( (dayMinutes, measurement) => dayMinutes + utils.timeToMinutesNumber(measurement.time), 0 ) > 1439 ) { const error = new Error(`Your worklog file has the measurement which exceeds the maximum time measured value at ${day.day} entry.`); error.code = "ERR_FILE_LOG_EXCEEDED_MAX_TIME"; reject(error); } }); resolve(true); }); }, writable: false, enumerable: false, configurable: false }); // Adds a measurement to the worklog. Object.defineProperty(Log.prototype, "add", { value: function add(day, measurement) { return new Promise((resolve, reject) => { let existingDay = this.days.find(logDay => logDay.day === day); if (existingDay) { let existingMeasurement = existingDay.measurements.find( logMeasurement => logMeasurement.time === measurement.time && logMeasurement.number === measurement.number && logMeasurement.hoursRange.every( (range, rangeIndex) => ( range.from === measurement.hoursRange[rangeIndex].from && range.to === measurement.hoursRange[rangeIndex].to ) ) && logMeasurement.project === measurement.project ); if (existingMeasurement) { const error = new Error(`The measurement you want to add already exists in your worklog file at ${day} entry.`); error.code = "ERR_FILE_LOG_MEASUREMENT_EXISTS"; reject(error); } else { this.days[this.days.indexOf(existingDay)].measurements.push( measurement ); } } else { this.days.push({day, measurements: [measurement]}); } this.createOrSave() .then(createOrSaveSuccess => resolve(true)) .catch(newEntryVerificationOrCreationError => { reject(newEntryVerificationOrCreationError) }); }); }, writable: false, enumerable: false, configurable: false }); // Reads and instantiates an existing log file. Utilises the base object // load method. Object.defineProperty(Log, "load", { value: function load(pathToJSONFile) { return new Promise((resolve, reject) => { WorkWatchFile.load(pathToJSONFile, true, Log) .then(fileObject => { if (fileObject instanceof Log) { resolve(fileObject); } }) .catch(fileLoadError => { if (fileLoadError.code === "ENOENT") { const error = new Error(`There is no log file at ${pathToJSONFile}.`); error.code = "ERR_FILE_NO_LOG"; reject(error); } else { reject(fileLoadError); } }); }); }, writable: false, enumerable: false, configurable: false }); module.exports = Log;