workwatch
Version:
A Linux terminal program for honest worktime tracking and billing.
220 lines (213 loc) • 8.44 kB
JavaScript
/**
* 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;