Compare commits
No commits in common. "main" and "v0.0.2" have entirely different histories.
26 changed files with 208 additions and 1598 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
target/
|
||||||
README.md
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
*
|
|
||||||
!bin/wtc
|
|
||||||
!dist/**/*.js
|
|
||||||
!README.md
|
|
35
Makefile
35
Makefile
|
@ -1,37 +1,8 @@
|
||||||
.PHONY: help prod dev clean update-npmjs-readme release publish
|
build: node_modules
|
||||||
|
npm run build
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " - help: Show this help message"
|
|
||||||
@echo " - prod: Build the project in production mode"
|
|
||||||
@echo " - dev: Build the project in development mode"
|
|
||||||
@echo " - clean: Remove build artifacts"
|
|
||||||
@echo " - release: Create a new release version"
|
|
||||||
@echo " - publish: Publish the new version created with the release target"
|
|
||||||
|
|
||||||
node_modules:
|
node_modules:
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
prod: node_modules
|
|
||||||
npm run prod
|
|
||||||
|
|
||||||
dev: node_modules
|
|
||||||
npm run dev
|
|
||||||
chmod +x dist/wtc-dev.mjs
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -r dist node_modules
|
rm -r target node_modules
|
||||||
|
|
||||||
release: prod
|
|
||||||
@read -p "Enter version bump (patch, minor, major): " bump && \
|
|
||||||
version=$$(npm version $$bump | grep -oP "(?<=v)[^']+") && \
|
|
||||||
echo "Version $$version created. Run 'make publish' to push the changes and publish the package."
|
|
||||||
|
|
||||||
update-npmjs-readme:
|
|
||||||
asciidoctor -b docbook -o dist/README.xml README.adoc
|
|
||||||
pandoc -f docbook -t gfm dist/README.xml -o README.md
|
|
||||||
|
|
||||||
publish: update-npmjs-readme
|
|
||||||
@git push && \
|
|
||||||
git push --tags && \
|
|
||||||
npm publish --otp $$(pass otp services/npmjs.com)
|
|
||||||
|
|
69
README.adoc
69
README.adoc
|
@ -1,69 +1,14 @@
|
||||||
= Work time calculator
|
== Work time calculator
|
||||||
|
|
||||||
An interactive CLI tool to calculate work time.
|
An interactive CLI tool to calculate work time.
|
||||||
|
|
||||||
image::img/demo.png[]
|
This is a highly opinionated CLI tool I built for my specific needs.
|
||||||
|
In time, I will probable make it more generic and configurable.
|
||||||
|
For now, the following assumptions are made.
|
||||||
|
|
||||||
== Install
|
* You have an unpaid 30 minute lunch break
|
||||||
|
|
||||||
You can run this in your terminal
|
=== TODO
|
||||||
|
|
||||||
[,shell]
|
* [ ] Configuration file for default settings and altering behaviour in interactive mode
|
||||||
----
|
|
||||||
npm i -g work-time-calculator
|
|
||||||
----
|
|
||||||
|
|
||||||
If you get a permission denied error, you can run the previous command
|
|
||||||
with sudo (**not recommended**), or you can set a local prefix to npm.
|
|
||||||
Feel free to create the prefix wherever you like, this is just a
|
|
||||||
location I decided to use. The only requirement is that the location
|
|
||||||
needs to be readable and writable by your user.
|
|
||||||
|
|
||||||
[,shell]
|
|
||||||
----
|
|
||||||
npm config set prefix '~/.local/share/npm'
|
|
||||||
----
|
|
||||||
|
|
||||||
After that you can run the installation again. Running the program
|
|
||||||
requires that you have your npm prefix in your `$PATH`. You can find
|
|
||||||
an example of this in my https://git.korhonen.cc/FunctionalHacker/dotfiles/src/commit/4442252c659179d860d71982a6b705dcecc54ea6/home/.config/zsh/02-env.zsh#L31-L32[dotfiles]. This configuration file is for ZSH but should also work for bash.
|
|
||||||
|
|
||||||
After installation, you should be able to run the program with
|
|
||||||
|
|
||||||
[,shell]
|
|
||||||
----
|
|
||||||
wtc
|
|
||||||
----
|
|
||||||
|
|
||||||
== Update
|
|
||||||
|
|
||||||
To update, just run the install command again
|
|
||||||
|
|
||||||
== Rationale
|
|
||||||
|
|
||||||
Don't know if it's just me but calculating my working hours sometimes
|
|
||||||
can get difficult. Especially if you have flexible hours and you end up
|
|
||||||
starting at a weird time, f.ex 08:15. This combined with the fact that
|
|
||||||
I have to log my hours to many different tasks, at the end of the day
|
|
||||||
calculating all this can get very confusing.
|
|
||||||
|
|
||||||
To alleviate my pains, I included the following features
|
|
||||||
|
|
||||||
* Asks wether you already had lunch or not and accommodates this in the calculation
|
|
||||||
* Asks the hours that you already logged and calculates unlogged hours
|
|
||||||
* Calculates how much under/overtime you worked
|
|
||||||
|
|
||||||
This is a highly opinionated tool I built for my specific needs.
|
|
||||||
There probably exists other tools to do the same task
|
|
||||||
(maybe even better) but I wanted something simple that fits for my
|
|
||||||
needs specifically.
|
|
||||||
|
|
||||||
== Configuration file
|
|
||||||
|
|
||||||
See the https://git.korhonen.cc/FunctionalHacker/work-time-calculator/src/branch/main/config/config.toml[default configuration file]
|
|
||||||
for more information on how to override configurations.
|
|
||||||
|
|
||||||
== TODO
|
|
||||||
|
|
||||||
* [x] Configuration file for default settings and altering behaviour in interactive mode
|
|
||||||
* [ ] Non-interactive mode with CLI arguments parsing
|
* [ ] Non-interactive mode with CLI arguments parsing
|
||||||
|
|
6
bin/wtc
6
bin/wtc
|
@ -1,4 +1,2 @@
|
||||||
#!/bin/sh
|
#!/usr/bin/env node
|
||||||
|
import '../target/main.js';
|
||||||
DIR="$(dirname "$(readlink -f "$0")")"
|
|
||||||
node "$DIR/../dist/wtc.mjs" "$@"
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"unpaidLunchBreakDuration": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Comment out or remove if you don't have an unpaid lunch break or if you normally log your lunch break hours"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["en", "fi"],
|
|
||||||
"description": "The language of the application. Currently supported languages are English (en) and Finnish (fi)"
|
|
||||||
},
|
|
||||||
"timestampFormat": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Time format used to display timestamps (started, stopped, etc.). Refer to the dayjs documentation on how to set this https://day.js.org/docs/en/display/format"
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workDayDuration": { "type": "string", "description": "Your work day duration" },
|
|
||||||
"startTime": { "type": "string", "description": "The time you start working" },
|
|
||||||
"stopTime": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"description": "The time you stop working. Can either be 'now' or a time"
|
|
||||||
},
|
|
||||||
"hadLunch": { "type": "boolean", "description": "Wether you had lunch already or not" }
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"description": "Default values for inputs"
|
|
||||||
},
|
|
||||||
"askInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workDayDuration": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Disable prompt for work day duration if set to false"
|
|
||||||
},
|
|
||||||
"startTime": { "type": "boolean", "description": "Disable prompt for start time if set to false" },
|
|
||||||
"stopTime": { "type": "boolean", "description": "Disable prompt for stop time if set to false" },
|
|
||||||
"logged": { "type": "boolean", "description": "Disable prompt for logged time if set to false" }
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"description": "Settings to disable prompts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"description": "Work Time Calculator configuration file. Configuration file location: $XDG_CONFIG_HOME/wtc/config.toml, usually ~/.config/wtc/config.toml"
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
#:schema https://git.korhonen.cc/FunctionalHacker/work-time-calculator/raw/branch/main/config/config-schema.json
|
|
||||||
|
|
||||||
# Work Time Calculator configuration file
|
|
||||||
# This is the default configuration.
|
|
||||||
# You can only partially override the config,
|
|
||||||
# any missing values will use the defaults described here.
|
|
||||||
# On Unix/Linux you can place your configuration file in $XDG_CONFIG_HOME/wtc/config.toml,
|
|
||||||
# usually ~/.config/wtc/config.toml
|
|
||||||
# For windows, I don't know.
|
|
||||||
|
|
||||||
# Comment out or remove if you don't have an unpaid lunch break
|
|
||||||
# or if you normally log your lunch break hours
|
|
||||||
unpaidLunchBreakDuration = "00:30"
|
|
||||||
|
|
||||||
# The language of the application.
|
|
||||||
# Currently supported languages are "en", "fi"
|
|
||||||
language = "en"
|
|
||||||
|
|
||||||
# Time format used to display timestamps (started, stopped etc.)
|
|
||||||
# Refer to the dayjs documentation on how to set this https://day.js.org/docs/en/display/format
|
|
||||||
# For example, the finnish format would be "MM.DD.YYYY [kello] HH.mm"
|
|
||||||
timestampFormat = "YYYY-MM-DD HH:mm"
|
|
||||||
|
|
||||||
# This section is for default values for inputs
|
|
||||||
[defaults]
|
|
||||||
|
|
||||||
# Your work day duration
|
|
||||||
workDayDuration = "07:30"
|
|
||||||
|
|
||||||
# The time you start working
|
|
||||||
startTime = "08:00"
|
|
||||||
|
|
||||||
# The time you stop working. Can either be "now" or a time
|
|
||||||
stopTime = "now"
|
|
||||||
|
|
||||||
# This section can be used to disable prompts for each
|
|
||||||
# of the questions. The default value will be used automatically
|
|
||||||
# if the setting is set to false
|
|
||||||
[askInput]
|
|
||||||
workDayDuration = true
|
|
||||||
startTime = true
|
|
||||||
stopTime = true
|
|
||||||
logged = true
|
|
BIN
img/demo.png
BIN
img/demo.png
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
812
package-lock.json
generated
812
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "work-time-calculator",
|
"name": "work-time-calculator",
|
||||||
"version": "1.0.2",
|
"version": "0.0.2",
|
||||||
"description": "An interactive CLI tool to calculate work time",
|
"description": "An interactive CLI tool to calculate work time",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://git.korhonen.cc/FunctionalHacker/work-time-calculator.git",
|
"url": "https://git/korhonen.cc/FunctionalHacker/work-time-calculator.git",
|
||||||
"type": "git"
|
"type": "git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -13,13 +13,12 @@
|
||||||
"email": "wtc@functionalhacker.korhonen.cc"
|
"email": "wtc@functionalhacker.korhonen.cc"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "src/main.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wtc": "bin/wtc"
|
"wtc": "bin/wtc"
|
||||||
},
|
},
|
||||||
"main": "dist/wtc.mjs",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prod": "rollup -c ./rollup.prod.config.js",
|
"build": "tsc"
|
||||||
"dev": "rollup -c ./rollup.dev.config.js"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"work",
|
"work",
|
||||||
|
@ -28,22 +27,13 @@
|
||||||
],
|
],
|
||||||
"author": "Marko Korhonen <marko@korhonen.cc>",
|
"author": "Marko Korhonen <marko@korhonen.cc>",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
|
||||||
"@rollup/plugin-typescript": "^11.1.5",
|
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@types/yargs": "^17.0.31",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"rollup": "^4.5.1",
|
"typescript": "^5.2.2"
|
||||||
"rollup-plugin-add-shebang": "^0.3.1",
|
|
||||||
"tslib": "^2.6.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10"
|
||||||
"iarna-toml-esm": "^3.0.5",
|
|
||||||
"xdg-basedir": "^5.1.0",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import typescript from '@rollup/plugin-typescript';
|
|
||||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
||||||
import shebang from 'rollup-plugin-add-shebang';
|
|
||||||
|
|
||||||
/** @type {import('rollup').RollupOptions} */
|
|
||||||
const config = {
|
|
||||||
input: 'src/main.ts',
|
|
||||||
output: {
|
|
||||||
format: 'esm',
|
|
||||||
file: 'dist/wtc-dev.mjs',
|
|
||||||
},
|
|
||||||
plugins: [typescript(), nodeResolve({ exportConditions: ['node'] }), shebang({ include: 'dist/wtc-dev.mjs' })],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
|
@ -1,15 +0,0 @@
|
||||||
import typescript from '@rollup/plugin-typescript';
|
|
||||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
||||||
import terser from '@rollup/plugin-terser';
|
|
||||||
|
|
||||||
/** @type {import('rollup').RollupOptions} */
|
|
||||||
const config = {
|
|
||||||
input: 'src/main.ts',
|
|
||||||
output: {
|
|
||||||
format: 'esm',
|
|
||||||
file: 'dist/wtc.mjs',
|
|
||||||
},
|
|
||||||
plugins: [typescript(), nodeResolve({ exportConditions: ['node'] }), terser()],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
|
@ -1,71 +0,0 @@
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { xdgConfig } from 'xdg-basedir';
|
|
||||||
import {parse} from 'iarna-toml-esm';
|
|
||||||
import { parseDuration, parseTimestamp } from './parse.js';
|
|
||||||
import WtcConfig from './types/WtcConfig.js';
|
|
||||||
import Language from './types/Language.js';
|
|
||||||
|
|
||||||
interface RawConfig extends Omit<WtcConfig, 'unpaidLunchBreakDuration' | 'defaults'> {
|
|
||||||
unpaidLunchBreakDuration: string;
|
|
||||||
defaults: {
|
|
||||||
workDayDuration: string;
|
|
||||||
startTime: string;
|
|
||||||
stopTime: string;
|
|
||||||
hadLunch: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultConfig: RawConfig = {
|
|
||||||
language: Language.en,
|
|
||||||
timestampFormat: 'YYYY-MM-DD HH:mm',
|
|
||||||
unpaidLunchBreakDuration: '00:30',
|
|
||||||
defaults: {
|
|
||||||
workDayDuration: '07:30',
|
|
||||||
startTime: '08:00',
|
|
||||||
stopTime: 'now',
|
|
||||||
hadLunch: true,
|
|
||||||
},
|
|
||||||
askInput: {
|
|
||||||
workDayDuration: true,
|
|
||||||
startTime: true,
|
|
||||||
stopTime: true,
|
|
||||||
logged: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getConfig = (): WtcConfig => {
|
|
||||||
const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config');
|
|
||||||
const configFilePath = path.join(configDir, 'wtc', 'config.toml');
|
|
||||||
|
|
||||||
let configData: Partial<RawConfig>;
|
|
||||||
if (fs.existsSync(configFilePath)) {
|
|
||||||
configData = parse(fs.readFileSync(configFilePath, 'utf8')) as unknown as RawConfig;
|
|
||||||
} else {
|
|
||||||
configData = defaultConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { language, timestampFormat, unpaidLunchBreakDuration, defaults, askInput } = configData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
language: language ?? defaultConfig.language,
|
|
||||||
timestampFormat: timestampFormat ?? defaultConfig.timestampFormat,
|
|
||||||
unpaidLunchBreakDuration: !unpaidLunchBreakDuration
|
|
||||||
? undefined
|
|
||||||
: parseDuration(unpaidLunchBreakDuration),
|
|
||||||
defaults: {
|
|
||||||
workDayDuration: parseDuration(defaults?.workDayDuration ?? defaultConfig.defaults.workDayDuration),
|
|
||||||
startTime: parseTimestamp(defaults?.startTime ?? defaultConfig.defaults.startTime),
|
|
||||||
stopTime: parseTimestamp(defaults?.stopTime ?? defaultConfig.defaults.stopTime),
|
|
||||||
hadLunch: defaults?.hadLunch ?? defaultConfig.defaults.hadLunch,
|
|
||||||
},
|
|
||||||
askInput: {
|
|
||||||
workDayDuration: askInput?.workDayDuration ?? defaultConfig.askInput.workDayDuration,
|
|
||||||
startTime: askInput?.startTime ?? defaultConfig.askInput.startTime,
|
|
||||||
stopTime: askInput?.stopTime ?? defaultConfig.askInput.stopTime,
|
|
||||||
logged: askInput?.logged ?? defaultConfig.askInput.logged,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getConfig;
|
|
10
src/dayjs.ts
10
src/dayjs.ts
|
@ -1,10 +0,0 @@
|
||||||
import dayjs, {Dayjs} from 'dayjs/esm';
|
|
||||||
import duration, {Duration} from 'dayjs/esm/plugin/duration';
|
|
||||||
import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
|
||||||
dayjs.extend(customParseFormat);
|
|
||||||
|
|
||||||
export default dayjs;
|
|
||||||
|
|
||||||
export type {Dayjs, Duration};
|
|
|
@ -1,43 +1,28 @@
|
||||||
import dayjs, { Dayjs } from './dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { Duration } from 'dayjs/plugin/duration.js';
|
import { Duration } from 'dayjs/plugin/duration.js';
|
||||||
import Language from './types/Language';
|
|
||||||
import { MessageKey, message } from './i18n';
|
export const formatTimestamp = (timestamp: Dayjs): string => timestamp.format('YYYY-MM-DD HH:mm');
|
||||||
|
|
||||||
export const formatTime = (time: Dayjs): string => time.format('HH:mm');
|
export const formatTime = (time: Dayjs): string => time.format('HH:mm');
|
||||||
|
|
||||||
export const formatDuration =
|
export const formatDuration = (duration: Duration, short?: boolean): string => {
|
||||||
(language: Language) =>
|
if (duration.hours() === 0 && duration.minutes() === 0) {
|
||||||
(duration?: Duration, short?: boolean): string => {
|
return 'none';
|
||||||
duration = duration ?? dayjs.duration(0, 'minutes');
|
}
|
||||||
if (duration.hours() === 0 && duration.minutes() === 0) {
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatString;
|
const formatString = short
|
||||||
|
? 'HH:mm'
|
||||||
|
: duration.hours() > 0 && duration.minutes() > 0
|
||||||
|
? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]`
|
||||||
|
: duration.hours() > 0
|
||||||
|
? `H [hour${duration.hours() > 1 ? 's' : ''}]`
|
||||||
|
: `m [minute${duration.minutes() > 1 ? 's' : ''}]`;
|
||||||
|
|
||||||
if (short) {
|
return duration.format(formatString);
|
||||||
formatString = 'HH:mm';
|
};
|
||||||
} else if (language === Language.fi) {
|
|
||||||
formatString =
|
|
||||||
duration.hours() > 0 && duration.minutes() > 0
|
|
||||||
? `H [tunti${duration.hours() > 1 ? 'a' : ''} ja] m [minuuttia]`
|
|
||||||
: duration.hours() > 0
|
|
||||||
? `H [tunti${duration.hours() > 1 ? 'a' : ''}]`
|
|
||||||
: 'm [minuuttia]';
|
|
||||||
} else {
|
|
||||||
formatString =
|
|
||||||
duration.hours() > 0 && duration.minutes() > 0
|
|
||||||
? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]`
|
|
||||||
: duration.hours() > 0
|
|
||||||
? `H [hour${duration.hours() > 1 ? 's' : ''}]`
|
|
||||||
: `m [minute${duration.minutes() > 1 ? 's' : ''}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return duration.format(formatString);
|
export const getHoursRoundedStr = (duration: Duration) =>
|
||||||
};
|
`(${getHoursRounded(duration)} as hours rounded to next even 15 minutes)`;
|
||||||
|
|
||||||
export const getHoursRoundedStr = (language: Language) => (duration: Duration) =>
|
|
||||||
`(${getHoursRounded(duration)} ${message(language)(MessageKey.hoursRounded)})`;
|
|
||||||
|
|
||||||
const getHoursRounded = (duration: Duration) => {
|
const getHoursRounded = (duration: Duration) => {
|
||||||
// Round up to the next multiple of 15
|
// Round up to the next multiple of 15
|
||||||
|
|
150
src/i18n.ts
150
src/i18n.ts
|
@ -1,150 +0,0 @@
|
||||||
import Language from './types/Language';
|
|
||||||
|
|
||||||
export enum MessageKey {
|
|
||||||
cliHelp,
|
|
||||||
cliVersion,
|
|
||||||
promptWorkDayDuration,
|
|
||||||
excludingLunch,
|
|
||||||
promptStartTime,
|
|
||||||
promptStopTime,
|
|
||||||
parseTimeFailed,
|
|
||||||
startTimeBeforeStopTimeError,
|
|
||||||
promptLunchBreak,
|
|
||||||
promptYesNoYes,
|
|
||||||
promptYesNoNo,
|
|
||||||
unpaidLunch,
|
|
||||||
promptLogged,
|
|
||||||
none,
|
|
||||||
startedWorking,
|
|
||||||
stoppedWorking,
|
|
||||||
workedToday,
|
|
||||||
loggedOver,
|
|
||||||
workedOvertime,
|
|
||||||
workLeft,
|
|
||||||
hoursCalculated,
|
|
||||||
klo,
|
|
||||||
unloggedToday,
|
|
||||||
hoursRounded,
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages: Record<MessageKey, Record<Language, string>> = {
|
|
||||||
[MessageKey.cliHelp]: {
|
|
||||||
[Language.en]: 'Show this help',
|
|
||||||
[Language.fi]: 'Näytä tämä ohje',
|
|
||||||
},
|
|
||||||
[MessageKey.cliVersion]: {
|
|
||||||
[Language.en]: 'Show program version',
|
|
||||||
[Language.fi]: 'Näytä ohjelman versio',
|
|
||||||
},
|
|
||||||
[MessageKey.promptWorkDayDuration]: {
|
|
||||||
[Language.en]: 'How long is your work day today{0}? [{1}]: ',
|
|
||||||
[Language.fi]: 'Kuinka pitkä työpäiväsi on tänään{0}? [{1}]: ',
|
|
||||||
},
|
|
||||||
[MessageKey.excludingLunch]: {
|
|
||||||
[Language.en]: ', excluding the lunch break',
|
|
||||||
[Language.fi]: ', poisluettuna lounastauko',
|
|
||||||
},
|
|
||||||
[MessageKey.promptStartTime]: {
|
|
||||||
[Language.en]: 'What time did you start work today? [{0}]: ',
|
|
||||||
[Language.fi]: 'Mihin aikaan aloitit työskentelyn tänään? [{0}]: ',
|
|
||||||
},
|
|
||||||
[MessageKey.promptStopTime]: {
|
|
||||||
[Language.en]: "What time did you stop working? If you didn't stop yet, leave this empty: ",
|
|
||||||
[Language.fi]: 'Mihin aikaan lopetit työskentelyn? Jos et lopettanut vielä, jätä tämä tyhjäksi: ',
|
|
||||||
},
|
|
||||||
[MessageKey.parseTimeFailed]: {
|
|
||||||
[Language.en]: 'Failed to parse time "{0}", using default value "{1}"',
|
|
||||||
[Language.fi]: 'Ajan "{0}" parsiminen epäonnistui, käytetään oletusasetusta "{1}"',
|
|
||||||
},
|
|
||||||
[MessageKey.startTimeBeforeStopTimeError]: {
|
|
||||||
[Language.en]: 'Start time ({0}) needs to be before stop time ({1}). Exiting',
|
|
||||||
[Language.fi]: 'Aloitusaika ({0}) pitää olla ennen lopetusaikaa ({1}). Ohjelma sammuu',
|
|
||||||
},
|
|
||||||
[MessageKey.promptLunchBreak]: {
|
|
||||||
[Language.en]: 'Did you have a lunch break? [{0}]: ',
|
|
||||||
[Language.fi]: 'Piditkö jo lounastauon? [{0}]: ',
|
|
||||||
},
|
|
||||||
[MessageKey.promptYesNoYes]: {
|
|
||||||
[Language.en]: 'Y/n',
|
|
||||||
[Language.fi]: 'K/e',
|
|
||||||
},
|
|
||||||
[MessageKey.promptYesNoNo]: {
|
|
||||||
[Language.en]: 'y/N',
|
|
||||||
[Language.fi]: 'k/E',
|
|
||||||
},
|
|
||||||
[MessageKey.unpaidLunch]: {
|
|
||||||
[Language.en]: 'Unpaid lunch:',
|
|
||||||
[Language.fi]: 'Palkaton lounas:',
|
|
||||||
},
|
|
||||||
[MessageKey.promptLogged]: {
|
|
||||||
[Language.en]: 'How many hours did you log already? [00:00] ',
|
|
||||||
[Language.fi]: 'Kuinka monta tuntia kirjasit jo? [00:00] ',
|
|
||||||
},
|
|
||||||
[MessageKey.none]: {
|
|
||||||
[Language.en]: 'None',
|
|
||||||
[Language.fi]: 'Ei yhtään',
|
|
||||||
},
|
|
||||||
[MessageKey.startedWorking]: {
|
|
||||||
[Language.en]: 'Started working:',
|
|
||||||
[Language.fi]: 'Aloitit työskentelyn:',
|
|
||||||
},
|
|
||||||
[MessageKey.stoppedWorking]: {
|
|
||||||
[Language.en]: 'Stopped working',
|
|
||||||
[Language.fi]: 'Lopetit työskentelyn',
|
|
||||||
},
|
|
||||||
[MessageKey.hoursCalculated]: {
|
|
||||||
[Language.en]: 'Hours calculated',
|
|
||||||
[Language.fi]: 'Tunnit laskettu',
|
|
||||||
},
|
|
||||||
[MessageKey.workedToday]: {
|
|
||||||
[Language.en]: 'Worked today:',
|
|
||||||
[Language.fi]: 'Tänään työskennelty:',
|
|
||||||
},
|
|
||||||
[MessageKey.workedOvertime]: {
|
|
||||||
[Language.en]: 'You worked {0} overtime today',
|
|
||||||
[Language.fi]: 'Olet tehnyt {0} ylitöitä tänään',
|
|
||||||
},
|
|
||||||
[MessageKey.loggedOver]: {
|
|
||||||
[Language.en]: 'You have logged {0} more than you worked today!',
|
|
||||||
[Language.fi]: 'Olet kirjannut {0} enemmän kuin olet työskennellyt!',
|
|
||||||
},
|
|
||||||
[MessageKey.workLeft]: {
|
|
||||||
[Language.en]: 'You still have to work {0} more today',
|
|
||||||
[Language.fi]: 'Sinun pitää työskennellä tänään vielä {0} lisää',
|
|
||||||
},
|
|
||||||
[MessageKey.klo]: {
|
|
||||||
[Language.en]: 'at',
|
|
||||||
[Language.fi]: 'klo',
|
|
||||||
},
|
|
||||||
[MessageKey.unloggedToday]: {
|
|
||||||
[Language.en]: 'Unlogged today:',
|
|
||||||
[Language.fi]: 'Kirjaamattomia tänään:',
|
|
||||||
},
|
|
||||||
[MessageKey.hoursRounded]: {
|
|
||||||
[Language.en]: 'as hours rounded to next even 15 minutes',
|
|
||||||
[Language.fi]: 'tunteina pyöristettynä seuraavaan 15 minuuttiin',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a function to fetch messages for a given language
|
|
||||||
* @param language The language to get the messages for
|
|
||||||
*/
|
|
||||||
export const message =
|
|
||||||
(language: Language) =>
|
|
||||||
/**
|
|
||||||
* Get a message for a fiven key
|
|
||||||
* @param key The key of the message
|
|
||||||
*/
|
|
||||||
(key: keyof typeof messages, ...params: string[]) => {
|
|
||||||
let result = messages[key][language];
|
|
||||||
if (!result) {
|
|
||||||
throw `Unknown language: ${language}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace parameters in the template
|
|
||||||
for (let i = 0; i < params.length; i++) {
|
|
||||||
result = result.replace(new RegExp(`\\{${i}\\}`, 'g'), params[i]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
146
src/input.ts
146
src/input.ts
|
@ -1,146 +0,0 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
import { parseDuration, parseTimestamp } from './parse';
|
|
||||||
import * as readline from 'readline/promises';
|
|
||||||
import { formatDuration, formatTime } from './format';
|
|
||||||
import { Dayjs } from 'dayjs';
|
|
||||||
import { WtcPromptResult } from './types/WtcPromptResult';
|
|
||||||
import { WtcRuntimeConfig } from './types/WtcConfig';
|
|
||||||
import { MessageKey } from './i18n';
|
|
||||||
import dayjs, { Duration } from './dayjs';
|
|
||||||
|
|
||||||
const { error } = console;
|
|
||||||
|
|
||||||
const input = async (runtimeCfg: WtcRuntimeConfig): Promise<WtcPromptResult> => {
|
|
||||||
const { config, msg } = runtimeCfg;
|
|
||||||
const fmtDuration = formatDuration(config.language);
|
|
||||||
const { defaults, askInput, unpaidLunchBreakDuration: lunchBreakDuration } = config;
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
});
|
|
||||||
|
|
||||||
let startedAt: Dayjs | undefined = undefined;
|
|
||||||
let stoppedAt: Dayjs | undefined = undefined;
|
|
||||||
let stoppedWorking = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get work day duration
|
|
||||||
let workDayDuration: Duration | undefined = undefined;
|
|
||||||
|
|
||||||
if (askInput.workDayDuration) {
|
|
||||||
const durationAnswer = await rl.question(
|
|
||||||
msg(
|
|
||||||
MessageKey.promptWorkDayDuration,
|
|
||||||
config.unpaidLunchBreakDuration ? msg(MessageKey.excludingLunch) : '',
|
|
||||||
fmtDuration(defaults.workDayDuration, true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (durationAnswer !== '') {
|
|
||||||
workDayDuration = parseDuration(durationAnswer);
|
|
||||||
if (workDayDuration.asMinutes() <= 0) {
|
|
||||||
error(
|
|
||||||
chalk.red(
|
|
||||||
msg(MessageKey.parseTimeFailed, durationAnswer, fmtDuration(defaults.workDayDuration)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
workDayDuration = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!workDayDuration) {
|
|
||||||
workDayDuration = defaults.workDayDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (askInput.startTime) {
|
|
||||||
const startTimeAnswer = await rl.question(msg(MessageKey.promptStartTime, formatTime(defaults.startTime)));
|
|
||||||
if (startTimeAnswer !== '') {
|
|
||||||
startedAt = parseTimestamp(startTimeAnswer);
|
|
||||||
if (!startedAt.isValid()) {
|
|
||||||
error(chalk.red(msg(MessageKey.parseTimeFailed, startTimeAnswer, formatTime(defaults.startTime))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startedAt?.isValid()) {
|
|
||||||
startedAt = defaults.startTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (askInput.stopTime) {
|
|
||||||
const stoppedAnswer = await rl.question(msg(MessageKey.promptStopTime, formatTime(defaults.stopTime)));
|
|
||||||
|
|
||||||
if (stoppedAnswer !== '') {
|
|
||||||
stoppedWorking = true;
|
|
||||||
stoppedAt = parseTimestamp(stoppedAnswer);
|
|
||||||
if (!stoppedAt.isValid()) {
|
|
||||||
error(chalk.red(msg(MessageKey.parseTimeFailed, stoppedAnswer, formatTime(defaults.stopTime))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stoppedAt) {
|
|
||||||
stoppedAt = defaults.stopTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) {
|
|
||||||
error(
|
|
||||||
chalk.red(msg(MessageKey.startTimeBeforeStopTimeError, formatTime(startedAt), formatTime(stoppedAt))),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let worked = dayjs.duration(stoppedAt.diff(startedAt));
|
|
||||||
|
|
||||||
let hadLunch = false;
|
|
||||||
if (lunchBreakDuration) {
|
|
||||||
const lunchAnswer = (
|
|
||||||
await rl.question(
|
|
||||||
msg(
|
|
||||||
MessageKey.promptLunchBreak,
|
|
||||||
msg(config.defaults.hadLunch ? MessageKey.promptYesNoYes : MessageKey.promptYesNoNo),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (
|
|
||||||
lunchAnswer === 'y' ||
|
|
||||||
lunchAnswer === 'k' ||
|
|
||||||
(config.defaults.hadLunch && lunchAnswer !== 'n' && lunchAnswer !== 'e')
|
|
||||||
) {
|
|
||||||
hadLunch = true;
|
|
||||||
worked = worked.subtract(lunchBreakDuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate unlogged time
|
|
||||||
let loggedAnswer = await rl.question(msg(MessageKey.promptLogged));
|
|
||||||
if (loggedAnswer === '') {
|
|
||||||
loggedAnswer = '00:00';
|
|
||||||
}
|
|
||||||
const logged = parseDuration(loggedAnswer);
|
|
||||||
const unLogged = worked.subtract(logged);
|
|
||||||
const workLeft = workDayDuration.subtract(worked);
|
|
||||||
const workLeftMinutes = workLeft.asMinutes();
|
|
||||||
let workedOvertime: Duration | undefined;
|
|
||||||
|
|
||||||
if (workLeftMinutes < 0) {
|
|
||||||
workedOvertime = dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
logged,
|
|
||||||
unLogged,
|
|
||||||
startedAt,
|
|
||||||
stoppedAt,
|
|
||||||
stoppedWorking,
|
|
||||||
hadLunch,
|
|
||||||
worked,
|
|
||||||
workLeft,
|
|
||||||
workedOvertime,
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default input;
|
|
182
src/main.ts
182
src/main.ts
|
@ -1,38 +1,150 @@
|
||||||
import yargs from 'yargs';
|
import chalk from 'chalk';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import ui from './ui.js';
|
import * as readline from 'readline/promises';
|
||||||
import update from './update.js';
|
import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js';
|
||||||
import getConfig from './config.js';
|
import duration, { Duration } from 'dayjs/plugin/duration.js';
|
||||||
import { MessageKey, message } from './i18n.js';
|
import { parseDuration, parseTimestamp } from './parse.js';
|
||||||
import { WtcRuntimeConfig } from './types/WtcConfig.js';
|
|
||||||
|
|
||||||
// Build runtime config
|
dayjs.extend(duration);
|
||||||
const config = getConfig();
|
|
||||||
const msg = message(config.language);
|
const { log, error } = console;
|
||||||
const runtimeConfig: WtcRuntimeConfig = {
|
const defaultStartTime = '08:00';
|
||||||
config,
|
const lunchBreakDuration = dayjs.duration({ minutes: 30 });
|
||||||
msg,
|
const defaultWorkDayDuration = dayjs.duration({ hours: 7, minutes: 30 });
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
let startedAt: Dayjs | undefined = undefined;
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get work day duration
|
||||||
|
let workDayDuration: Duration | undefined = undefined;
|
||||||
|
|
||||||
|
const durationAnswer = await rl.question(
|
||||||
|
`How long is your work day today, excluding the lunch break? [${formatDuration(
|
||||||
|
defaultWorkDayDuration,
|
||||||
|
true,
|
||||||
|
)}] `,
|
||||||
|
);
|
||||||
|
if (durationAnswer !== '') {
|
||||||
|
workDayDuration = parseDuration(durationAnswer);
|
||||||
|
if (workDayDuration.asMinutes() <= 0) {
|
||||||
|
error(
|
||||||
|
chalk.red(
|
||||||
|
`Failed to parse ${durationAnswer} to duration, using default work day duration ${defaultWorkDayDuration}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
workDayDuration = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workDayDuration) {
|
||||||
|
workDayDuration = defaultWorkDayDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate worked time
|
||||||
|
const startTimeAnswer = await rl.question(`What time did you start work today? [${defaultStartTime}] `);
|
||||||
|
if (startTimeAnswer !== '') {
|
||||||
|
startedAt = parseTimestamp(startTimeAnswer);
|
||||||
|
if (!startedAt.isValid()) {
|
||||||
|
error(
|
||||||
|
chalk.red(
|
||||||
|
`Failed to parse ${startTimeAnswer} to time, using default start time ${defaultStartTime}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startedAt?.isValid()) {
|
||||||
|
startedAt = parseTimestamp(defaultStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stoppedWorking = false;
|
||||||
|
let stoppedAt: Dayjs | undefined = undefined;
|
||||||
|
const stoppedAnswer = await rl.question(
|
||||||
|
`What time did you stop working (default is current time if you didn't stop yet)? [${formatTime(now)}] `,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stoppedAnswer === '') {
|
||||||
|
stoppedAt = now;
|
||||||
|
} else {
|
||||||
|
stoppedWorking = true;
|
||||||
|
stoppedAt = parseTimestamp(stoppedAnswer);
|
||||||
|
if (!stoppedAt.isValid()) {
|
||||||
|
error(`Failed to parse ${stoppedAnswer} to time, using current time`);
|
||||||
|
stoppedAt = dayjs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) {
|
||||||
|
error(
|
||||||
|
chalk.red(
|
||||||
|
`Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime(
|
||||||
|
stoppedAt,
|
||||||
|
)}). Exiting`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let worked = dayjs.duration(stoppedAt.diff(startedAt));
|
||||||
|
|
||||||
|
if ((await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n') {
|
||||||
|
worked = worked.subtract(lunchBreakDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate unlogged time
|
||||||
|
let loggedAnswer = await rl.question('How many hours did you log already? [00:00] ');
|
||||||
|
if (loggedAnswer === '') {
|
||||||
|
loggedAnswer = '00:00';
|
||||||
|
}
|
||||||
|
const logged = parseDuration(loggedAnswer);
|
||||||
|
let unLogged: Duration | undefined = undefined;
|
||||||
|
|
||||||
|
if (logged.asMinutes() === worked.asMinutes()) {
|
||||||
|
unLogged = worked.subtract(logged);
|
||||||
|
} else {
|
||||||
|
unLogged = worked.subtract(logged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log result
|
||||||
|
log();
|
||||||
|
log('Started working at:', formatTimestamp(startedAt));
|
||||||
|
log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt));
|
||||||
|
log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked)));
|
||||||
|
|
||||||
|
if (unLogged.asMinutes() == 0) {
|
||||||
|
log('Unlogged today:', chalk.green('none'));
|
||||||
|
} else if (unLogged.asMinutes() > 0) {
|
||||||
|
log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged)));
|
||||||
|
} else if (unLogged.asMinutes() < 0) {
|
||||||
|
log(
|
||||||
|
chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`),
|
||||||
|
chalk.yellow(getHoursRoundedStr(unLogged)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workLeft = workDayDuration.subtract(worked);
|
||||||
|
const workLeftMinutes = workLeft.asMinutes();
|
||||||
|
if (workLeftMinutes > 0) {
|
||||||
|
log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today');
|
||||||
|
} else if (workLeft.asMinutes() < 0) {
|
||||||
|
log(
|
||||||
|
'You worked',
|
||||||
|
chalk.green(
|
||||||
|
formatDuration(dayjs.duration({ minutes: Math.round(workLeftMinutes * -1) })),
|
||||||
|
'overtime today',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process args. Yargs will exit if it detects help or version
|
main();
|
||||||
const args = await yargs(hideBin(process.argv))
|
|
||||||
.usage('Work time calculator')
|
|
||||||
.alias('help', 'h')
|
|
||||||
.alias('version', 'v')
|
|
||||||
.options({
|
|
||||||
help: {
|
|
||||||
description: msg(MessageKey.cliHelp),
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
description: msg(MessageKey.cliVersion),
|
|
||||||
},
|
|
||||||
}).argv;
|
|
||||||
|
|
||||||
// Run updater if requested
|
|
||||||
if (args.update) {
|
|
||||||
update();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run UI if no arguments
|
|
||||||
ui(runtimeConfig);
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
import { formatDuration, getHoursRoundedStr } from './format';
|
|
||||||
import { WtcPromptResult } from './types/WtcPromptResult';
|
|
||||||
import { MessageKey } from './i18n.js';
|
|
||||||
import { WtcRuntimeConfig } from './types/WtcConfig';
|
|
||||||
import dayjs from './dayjs';
|
|
||||||
|
|
||||||
const { log } = console;
|
|
||||||
|
|
||||||
const output = (result: WtcPromptResult, runtimeCfg: WtcRuntimeConfig) => {
|
|
||||||
const {config, msg} = runtimeCfg;
|
|
||||||
const { language, timestampFormat } = config;
|
|
||||||
const fmtDuration = formatDuration(language);
|
|
||||||
const hoursRounded = getHoursRoundedStr(language);
|
|
||||||
const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime, hadLunch } = result;
|
|
||||||
|
|
||||||
log();
|
|
||||||
log(msg(MessageKey.startedWorking), startedAt.format(timestampFormat));
|
|
||||||
log(
|
|
||||||
(stoppedWorking ? msg(MessageKey.stoppedWorking) : msg(MessageKey.hoursCalculated)) +
|
|
||||||
` ${msg(MessageKey.klo)}:`,
|
|
||||||
stoppedAt.format(timestampFormat),
|
|
||||||
);
|
|
||||||
log(msg(MessageKey.workedToday), chalk.green(fmtDuration(worked)), chalk.yellow(hoursRounded(worked)));
|
|
||||||
|
|
||||||
if (hadLunch) {
|
|
||||||
log(msg(MessageKey.unpaidLunch), chalk.green(fmtDuration(config.unpaidLunchBreakDuration)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const unLoggedMinutes = unLogged.asMinutes();
|
|
||||||
if (unLoggedMinutes >= 0) {
|
|
||||||
log(
|
|
||||||
msg(MessageKey.unloggedToday),
|
|
||||||
unLoggedMinutes === 0 ? chalk.green(msg(MessageKey.none)) : chalk.red(fmtDuration(unLogged)),
|
|
||||||
unLoggedMinutes === 0 ? '' : chalk.yellow(hoursRounded(unLogged)),
|
|
||||||
);
|
|
||||||
} else if (unLoggedMinutes < 0) {
|
|
||||||
const overLogged = dayjs.duration(Math.abs(unLogged.asMilliseconds()), 'milliseconds');
|
|
||||||
log(chalk.red(msg(MessageKey.loggedOver, fmtDuration(overLogged))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workLeft.asMinutes() > 0) {
|
|
||||||
log(msg(MessageKey.workLeft, chalk.green(fmtDuration(workLeft))));
|
|
||||||
} else if (workedOvertime) {
|
|
||||||
log(msg(MessageKey.workedOvertime, chalk.green(fmtDuration(workedOvertime))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default output;
|
|
|
@ -1,6 +1,11 @@
|
||||||
import dayjs, { Dayjs, Duration } from './dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
|
||||||
|
import duration, { Duration } from 'dayjs/plugin/duration.js';
|
||||||
|
|
||||||
export const parseTimestamp = (time: string): Dayjs => (time === 'now' ? dayjs() : dayjs(time, 'HH:mm', true));
|
dayjs.extend(customParseFormat);
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export const parseTimestamp = (time: string): Dayjs => dayjs(time, 'HH:mm', true);
|
||||||
|
|
||||||
export const parseDuration = (time: string): Duration => {
|
export const parseDuration = (time: string): Duration => {
|
||||||
const [hours, minutes] = time.split(':').map(Number);
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
enum Language {
|
|
||||||
en = 'en',
|
|
||||||
fi = 'fi',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Language;
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { Dayjs } from 'dayjs';
|
|
||||||
import { Duration } from 'dayjs/plugin/duration.js';
|
|
||||||
import Language from './Language.js';
|
|
||||||
import { message } from '../i18n.js';
|
|
||||||
|
|
||||||
export default interface WtcConfig {
|
|
||||||
language: Language,
|
|
||||||
timestampFormat: string,
|
|
||||||
unpaidLunchBreakDuration?: Duration;
|
|
||||||
defaults: {
|
|
||||||
workDayDuration: Duration;
|
|
||||||
startTime: Dayjs;
|
|
||||||
stopTime: Dayjs;
|
|
||||||
hadLunch: boolean;
|
|
||||||
};
|
|
||||||
askInput: {
|
|
||||||
workDayDuration: boolean;
|
|
||||||
startTime: boolean;
|
|
||||||
stopTime: boolean;
|
|
||||||
logged: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Config and current language msg function together */
|
|
||||||
export interface WtcRuntimeConfig {
|
|
||||||
config: WtcConfig;
|
|
||||||
msg: ReturnType<typeof message>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Dayjs } from 'dayjs';
|
|
||||||
import { Duration } from 'dayjs/plugin/duration';
|
|
||||||
|
|
||||||
export interface WtcPromptResult {
|
|
||||||
startedAt: Dayjs;
|
|
||||||
stoppedAt: Dayjs;
|
|
||||||
stoppedWorking: boolean;
|
|
||||||
logged: Duration;
|
|
||||||
unLogged: Duration;
|
|
||||||
hadLunch: boolean;
|
|
||||||
worked: Duration;
|
|
||||||
workLeft: Duration;
|
|
||||||
workedOvertime?: Duration;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import input from './input.js';
|
|
||||||
import output from './output.js';
|
|
||||||
import { WtcRuntimeConfig } from './types/WtcConfig.js';
|
|
||||||
|
|
||||||
const ui = async (config: WtcRuntimeConfig) => output(await input(config), config);
|
|
||||||
|
|
||||||
export default ui;
|
|
|
@ -1,7 +0,0 @@
|
||||||
const { log } = console;
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
log('update');
|
|
||||||
};
|
|
||||||
|
|
||||||
export default update;
|
|
|
@ -8,7 +8,7 @@
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "dist"
|
"outDir": "target"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue