From ac1e9d79619254a8c3ada6908f7d94660e9c1c1a Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 25 Mar 2026 10:46:25 +1300 Subject: [PATCH 1/3] refactor: replace react based pdf rendering with pdfmake layout engine --- esbuild.config.js | 50 +- package-lock.json | 546 ++++-------------- package.json | 3 +- src/components/pdf/TimesheetPdf.tsx | 81 --- .../pdf/TimesheetPdfDetailField.tsx | 24 - src/components/pdf/TimesheetPdfTable.tsx | 75 --- src/components/pdf/TimesheetPdfTableRow.tsx | 69 --- src/components/pdf/index.ts | 55 +- src/components/pdf/renderer.ts | 377 ++++++++++++ src/components/pdf/styles.ts | 119 ---- src/timekeep/create.test.ts | 45 +- src/timekeep/parser.test.ts | 10 +- src/timekeep/queries.test.ts | 95 ++- src/timekeep/sort.test.ts | 25 +- src/timekeep/start.test.ts | 39 +- src/timekeep/update.test.ts | 59 +- 16 files changed, 622 insertions(+), 1050 deletions(-) delete mode 100644 src/components/pdf/TimesheetPdf.tsx delete mode 100644 src/components/pdf/TimesheetPdfDetailField.tsx delete mode 100644 src/components/pdf/TimesheetPdfTable.tsx delete mode 100644 src/components/pdf/TimesheetPdfTableRow.tsx create mode 100644 src/components/pdf/renderer.ts delete mode 100644 src/components/pdf/styles.ts diff --git a/esbuild.config.js b/esbuild.config.js index ce5b824..8f9c355 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -3,30 +3,30 @@ const process = require("process"); const builtins = require("builtin-modules"); -const prod = (process.argv[2] === "production"); - +const prod = process.argv[2] === "production"; module.exports = { - platform: "browser", - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins], - format: "cjs", - target: "es2018", - sourcemap: prod ? false : "inline", - treeShaking: true, - loader: { '.ttf': 'dataurl' }, -} \ No newline at end of file + platform: "browser", + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2018", + sourcemap: prod ? false : "inline", + treeShaking: true, + loader: { ".ttf": "dataurl" }, +}; diff --git a/package-lock.json b/package-lock.json index 3b0a1b1..7c9576f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "1.16.0", "license": "MIT", "dependencies": { - "@react-pdf/renderer": "^4.3.2", "moment": "^2.30.1", + "pdfmake": "^0.3.7", "react": "^19.2.4", "react-dom": "^19.2.4", "uuid": "^11.0.5", @@ -19,6 +19,7 @@ "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^16.11.6", + "@types/pdfmake": "^0.3.2", "@types/react": "^19.2.4", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "8.57.2", @@ -496,15 +497,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1728,6 +1720,30 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1752,174 +1768,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@react-pdf/fns": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz", - "integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==", - "license": "MIT" - }, - "node_modules/@react-pdf/font": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.4.tgz", - "integrity": "sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==", - "license": "MIT", - "dependencies": { - "@react-pdf/pdfkit": "^4.1.0", - "@react-pdf/types": "^2.9.2", - "fontkit": "^2.0.2", - "is-url": "^1.2.4" - } - }, - "node_modules/@react-pdf/image": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.4.tgz", - "integrity": "sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==", - "license": "MIT", - "dependencies": { - "@react-pdf/png-js": "^3.0.0", - "jay-peg": "^1.1.1" - } - }, - "node_modules/@react-pdf/layout": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.2.tgz", - "integrity": "sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.2", - "@react-pdf/image": "^3.0.4", - "@react-pdf/primitives": "^4.1.1", - "@react-pdf/stylesheet": "^6.1.2", - "@react-pdf/textkit": "^6.1.0", - "@react-pdf/types": "^2.9.2", - "emoji-regex-xs": "^1.0.0", - "queue": "^6.0.1", - "yoga-layout": "^3.2.1" - } - }, - "node_modules/@react-pdf/pdfkit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.1.0.tgz", - "integrity": "sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@react-pdf/png-js": "^3.0.0", - "browserify-zlib": "^0.2.0", - "crypto-js": "^4.2.0", - "fontkit": "^2.0.2", - "jay-peg": "^1.1.1", - "linebreak": "^1.1.0", - "vite-compatible-readable-stream": "^3.6.1" - } - }, - "node_modules/@react-pdf/png-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz", - "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==", - "license": "MIT", - "dependencies": { - "browserify-zlib": "^0.2.0" - } - }, - "node_modules/@react-pdf/primitives": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz", - "integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==", - "license": "MIT" - }, - "node_modules/@react-pdf/reconciler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz", - "integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "scheduler": "0.25.0-rc-603e6108-20241029" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@react-pdf/render": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.2.tgz", - "integrity": "sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@react-pdf/fns": "3.1.2", - "@react-pdf/primitives": "^4.1.1", - "@react-pdf/textkit": "^6.1.0", - "@react-pdf/types": "^2.9.2", - "abs-svg-path": "^0.1.1", - "color-string": "^1.9.1", - "normalize-svg-path": "^1.1.0", - "parse-svg-path": "^0.1.2", - "svg-arc-to-cubic-bezier": "^3.2.0" - } - }, - "node_modules/@react-pdf/renderer": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.2.tgz", - "integrity": "sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@react-pdf/fns": "3.1.2", - "@react-pdf/font": "^4.0.4", - "@react-pdf/layout": "^4.4.2", - "@react-pdf/pdfkit": "^4.1.0", - "@react-pdf/primitives": "^4.1.1", - "@react-pdf/reconciler": "^2.0.0", - "@react-pdf/render": "^4.3.2", - "@react-pdf/types": "^2.9.2", - "events": "^3.3.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "queue": "^6.0.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@react-pdf/stylesheet": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.2.tgz", - "integrity": "sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.2", - "@react-pdf/types": "^2.9.2", - "color-string": "^1.9.1", - "hsl-to-hex": "^1.0.0", - "media-engine": "^1.0.3", - "postcss-value-parser": "^4.1.0" - } - }, - "node_modules/@react-pdf/textkit": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.1.0.tgz", - "integrity": "sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.2", - "bidi-js": "^1.0.2", - "hyphen": "^1.6.4", - "unicode-properties": "^1.4.1" - } - }, - "node_modules/@react-pdf/types": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.2.tgz", - "integrity": "sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==", - "license": "MIT", - "dependencies": { - "@react-pdf/font": "^4.0.4", - "@react-pdf/primitives": "^4.1.1", - "@react-pdf/stylesheet": "^6.1.2" - } - }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -2090,6 +1938,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pdfkit": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.5.tgz", + "integrity": "sha512-T3ZHnvF91HsEco5ClhBCOuBwobZfPcI2jaiSHybkkKYq4KhVIIurod94JVKvDIG0JXT6o3KiERC0X0//m8dyrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfmake": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.3.2.tgz", + "integrity": "sha512-2TZSL8puKJs/rHvMV1b8BhHD+qYyV9da8mVY83/x7ZR/NaEPXbm3+t5SwkwaH6QAIhY1zQVAaFDhHWL0haMstA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/pdfkit": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -2653,12 +2522,6 @@ "win32" ] }, - "node_modules/abs-svg-path": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", - "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", - "license": "MIT" - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2926,15 +2789,6 @@ "node": ">=6.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -2957,15 +2811,6 @@ "base64-js": "^1.1.2" } }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "license": "MIT", - "dependencies": { - "pako": "~1.0.5" - } - }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -3272,18 +3117,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3321,12 +3157,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3435,12 +3265,6 @@ "dev": true, "license": "MIT" }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "license": "MIT" - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3762,15 +3586,6 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4141,21 +3956,6 @@ "node": ">=8" } }, - "node_modules/hsl-to-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", - "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", - "license": "MIT", - "dependencies": { - "hsl-to-rgb-for-reals": "^1.1.0" - } - }, - "node_modules/hsl-to-rgb-for-reals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", - "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", - "license": "ISC" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4173,12 +3973,6 @@ "node": ">=10.17.0" } }, - "node_modules/hyphen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", - "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", - "license": "ISC" - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -4235,6 +4029,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/is-arrayish": { @@ -4302,12 +4097,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4402,15 +4191,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jay-peg": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", - "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", - "license": "MIT", - "dependencies": { - "restructure": "^3.0.0" - } - }, "node_modules/jest": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", @@ -4990,10 +4770,17 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5153,18 +4940,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5208,12 +4983,6 @@ "tmpl": "1.0.5" } }, - "node_modules/media-engine": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", - "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", - "license": "MIT" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5347,15 +5116,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-svg-path": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", - "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", - "license": "MIT", - "dependencies": { - "svg-arc-to-cubic-bezier": "^3.0.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -5369,15 +5129,6 @@ "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obsidian": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", @@ -5498,12 +5249,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -5523,12 +5268,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-svg-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", - "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5583,6 +5322,34 @@ "dev": true, "license": "ISC" }, + "node_modules/pdfkit": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.18.0.tgz", + "integrity": "sha512-NvUwSDZ0eYEzqAiWwVQkRkjYUkZ48kcsHuCO31ykqPPIVkwoSDjDGiwIgHHNtsiwls3z3P/zy4q00hl2chg2Ug==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "fontkit": "^2.0.4", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/pdfmake": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.3.7.tgz", + "integrity": "sha512-SwTFcaH3kCJBlPFWi/YB34zRg6lpCxq90tkZ9GxfSi9/v4Tk96cv4IvOstA+CC40rdW1OzQIuNhD2DLD1RDVgA==", + "license": "MIT", + "dependencies": { + "linebreak": "^1.1.0", + "pdfkit": "^0.18.0", + "xmldoc": "^2.0.3" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5682,11 +5449,10 @@ "node": ">=8" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -5743,23 +5509,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5788,15 +5537,6 @@ ], "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5855,15 +5595,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -5893,31 +5624,14 @@ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", "license": "MIT" }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.25.0-rc-603e6108-20241029", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", - "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", - "license": "MIT" + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/semver": { "version": "7.7.4", @@ -5968,21 +5682,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6044,15 +5743,6 @@ "node": ">=8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6248,12 +5938,6 @@ "node": ">=8" } }, - "node_modules/svg-arc-to-cubic-bezier": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", - "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", - "license": "ISC" - }, "node_modules/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -6621,12 +6305,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -6655,20 +6333,6 @@ "node": ">=10.12.0" } }, - "node_modules/vite-compatible-readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", - "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -6837,6 +6501,18 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xmldoc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.3.tgz", + "integrity": "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w==", + "license": "MIT", + "dependencies": { + "sax": "^1.4.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6941,12 +6617,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", - "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", - "license": "MIT" - }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index ebdc85a..81ef6c3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^16.11.6", + "@types/pdfmake": "^0.3.2", "@types/react": "^19.2.4", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "8.57.2", @@ -33,8 +34,8 @@ "typescript": "5.9.2" }, "dependencies": { - "@react-pdf/renderer": "^4.3.2", "moment": "^2.30.1", + "pdfmake": "^0.3.7", "react": "^19.2.4", "react-dom": "^19.2.4", "uuid": "^11.0.5", diff --git a/src/components/pdf/TimesheetPdf.tsx b/src/components/pdf/TimesheetPdf.tsx deleted file mode 100644 index daf2d93..0000000 --- a/src/components/pdf/TimesheetPdf.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react"; -import type { Moment } from "moment"; -import { Timekeep } from "@/timekeep/schema"; -import { getTotalDuration } from "@/timekeep"; -import { TimekeepSettings } from "@/settings"; -import { Page, View, Text, Document } from "@react-pdf/renderer"; -import { - formatPdfDate, - formatDurationLong, - formatDurationShort, -} from "@/utils"; - -import { createStyles } from "./styles"; -import TimesheetPdfTable from "./TimesheetPdfTable"; -import TimesheetPdfDetailField from "./TimesheetPdfDetailField"; - -type Props = { - title: string; - data: Timekeep; - currentTime: Moment; - footnote: string; - settings: TimekeepSettings; -}; - -export default function TimesheetPdf({ - title, - data, - currentTime, - footnote, - settings, -}: Props) { - // Get the total elapsed duration - const duration = getTotalDuration(data.entries, currentTime); - - const currentDate = formatPdfDate(currentTime, settings); - const totalDuration = formatDurationLong(duration); - const totalDurationShort = formatDurationShort(duration); - - const styles = createStyles(settings.pdfFontFamily); - - return ( - - - - {title} - Timesheet - - - - - - - - - - - - {footnote} - - - - ); -} diff --git a/src/components/pdf/TimesheetPdfDetailField.tsx b/src/components/pdf/TimesheetPdfDetailField.tsx deleted file mode 100644 index 140df4d..0000000 --- a/src/components/pdf/TimesheetPdfDetailField.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { Text } from "@react-pdf/renderer"; - -import { Styles } from "./styles"; - -type Props = { - name: string; - value: string; - styles: Styles; -}; - -// Individual field within the details section -export default function TimesheetPdfDetailField({ - name, - value, - styles, -}: Props) { - return ( - - {name}: - {value} - - ); -} diff --git a/src/components/pdf/TimesheetPdfTable.tsx b/src/components/pdf/TimesheetPdfTable.tsx deleted file mode 100644 index 478e4b6..0000000 --- a/src/components/pdf/TimesheetPdfTable.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from "react"; -import type { Moment } from "moment"; -import { Timekeep } from "@/timekeep/schema"; -import { TimekeepSettings } from "@/settings"; -import { View, Text } from "@react-pdf/renderer"; - -import { Styles } from "./styles"; -import { TimesheetPdfTableRow } from "./TimesheetPdfTableRow"; - -type Props = { - data: Timekeep; - currentTime: Moment; - totalDuration: string; - settings: TimekeepSettings; - styles: Styles; -}; - -export default function TimesheetPdfTable({ - data, - currentTime, - totalDuration, - settings, - styles, -}: Props) { - // Render the table header - const renderHeader = ( - - - Block Name - - - Start Time - - - End Time - - - Duration - - - ); - - // Render the rows of the table - const renderEntries = data.entries.map((entry, index) => ( - - )); - - // Render the table footer - const renderFooter = ( - - Total - - - - - - {totalDuration} - - - ); - - return ( - - {renderHeader} - {renderEntries} - {renderFooter} - - ); -} diff --git a/src/components/pdf/TimesheetPdfTableRow.tsx b/src/components/pdf/TimesheetPdfTableRow.tsx deleted file mode 100644 index 48c172d..0000000 --- a/src/components/pdf/TimesheetPdfTableRow.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { Moment } from "moment"; -import React, { Fragment } from "react"; -import { getEntryDuration } from "@/timekeep"; -import { TimekeepSettings } from "@/settings"; -import { TimeEntry } from "@/timekeep/schema"; -import { View, Text } from "@react-pdf/renderer"; -import { formatPdfRowDate, formatDurationLong } from "@/utils"; - -import { Styles } from "./styles"; - -type Props = { - entry: TimeEntry; - currentTime: Moment; - settings: TimekeepSettings; - styles: Styles; -}; - -export function TimesheetPdfTableRow({ - entry, - currentTime, - settings, - styles, -}: Props) { - const duration = getEntryDuration(entry, currentTime); - const durationFormatted = formatDurationLong(duration); - - // Render start and end timing for individual entries - const renderTiming = entry.startTime !== null && ( - <> - - {formatPdfRowDate(entry.startTime, settings)} - - - {formatPdfRowDate(entry.endTime ?? currentTime, settings)} - - - ); - - // Render the child rows for groups - const renderChildren = entry.subEntries != null && ( - - {entry.subEntries.map((entry, index) => ( - - ))} - - ); - - return ( - - - - {entry.name} - - {renderTiming} - - {durationFormatted} - - - - {renderChildren} - - ); -} diff --git a/src/components/pdf/index.ts b/src/components/pdf/index.ts index c7a6725..2832ca2 100644 --- a/src/components/pdf/index.ts +++ b/src/components/pdf/index.ts @@ -1,45 +1,7 @@ -import React from "react"; import { Moment } from "moment"; import { Timekeep } from "@/timekeep/schema"; import { TimekeepSettings } from "@/settings"; -import RubikBold from "@/fonts/Rubik-Bold.ttf"; -import { pdf, Font } from "@react-pdf/renderer"; -// Embedded fonts -import RobotoBold from "@/fonts/Roboto-Bold.ttf"; -import RubikRegular from "@/fonts/Rubik-Regular.ttf"; -import RobotoRegular from "@/fonts/Roboto-Regular.ttf"; - -import TimesheetPdf from "./TimesheetPdf"; - -// Register the embedded font (Roboto for a large range of supported languages/characters https://github.com/jacobtread/obsidian-timekeep/issues/1) -Font.register({ - family: "Roboto", - fonts: [ - { - src: RobotoRegular, - fontWeight: 400, - }, - { - src: RobotoBold, - fontWeight: 700, - }, - ], -}); - -// Register the Rubik font for Arabic character support (https://github.com/jacobtread/obsidian-timekeep/issues/61) -Font.register({ - family: "Rubik", - fonts: [ - { - src: RubikRegular, - fontWeight: 400, - }, - { - src: RubikBold, - fontWeight: 700, - }, - ], -}); +import { createPdfExport } from "@/components/pdf/renderer"; /** * Generates a PDF for the provided timekeep @@ -54,18 +16,5 @@ export async function createPdf( settings: TimekeepSettings, currentTime: Moment ): Promise { - const document = React.createElement(TimesheetPdf, { - data: timekeep, - title: settings.pdfTitle, - footnote: settings.pdfFootnote, - currentTime, - settings, - }); - - // Create the PDF - const createdPdf = pdf(document); - - // Create a blob from the PDF - const buffer = await createdPdf.toBuffer(); - return buffer; + return createPdfExport(timekeep, settings, currentTime); } diff --git a/src/components/pdf/renderer.ts b/src/components/pdf/renderer.ts new file mode 100644 index 0000000..5975cd8 --- /dev/null +++ b/src/components/pdf/renderer.ts @@ -0,0 +1,377 @@ +import type { TimeEntry, Timekeep } from "@/timekeep/schema"; +import type { TimekeepSettings } from "@/settings"; +import type { Moment } from "moment"; + +import pdfMake from "pdfmake"; +import type { + DynamicContent, + Table, + TableCell, + TDocumentDefinitions, + TFontDictionary, +} from "pdfmake/interfaces"; + +// Embedded fonts +import RobotoBold from "@/fonts/Roboto-Bold.ttf"; +import RubikRegular from "@/fonts/Rubik-Regular.ttf"; +import RubikBold from "@/fonts/Rubik-Bold.ttf"; +import RobotoRegular from "@/fonts/Roboto-Regular.ttf"; +import { getEntryDuration, getTotalDuration } from "@/timekeep"; +import { + formatDurationLong, + formatDurationShort, + formatPdfDate, + formatPdfRowDate, +} from "@/utils"; +import { Readable } from "stream"; + +const fonts: TFontDictionary = { + Roboto: { + normal: "Roboto-Regular.ttf", + bold: "Roboto-Bold.ttf", + }, + Rubik: { + normal: "Rubik-Regular.ttf", + bold: "Rubik-Bold.ttf", + }, +}; + +function stripDataUrlPrefix(text: string): string { + return text.substring("data:font/ttf;base64,".length); +} + +pdfMake.addVirtualFileSystem({ + "Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular), + "Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold), + "Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular), + "Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold), +}); + +pdfMake.addFonts(fonts); + +export async function createPdfExport( + timekeep: Timekeep, + settings: TimekeepSettings, + currentTime: Moment +): Promise { + const definition = createPdfDefinition(timekeep, settings, currentTime); + const pdf = pdfMake.createPdf(definition, {}); + const stream = await pdf.getBuffer(); + return Readable.from([stream]); +} + +export function createPdfDefinition( + timekeep: Timekeep, + settings: TimekeepSettings, + currentTime: Moment +): TDocumentDefinitions { + const duration = getTotalDuration(timekeep.entries, currentTime); + const currentDate = formatPdfDate(currentTime, settings); + const totalDuration = formatDurationLong(duration); + const totalDurationShort = formatDurationShort(duration); + + return { + content: [ + createPdfHeader( + settings.pdfTitle, + currentDate, + totalDuration, + totalDurationShort + ), + createPdfTable(timekeep, totalDuration, settings, currentTime), + ], + footer: createPdfFooter(settings), + defaultStyle: { + font: settings.pdfFontFamily, + }, + pageSize: "A4", + pageMargins: 15, + styles: { + heading: { + marginBottom: 5, + columnGap: 10, + }, + title: { + bold: true, + fontSize: 12, + marginBottom: 5, + }, + details: { + marginBottom: 5, + }, + footNote: { + noWrap: true, + fontSize: 6, + bold: true, + }, + pageNumber: { + noWrap: true, + fontSize: 6, + }, + footer: { + marginLeft: 15, + marginRight: 15, + }, + tableCell: { + fontSize: 8, + }, + tableCellTime: { + alignment: "right", + }, + tableCellHeader: { + bold: true, + }, + }, + }; +} + +function createPdfHeader( + pdfTitle: string, + currentDate: string, + totalDuration: string, + totalDurationShort: string +): pdfMake.Content { + return [ + { + columns: [ + { + text: pdfTitle, + alignment: "left", + style: "title", + width: "50%", + }, + { + text: "Timesheet", + alignment: "right", + style: "title", + width: "50%", + }, + ], + style: "heading", + }, + { + stack: [ + createPdfHeaderDetailField("Date", currentDate), + createPdfHeaderDetailField("Total Duration", totalDuration), + createPdfHeaderDetailField( + "Total Duration (hours)", + totalDurationShort + ), + ], + + style: "details", + }, + ]; +} + +function createPdfHeaderDetailField( + title: string, + value: string +): pdfMake.Content { + return { + text: [ + { text: title + ": ", fontSize: 8, bold: true }, + { text: value, fontSize: 8 }, + ], + marginBottom: 8, + }; +} + +function createPdfFooter(settings: TimekeepSettings): DynamicContent { + return function (currentPage: number, pageCount: number) { + return { + columns: [ + { + text: `${settings.pdfFootnote}`, + style: "footNote", + width: "50%", + alignment: "left", + }, + { + text: `${currentPage} of ${pageCount}`, + style: "pageNumber", + width: "50%", + alignment: "right", + }, + ], + style: "footer", + }; + }; +} + +function createPdfTable( + timekeep: Timekeep, + totalDuration: string, + settings: TimekeepSettings, + currentTime: Moment +): pdfMake.Content { + const rows = createPdfTableRows(timekeep.entries, settings, currentTime); + + return { + table: { + headerRows: 1, + body: [ + [ + { + text: "Block Name", + style: ["tableCell", "tableCellHeader"], + border: [true, false, false, true], + }, + { + text: "Start Time", + style: ["tableCell", "tableCellHeader"], + alignment: "right", + border: [false, false, false, true], + }, + { + text: "End Time", + style: ["tableCell", "tableCellHeader"], + alignment: "right", + border: [false, false, false, true], + }, + { + text: "Duration", + style: ["tableCell", "tableCellHeader"], + alignment: "right", + border: [false, false, true, true], + }, + ], + ...rows.map((row) => row.row), + [ + { text: "Total", style: "tableCell" }, + { text: "", style: "tableCell" }, + { text: "", style: "tableCell" }, + { + text: totalDuration, + style: ["tableCell", "tableCellFooter"], + bold: true, + alignment: "right", + }, + ], + ], + }, + + layout: { + fillColor: function (rowIndex, node, columnIndex) { + // Header & Footer styling + if (rowIndex === 0 || rowIndex === rows.length + 1) { + return "#ececec"; + } + + // Group row styling + if (rowIndex > 0 && rowIndex - 1 < rows.length) { + if (rows[rowIndex - 1].group) { + return "#f4f4f4"; + } + } + + // Default styling + return "#ffffff"; + }, + + vLineColor: function (columnIndex, node, rowIndex) { + return "#ececec"; + }, + + hLineColor: function (rowIndex, node, columnIndex) { + return "#ececec"; + }, + paddingBottom: () => 5, + paddingLeft: () => 5, + paddingTop: () => 5, + paddingRight: () => 5, + }, + }; +} + +type TableEntryRow = { + row: TableCell[]; + depth: number; + group: boolean; +}; + +function createPdfTableRows( + entries: TimeEntry[], + settings: TimekeepSettings, + currentTime: Moment +): TableEntryRow[] { + type StackEntry = { entry: TimeEntry; depth: number }; + + const rows: TableEntryRow[] = []; + const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 })); + + while (stack.length > 0) { + const { entry, depth } = stack.pop()!; + + rows.push({ + group: entry.subEntries !== null && entry.subEntries.length > 0, + row: createTableEntryCells(entry, depth, settings, currentTime), + depth, + }); + + if (entry.subEntries && entry.subEntries.length > 0) { + for (let i = entry.subEntries.length - 1; i >= 0; i--) { + stack.push({ + entry: entry.subEntries[i], + depth: depth + 1, + }); + } + } + } + + return rows; +} + +function createTableEntryCells( + entry: TimeEntry, + depth: number, + settings: TimekeepSettings, + currentTime: Moment +): TableCell[] { + const duration = getEntryDuration(entry, currentTime); + const durationFormatted = formatDurationLong(duration); + + let startTime: string; + let endTime: string; + + if (entry.startTime !== null) { + startTime = formatPdfRowDate(entry.startTime, settings); + endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings); + } else { + startTime = ""; + endTime = ""; + } + + const marginLeft = depth * 5; + + if (depth > 0) { + const bg = "#999"; + } + + return [ + { + text: entry.name, + style: ["tableCell", "tableCellBlock"], + marginLeft, + border: [true, false, false, true], + }, + { + text: startTime, + style: ["tableCell", "tableCellTime"], + alignment: "right", + border: [false, false, false, true], + }, + { + text: endTime, + style: ["tableCell", "tableCellTime"], + alignment: "right", + border: [false, false, false, true], + }, + { + text: durationFormatted, + style: ["tableCell", "tableCellTime"], + alignment: "right", + border: [false, false, true, true], + }, + ]; +} diff --git a/src/components/pdf/styles.ts b/src/components/pdf/styles.ts deleted file mode 100644 index fc29483..0000000 --- a/src/components/pdf/styles.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { StyleSheet } from "@react-pdf/renderer"; - -export type Styles = ReturnType; - -export function createStyles(fontFamily = "Roboto") { - return StyleSheet.create({ - // Styles for a page - page: { - fontFamily, - padding: 15, - }, - - // Heading with the page title and timesheet title - heading: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 5, - }, - - // User specified title and "Timesheet" - title: { - fontFamily, - fontWeight: 700, - fontSize: 12, - marginBottom: 5, - }, - - // Detail items at the top (Date, Total Duration, Short Duration) - details: { - gap: 4, - marginBottom: 5, - }, - - // Footer note - footNote: { - marginTop: 10, - fontSize: 6, - fontFamily, - fontWeight: 700, - }, - - // Name of a details field - detailsFieldName: { - fontFamily, - fontWeight: 700, - fontSize: 8, - marginBottom: 5, - }, - - // Value of a details field - detailsFieldValue: { - fontSize: 8, - marginBottom: 5, - }, - - // Container around the timesheet table - tableContainer: { - flexDirection: "row", - flexWrap: "wrap", - borderWidth: 1, - borderColor: "#ececec", - }, - - // Row within the table - tableRow: { - flexDirection: "row", - borderBottomColor: "#ececec", - borderBottomWidth: 1, - alignItems: "center", - height: 24, - fontSize: 8, - width: "100%", - }, - - // Header row - tableRowHeader: { - fontFamily, - fontWeight: 700, - backgroundColor: "#ececec", - }, - - // Base table cell - tableCell: { - padding: 15, - }, - - // Table cell that spans available width - tableCellBlock: { - width: "100%", - }, - - // Table cell that contains a timestamp - tableCellTime: { - width: "200px", - textAlign: "right", - }, - - // Table cell that contains a duration - tableCellDuration: { - width: "400px", - textAlign: "right", - fontWeight: 700, - fontFamily, - }, - - // Wrapper for nested rows - tableRowWrapper: { - borderColor: "#f9f9f9", - borderWidth: 5, - }, - - // Table indentation container - tableIndent: { - borderLeftWidth: 5, - borderLeftColor: "#999", - }, - }); -} diff --git a/src/timekeep/create.test.ts b/src/timekeep/create.test.ts index acb434b..2fcc71c 100644 --- a/src/timekeep/create.test.ts +++ b/src/timekeep/create.test.ts @@ -27,9 +27,8 @@ describe("createEntry", () => { describe("withEntry", () => { it("should add new entry to entries", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_entry/addNewEntry" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_entry/addNewEntry"); const output = withEntry(input, "New Entry", currentTime); expect(stripEntriesRuntimeData(output)).toEqual( stripEntriesRuntimeData(expected) @@ -37,9 +36,8 @@ describe("withEntry", () => { }); it("should generate block name when empty", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_entry/addEmptyBlockName" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_entry/addEmptyBlockName"); const output1 = withEntry(input, "", currentTime); expect(stripEntriesRuntimeData(output1)).toEqual( @@ -54,9 +52,8 @@ describe("withEntry", () => { }); it("should maintain existing entries when adding to a list", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_entry/addEntryToList" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_entry/addEntryToList"); const output = withEntry(input, "New Test Entry", currentTime); expect(stripEntriesRuntimeData(output)).toEqual( @@ -67,9 +64,8 @@ describe("withEntry", () => { describe("withSubEntry", () => { it("adding first entry should convert to group", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup"); const output = withSubEntry(input, "New Entry", currentTime); expect(stripEntryRuntimeData(output)).toEqual( stripEntryRuntimeData(expected) @@ -77,9 +73,8 @@ describe("withSubEntry", () => { }); it("adding first entry for folder should populate subentries", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries"); const output = withSubEntry(input, "New Entry", currentTime); expect(stripEntryRuntimeData(output)).toEqual( stripEntryRuntimeData(expected) @@ -87,9 +82,8 @@ describe("withSubEntry", () => { }); it("adding first entry for folder should extend subentries", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries"); const output = withSubEntry(input, "New Entry 2", currentTime); expect(stripEntryRuntimeData(output)).toEqual( stripEntryRuntimeData(expected) @@ -97,9 +91,8 @@ describe("withSubEntry", () => { }); it("adding to group should extend sub entries", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries"); const output = withSubEntry(input, "New Entry", currentTime); expect(stripEntryRuntimeData(output)).toEqual( @@ -108,9 +101,8 @@ describe("withSubEntry", () => { }); it("empty name should generate a part name (single)", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle"); const output = withSubEntry(input, "", currentTime); expect(stripEntryRuntimeData(output)).toEqual( @@ -119,9 +111,8 @@ describe("withSubEntry", () => { }); it("empty name should generate a part name (group)", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup"); const output = withSubEntry(input, "", currentTime); expect(stripEntryRuntimeData(output)).toEqual( diff --git a/src/timekeep/parser.test.ts b/src/timekeep/parser.test.ts index 7ab237b..2ead34d 100644 --- a/src/timekeep/parser.test.ts +++ b/src/timekeep/parser.test.ts @@ -38,9 +38,8 @@ const createCodeBlock = ( describe("extracting code blocks", () => { it("should extract codeblock contents", async () => { - const { text, inputTimekeep1, inputTimekeep2 } = await import( - "./__fixtures__/extracting/codeblockContents" - ); + const { text, inputTimekeep1, inputTimekeep2 } = + await import("./__fixtures__/extracting/codeblockContents"); const output = extractTimekeepCodeblocks(text); @@ -54,9 +53,8 @@ describe("extracting code blocks", () => { }); it("should ignore codeblocks that are not closed", async () => { - const { text, inputTimekeep1 } = await import( - "./__fixtures__/extracting/unclosedCodeBlock" - ); + const { text, inputTimekeep1 } = + await import("./__fixtures__/extracting/unclosedCodeBlock"); const output = extractTimekeepCodeblocks(text); expect(stripTimekeepRuntimeData(output[0])).toStrictEqual( diff --git a/src/timekeep/queries.test.ts b/src/timekeep/queries.test.ts index decae6e..69f625e 100644 --- a/src/timekeep/queries.test.ts +++ b/src/timekeep/queries.test.ts @@ -11,27 +11,24 @@ import { describe("getEntryById", () => { it("find top level entry", async () => { - const { input, targetEntry, targetEntryId } = await import( - "./__fixtures__/checking/findEntryById" - ); + const { input, targetEntry, targetEntryId } = + await import("./__fixtures__/checking/findEntryById"); const output = getEntryById(targetEntryId, input); expect(output).toEqual(targetEntry); }); it("find nested entry", async () => { - const { input, targetEntry, targetEntryId } = await import( - "./__fixtures__/checking/findEntryByIdNested" - ); + const { input, targetEntry, targetEntryId } = + await import("./__fixtures__/checking/findEntryByIdNested"); const output = getEntryById(targetEntryId, input); expect(output).toEqual(targetEntry); }); it("find entry non existent", async () => { - const { input, targetEntryId } = await import( - "./__fixtures__/checking/findEntryByIdMissing" - ); + const { input, targetEntryId } = + await import("./__fixtures__/checking/findEntryByIdMissing"); const output = getEntryById(targetEntryId, input); expect(output).toBeUndefined(); @@ -40,9 +37,8 @@ describe("getEntryById", () => { describe("getPathToEntry", () => { it("path not found", async () => { - const { targetEntry, entries, expected } = await import( - "./__fixtures__/path/pathNotFound" - ); + const { targetEntry, entries, expected } = + await import("./__fixtures__/path/pathNotFound"); const output = getPathToEntry(entries, targetEntry); expect(output).toEqual(expected); }); @@ -52,9 +48,8 @@ describe("getPathToEntry", () => { it("deep path found", () => {}); it("should find running entry path", async () => { - const { input, runningEntry, path } = await import( - "./__fixtures__/checking/findRunningEntryPath" - ); + const { input, runningEntry, path } = + await import("./__fixtures__/checking/findRunningEntryPath"); const output = getPathToEntry(input, runningEntry); expect(output).toEqual(path); @@ -63,18 +58,16 @@ describe("getPathToEntry", () => { describe("isEntryRunning", () => { it("should determine entry running state", async () => { - const { running, notRunning } = await import( - "./__fixtures__/checking/runningState" - ); + const { running, notRunning } = + await import("./__fixtures__/checking/runningState"); expect(isEntryRunning(running)).toBe(true); expect(isEntryRunning(notRunning)).toBe(false); }); it("should determine entry running state (nested)", async () => { - const { runningNested, stoppedNested } = await import( - "./__fixtures__/checking/runningState" - ); + const { runningNested, stoppedNested } = + await import("./__fixtures__/checking/runningState"); expect(isEntryRunning(runningNested)).toBe(true); expect(isEntryRunning(stoppedNested)).toBe(false); @@ -83,9 +76,8 @@ describe("isEntryRunning", () => { describe("getRunningEntry", () => { it("should find running entry", async () => { - const { input, runningEntry } = await import( - "./__fixtures__/checking/shouldFindRunningEntry" - ); + const { input, runningEntry } = + await import("./__fixtures__/checking/shouldFindRunningEntry"); const output = getRunningEntry(input); @@ -93,9 +85,8 @@ describe("getRunningEntry", () => { }); it("should find nested running entry", async () => { - const { input, runningEntry } = await import( - "./__fixtures__/checking/shouldFindRunningEntryNested" - ); + const { input, runningEntry } = + await import("./__fixtures__/checking/shouldFindRunningEntryNested"); const output = getRunningEntry(input); @@ -103,9 +94,8 @@ describe("getRunningEntry", () => { }); it("should not find running entry", async () => { - const { input } = await import( - "./__fixtures__/checking/shouldNotFindRunningEntry" - ); + const { input } = + await import("./__fixtures__/checking/shouldNotFindRunningEntry"); const output = getRunningEntry(input); @@ -115,17 +105,15 @@ describe("getRunningEntry", () => { describe("isKeepRunning", () => { it("should show keep running", async () => { - const { input } = await import( - "./__fixtures__/checking/shouldBeRunning" - ); + const { input } = + await import("./__fixtures__/checking/shouldBeRunning"); expect(isKeepRunning(input)).toBe(true); }); it("should show keep not running", async () => { - const { input } = await import( - "./__fixtures__/checking/shouldNotBeRunning" - ); + const { input } = + await import("./__fixtures__/checking/shouldNotBeRunning"); expect(isKeepRunning(input)).toBe(false); }); @@ -133,9 +121,8 @@ describe("isKeepRunning", () => { describe("getEntryDuration", () => { it("should get entry duration", async () => { - const { input, currentTime, durationMs } = await import( - "./__fixtures__/duration/shouldGetEntryDuration" - ); + const { input, currentTime, durationMs } = + await import("./__fixtures__/duration/shouldGetEntryDuration"); const output = getEntryDuration(input, currentTime); @@ -143,9 +130,8 @@ describe("getEntryDuration", () => { }); it("duration of non started entry should be zero", async () => { - const { input, currentTime, durationMs } = await import( - "./__fixtures__/duration/nonStartedZeroDuration" - ); + const { input, currentTime, durationMs } = + await import("./__fixtures__/duration/nonStartedZeroDuration"); const output = getEntryDuration(input, currentTime); @@ -153,9 +139,8 @@ describe("getEntryDuration", () => { }); it("duration should include children", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/duration/durationIncludeChildren" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/duration/durationIncludeChildren"); const output = getEntryDuration(input, currentTime); @@ -163,9 +148,8 @@ describe("getEntryDuration", () => { }); it("duration should use current as end for unfinished entries", async () => { - const { input, endTime, durationMs } = await import( - "./__fixtures__/duration/currentEndUnfinished" - ); + const { input, endTime, durationMs } = + await import("./__fixtures__/duration/currentEndUnfinished"); const output = getEntryDuration(input, endTime); @@ -175,9 +159,8 @@ describe("getEntryDuration", () => { describe("getTotalDuration", () => { it("should get total duration", async () => { - const { input, currentTime, expected } = await import( - "./__fixtures__/duration/totalDuration" - ); + const { input, currentTime, expected } = + await import("./__fixtures__/duration/totalDuration"); const output = getTotalDuration(input, currentTime); @@ -187,17 +170,15 @@ describe("getTotalDuration", () => { describe("getUniqueEntryHash", () => { it("hash should match when content matches", async () => { - const { left, right } = await import( - "./__fixtures__/hashing/hashMatches" - ); + const { left, right } = + await import("./__fixtures__/hashing/hashMatches"); expect(getUniqueEntryHash(left)).toBe(getUniqueEntryHash(right)); }); it("hash shouldn't match when content is different", async () => { - const { left, right } = await import( - "./__fixtures__/hashing/hashDoesNotMatch" - ); + const { left, right } = + await import("./__fixtures__/hashing/hashDoesNotMatch"); expect(getUniqueEntryHash(left)).not.toBe(getUniqueEntryHash(right)); }); diff --git a/src/timekeep/sort.test.ts b/src/timekeep/sort.test.ts index 8c0857b..cfc32a1 100644 --- a/src/timekeep/sort.test.ts +++ b/src/timekeep/sort.test.ts @@ -9,9 +9,8 @@ import { getEntriesSorted } from "./sort"; describe("getEntriesSorted", () => { it("should be in reverse order", async () => { - const { input, expected } = await import( - "./__fixtures__/ordering/reverseOrder" - ); + const { input, expected } = + await import("./__fixtures__/ordering/reverseOrder"); const settings: TimekeepSettings = defaultSettings; settings.sortOrder = SortOrder.REVERSE_INSERTION; @@ -21,9 +20,8 @@ describe("getEntriesSorted", () => { }); it("should be in newest first order", async () => { - const { input, expected } = await import( - "./__fixtures__/ordering/newestFirstOrder" - ); + const { input, expected } = + await import("./__fixtures__/ordering/newestFirstOrder"); const settings: TimekeepSettings = defaultSettings; settings.sortOrder = SortOrder.NEWEST_START; @@ -33,9 +31,8 @@ describe("getEntriesSorted", () => { }); it("should be in newest last order", async () => { - const { input, expected } = await import( - "./__fixtures__/ordering/newestLastOrder" - ); + const { input, expected } = + await import("./__fixtures__/ordering/newestLastOrder"); const settings: TimekeepSettings = defaultSettings; settings.sortOrder = SortOrder.OLDEST_START; @@ -45,9 +42,8 @@ describe("getEntriesSorted", () => { }); it("should be in newest last order with nulls first", async () => { - const { input, expected } = await import( - "./__fixtures__/ordering/newestLastNullsFirst" - ); + const { input, expected } = + await import("./__fixtures__/ordering/newestLastNullsFirst"); const settings: TimekeepSettings = defaultSettings; settings.sortOrder = SortOrder.OLDEST_START; @@ -58,9 +54,8 @@ describe("getEntriesSorted", () => { }); it("order should not change", async () => { - const { input, expected } = await import( - "./__fixtures__/ordering/orderShouldNotChange" - ); + const { input, expected } = + await import("./__fixtures__/ordering/orderShouldNotChange"); const settings: TimekeepSettings = defaultSettings; settings.sortOrder = SortOrder.INSERTION; diff --git a/src/timekeep/start.test.ts b/src/timekeep/start.test.ts index 4f85edb..24d1af2 100644 --- a/src/timekeep/start.test.ts +++ b/src/timekeep/start.test.ts @@ -3,9 +3,8 @@ import { startNewEntry, startNewNestedEntry } from "./start"; describe("startNewEntry", () => { it("starting a new entry should stop any running entries", async () => { - const { currentTime, stopped, name, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startShouldStopRunning" - ); + const { currentTime, stopped, name, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startShouldStopRunning"); const output = startNewEntry(name, currentTime, input); const stoppedEntry = output.find((entry) => entry.id === stopped); @@ -19,9 +18,8 @@ describe("startNewEntry", () => { }); it("starting a new entry should add a new entry", async () => { - const { currentTime, name, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startShouldStopRunning" - ); + const { currentTime, name, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startShouldStopRunning"); const output = startNewEntry(name, currentTime, input); expect(stripEntriesRuntimeData(output)).toEqual( @@ -32,9 +30,8 @@ describe("startNewEntry", () => { describe("startNewNestedEntry", () => { it("starting a new entry should stop any running entries", async () => { - const { currentTime, targetEntry, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startNotStartedEntry" - ); + const { currentTime, targetEntry, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry"); const output = startNewNestedEntry(currentTime, targetEntry.id, input); expect(stripEntriesRuntimeData(output)).toEqual( @@ -43,9 +40,8 @@ describe("startNewNestedEntry", () => { }); it("starting a new entry within a folder should create a subentry", async () => { - const { currentTime, targetEntry, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startNestedFolderEntry" - ); + const { currentTime, targetEntry, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startNestedFolderEntry"); const output = startNewNestedEntry(currentTime, targetEntry.id, input); expect(stripEntriesRuntimeData(output)).toEqual( @@ -54,9 +50,8 @@ describe("startNewNestedEntry", () => { }); it("starting a new entry should stop any running entries", async () => { - const { currentTime, targetEntry, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startNotStartedEntry" - ); + const { currentTime, targetEntry, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry"); const output = startNewNestedEntry(currentTime, targetEntry.id, input); expect(stripEntriesRuntimeData(output)).toEqual( @@ -65,9 +60,8 @@ describe("startNewNestedEntry", () => { }); it("starting a new entry should stop any running entries", async () => { - const { currentTime, targetEntryId, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startNestedNonExistent" - ); + const { currentTime, targetEntryId, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startNestedNonExistent"); const output = startNewNestedEntry(currentTime, targetEntryId, input); expect(stripEntriesRuntimeData(output)).toEqual( @@ -77,9 +71,7 @@ describe("startNewNestedEntry", () => { it("starting a new entry should stop any running entries", async () => { const { currentTime, stopped, targetEntry, input, expected } = - await import( - "./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning" - ); + await import("./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning"); const output = startNewNestedEntry(currentTime, targetEntry.id, input); @@ -97,9 +89,8 @@ describe("startNewNestedEntry", () => { }); it("starting a new entry should add a new entry", async () => { - const { currentTime, targetEntry, input, expected } = await import( - "./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning" - ); + const { currentTime, targetEntry, input, expected } = + await import("./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning"); const output = startNewNestedEntry(currentTime, targetEntry.id, input); expect(stripEntriesRuntimeData(output)).toEqual( diff --git a/src/timekeep/update.test.ts b/src/timekeep/update.test.ts index 298fb42..9b6650c 100644 --- a/src/timekeep/update.test.ts +++ b/src/timekeep/update.test.ts @@ -10,9 +10,7 @@ import { describe("updateEntry", () => { it("updating existing entry should succeed", async () => { const { entries, entryToUpdate, updatedEntry, expectedEntries } = - await import( - "./__fixtures__/manipulating/update_entry/updateEntry" - ); + await import("./__fixtures__/manipulating/update_entry/updateEntry"); const updated = updateEntry(entries, entryToUpdate.id, updatedEntry); expect(updated).toEqual(expectedEntries); @@ -21,9 +19,8 @@ describe("updateEntry", () => { describe("setEntryCollapsed", () => { it("should update group collapse state when set to true", async () => { - const { input } = await import( - "./__fixtures__/manipulating/collapse/shouldUpdateCollapseTrue" - ); + const { input } = + await import("./__fixtures__/manipulating/collapse/shouldUpdateCollapseTrue"); const collapsed = setEntryCollapsed(input, true); @@ -33,9 +30,8 @@ describe("setEntryCollapsed", () => { }); it("collapsed state should be undefined when false", async () => { - const { input } = await import( - "./__fixtures__/manipulating/collapse/shouldUpdateCollapseFalse" - ); + const { input } = + await import("./__fixtures__/manipulating/collapse/shouldUpdateCollapseFalse"); const collapsed = setEntryCollapsed(input, false); expect(collapsed.subEntries).not.toBeNull(); @@ -43,9 +39,8 @@ describe("setEntryCollapsed", () => { }); it("should not set collapse state on single entry", async () => { - const { input } = await import( - "./__fixtures__/manipulating/collapse/shouldNotCollapseSingleEntry" - ); + const { input } = + await import("./__fixtures__/manipulating/collapse/shouldNotCollapseSingleEntry"); const collapsed = setEntryCollapsed(input, true); @@ -56,9 +51,8 @@ describe("setEntryCollapsed", () => { describe("stopRunningEntries", () => { it("should stop running entries", async () => { - const { input, endTime, expected } = await import( - "./__fixtures__/manipulating/stopping_entries/stopRunningEntries" - ); + const { input, endTime, expected } = + await import("./__fixtures__/manipulating/stopping_entries/stopRunningEntries"); const output = stopRunningEntries(input, endTime); expect(output).toEqual(expected); @@ -67,50 +61,44 @@ describe("stopRunningEntries", () => { describe("removeEntry", () => { it("remove on single entry should stay same if not target", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeSingleEntry" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeSingleEntry"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); it("should be able to remove entry", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeEntrySuccess" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeEntrySuccess"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); it("should be able to remove nested entry", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeNestedEntry" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeNestedEntry"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); it("should collapse groups with only one entry on remove", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeEntryCollapse" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeEntryCollapse"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); it("should collapse groups with only one entry on remove (single)", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeEntryCollapseSingle" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeEntryCollapseSingle"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); it("should not collapse folder on empty entries", async () => { - const { entries, entryToRemove, expectedEntries } = await import( - "./__fixtures__/manipulating/remove_entry/removeEntryFolder" - ); + const { entries, entryToRemove, expectedEntries } = + await import("./__fixtures__/manipulating/remove_entry/removeEntryFolder"); const updated = removeEntry(entries, entryToRemove); expect(updated).toEqual(expectedEntries); }); @@ -118,9 +106,8 @@ describe("removeEntry", () => { describe("removeSubEntry", () => { it("attempting to remove sub entry on non group should do nothing", async () => { - const { parent, entryToRemove } = await import( - "./__fixtures__/manipulating/remove_entry/removeEntry" - ); + const { parent, entryToRemove } = + await import("./__fixtures__/manipulating/remove_entry/removeEntry"); const output = removeSubEntry(parent, entryToRemove); expect(output).toEqual(parent); }); From 2af12e891a31530dad7c097a50a6246fa506982c Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 25 Mar 2026 10:57:56 +1300 Subject: [PATCH 2/3] feat: improve pdf rendering styling --- src/components/pdf/renderer.ts | 21 +++++++++++---------- test-vault/Obsidian Timekeep.md | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/pdf/renderer.ts b/src/components/pdf/renderer.ts index 5975cd8..29fbbba 100644 --- a/src/components/pdf/renderer.ts +++ b/src/components/pdf/renderer.ts @@ -210,6 +210,7 @@ function createPdfTable( return { table: { headerRows: 1, + widths: ["*", 70, 70, 50], body: [ [ { @@ -220,13 +221,13 @@ function createPdfTable( { text: "Start Time", style: ["tableCell", "tableCellHeader"], - alignment: "right", + alignment: "center", border: [false, false, false, true], }, { text: "End Time", style: ["tableCell", "tableCellHeader"], - alignment: "right", + alignment: "center", border: [false, false, false, true], }, { @@ -276,14 +277,16 @@ function createPdfTable( hLineColor: function (rowIndex, node, columnIndex) { return "#ececec"; }, - paddingBottom: () => 5, - paddingLeft: () => 5, - paddingTop: () => 5, - paddingRight: () => 5, + paddingBottom: cellPadding, + paddingLeft: cellPadding, + paddingTop: cellPadding, + paddingRight: cellPadding, }, }; } +const cellPadding = () => 8; + type TableEntryRow = { row: TableCell[]; depth: number; @@ -358,14 +361,12 @@ function createTableEntryCells( { text: startTime, style: ["tableCell", "tableCellTime"], - alignment: "right", - border: [false, false, false, true], + alignment: "center", }, { text: endTime, style: ["tableCell", "tableCellTime"], - alignment: "right", - border: [false, false, false, true], + alignment: "center", }, { text: durationFormatted, diff --git a/test-vault/Obsidian Timekeep.md b/test-vault/Obsidian Timekeep.md index 1534db2..7daa32b 100644 --- a/test-vault/Obsidian Timekeep.md +++ b/test-vault/Obsidian Timekeep.md @@ -28,7 +28,7 @@ dv.span(totalRunningDuration); ```timekeep -{"entries":[{"name":"Block 4 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-03-17T01:33:51.000Z","endTime":"2024-03-17T01:33:55.000Z","subEntries":null},{"name":"Part 2","startTime":"2024-03-24T05:20:54.000Z","endTime":"2024-03-24T05:20:55.000Z","subEntries":null}]},{"name":"Part 2","startTime":"2024-03-24T05:20:51.498Z","endTime":"2024-03-24T05:20:53.370Z","subEntries":null},{"name":"Part 3","startTime":"2024-03-24T05:20:56.000Z","endTime":"2024-03-24T05:21:30.000Z","subEntries":null}]},{"name":"Block 5","startTime":"2024-03-17T01:33:57.375Z","endTime":"2024-03-17T01:33:58.867Z","subEntries":null},{"name":"Block 8","startTime":"2024-03-17T01:37:33.417Z","endTime":"2024-03-17T01:37:35.048Z","subEntries":null},{"name":"Block 9","startTime":"2024-03-17T01:37:44.470Z","endTime":"2024-03-17T01:37:45.141Z","subEntries":null},{"name":"Block 6","startTime":"2024-03-17T02:00:38.491Z","endTime":"2024-03-17T02:00:39.208Z","subEntries":null},{"name":"Block 7","startTime":"2024-03-17T02:00:39.528Z","endTime":"2024-03-17T02:00:39.823Z","subEntries":null},{"name":"Block 8","startTime":"2024-03-17T02:00:40.118Z","endTime":"2024-03-17T02:00:40.428Z","subEntries":null},{"name":"Block 9","startTime":"2024-03-17T02:00:40.814Z","endTime":"2024-03-17T02:00:41.066Z","subEntries":null},{"name":"Block 10","startTime":"2024-03-17T02:00:41.327Z","endTime":"2024-03-17T02:00:41.507Z","subEntries":null},{"name":"Block 11","startTime":"2024-03-17T02:00:41.783Z","endTime":"2024-03-17T02:00:42.009Z","subEntries":null},{"name":"Block 13","startTime":"2024-03-17T02:57:16.000Z","endTime":"2024-03-17T02:57:16.000Z","subEntries":null},{"name":"Block 14","startTime":"2024-03-17T02:57:19.000Z","endTime":"2024-03-17T02:57:19.000Z","subEntries":null},{"name":"Block 15","startTime":"2024-03-17T02:57:24.000Z","endTime":"2024-03-17T02:57:24.000Z","subEntries":null},{"name":"Block 16 Supper reallly long name that leaves the table bounds","startTime":"2024-03-17T02:57:25.000Z","endTime":"2024-03-17T02:57:25.000Z","subEntries":null},{"name":"Block 17","startTime":"2024-03-17T03:29:53.766Z","endTime":"2024-03-17T03:29:54.925Z","subEntries":null},{"name":"Block 18","startTime":"2024-03-17T03:30:26.368Z","endTime":"2024-03-17T03:30:34.257Z","subEntries":null},{"name":"New Block","startTime":"2024-03-17T03:32:42.451Z","endTime":"2024-03-17T03:33:41.318Z","subEntries":null},{"name":"Block 20","startTime":"2024-03-17T03:37:45.552Z","endTime":"2024-03-17T03:37:47.568Z","subEntries":null},{"name":"Block 21","startTime":"2024-03-17T03:38:14.405Z","endTime":"2024-03-17T03:38:18.039Z","subEntries":null},{"name":"Block 21","startTime":"2024-03-17T05:01:30.553Z","endTime":"2024-03-17T05:01:36.012Z","subEntries":null},{"name":"Block 22","startTime":"2024-03-17T05:01:37.365Z","endTime":"2024-03-17T05:01:40.180Z","subEntries":null},{"name":"Block 23","startTime":"2024-03-24T03:44:16.500Z","endTime":"2024-03-24T03:44:18.168Z","subEntries":null},{"name":"Block 24","startTime":"2024-03-24T03:44:34.125Z","endTime":"2024-03-24T03:44:35.421Z","subEntries":null},{"name":"Block 25","startTime":"2024-03-24T03:44:38.073Z","endTime":"2024-03-24T03:44:50.001Z","subEntries":null},{"name":"Block 26","startTime":"2024-03-24T04:05:50.912Z","endTime":"2024-03-24T04:05:53.243Z","subEntries":null},{"name":"This is my block","startTime":"2024-03-24T04:09:02.146Z","endTime":"2024-03-24T04:09:06.662Z","subEntries":null},{"name":"Block 28","startTime":"2024-03-24T04:15:47.980Z","endTime":"2024-03-24T04:15:48.924Z","subEntries":null},{"name":"Block 29","startTime":"2024-03-24T04:15:49.518Z","endTime":"2024-03-24T04:15:50.561Z","subEntries":null},{"name":"Block 30","startTime":"2024-03-24T04:15:51.654Z","endTime":"2024-03-24T04:15:52.560Z","subEntries":null},{"name":"Block 31","startTime":"2024-03-24T04:17:34.822Z","endTime":"2024-03-24T04:17:35.571Z","subEntries":null},{"name":"Block 32","startTime":"2024-03-24T04:17:36.303Z","endTime":"2024-03-24T04:17:36.936Z","subEntries":null},{"name":"Block 33","startTime":"2024-03-24T04:17:43.413Z","endTime":"2024-03-24T04:17:44.142Z","subEntries":null},{"name":"Block 34","startTime":"2024-03-24T04:17:44.916Z","endTime":"2024-03-24T04:17:45.552Z","subEntries":null},{"name":"Block 35","startTime":"2024-03-24T04:17:52.315Z","endTime":"2024-03-24T04:17:52.971Z","subEntries":null},{"name":"Block 36","startTime":"2024-03-24T04:17:53.590Z","endTime":"2024-03-24T04:17:54.318Z","subEntries":null},{"name":"Block 37","startTime":"2024-03-24T04:17:54.858Z","endTime":"2024-03-24T04:17:56.340Z","subEntries":null},{"name":"Block 38","startTime":"2024-03-24T04:21:19.540Z","endTime":"2024-03-24T04:21:20.803Z","subEntries":null},{"name":"Block 39","startTime":"2024-03-24T04:21:21.876Z","endTime":"2024-03-24T04:21:24.420Z","subEntries":null},{"name":"Block 40","startTime":"2024-03-24T04:22:14.073Z","endTime":"2024-03-24T04:22:14.793Z","subEntries":null},{"name":"Block 41","startTime":"2024-03-24T04:22:15.463Z","endTime":"2024-03-24T04:22:16.016Z","subEntries":null},{"name":"Block 42","startTime":"2024-03-24T04:28:14.807Z","endTime":"2024-03-24T04:28:15.565Z","subEntries":null},{"name":"Block 43","startTime":"2024-03-24T04:28:39.885Z","endTime":"2024-03-24T04:28:42.912Z","subEntries":null},{"name":"Block 44","startTime":"2024-03-24T04:28:44.495Z","endTime":"2024-03-24T04:28:45.736Z","subEntries":null},{"name":"Block 44","startTime":"2024-03-24T04:30:43.184Z","endTime":"2024-03-24T04:30:45.307Z","subEntries":null},{"name":"Block 45","startTime":"2024-03-24T04:30:46.565Z","endTime":"2024-03-24T04:30:47.749Z","subEntries":null},{"name":"Block 46","startTime":"2024-03-24T04:30:48.502Z","endTime":"2024-03-24T04:30:49.762Z","subEntries":null},{"name":"Block 47","startTime":"2024-03-24T04:31:01.261Z","endTime":"2024-03-24T04:31:12.831Z","subEntries":null},{"name":"Block 48","startTime":"2024-03-24T04:31:14.603Z","endTime":"2024-03-24T04:31:16.328Z","subEntries":null},{"name":"Block 49","startTime":"2024-03-24T04:31:17.249Z","endTime":"2024-03-24T04:31:18.115Z","subEntries":null},{"name":"Block 50","startTime":"2024-03-24T04:31:19.266Z","endTime":"2024-03-24T04:31:20.199Z","subEntries":null},{"name":"Block 51","startTime":"2024-03-24T04:33:17.046Z","endTime":"2024-03-24T04:33:17.646Z","subEntries":null},{"name":"Block 52","startTime":"2024-03-24T04:33:18.253Z","endTime":"2024-03-24T04:33:18.969Z","subEntries":null},{"name":"Block 53","startTime":"2024-03-24T04:33:19.684Z","endTime":"2024-03-24T04:33:21.706Z","subEntries":null},{"name":"Block 54","startTime":"2024-03-24T04:33:22.293Z","endTime":"2024-03-24T04:33:22.674Z","subEntries":null},{"name":"Block 55","startTime":"2024-03-24T06:35:53.785Z","endTime":"2024-03-24T06:35:54.322Z","subEntries":null},{"name":"Block 56","startTime":"2024-03-24T06:35:54.693Z","endTime":"2024-03-24T06:35:54.864Z","subEntries":null},{"name":"Block 57","startTime":"2024-03-24T06:35:55.031Z","endTime":"2024-03-24T06:35:55.190Z","subEntries":null},{"name":"Block 58","startTime":"2024-03-24T06:35:55.631Z","endTime":"2024-03-24T06:35:57.836Z","subEntries":null},{"name":"Block 59","startTime":"2024-03-24T06:36:06.400Z","endTime":"2024-03-24T06:36:08.452Z","subEntries":null},{"name":"Block 60","startTime":"2024-03-24T06:36:53.108Z","endTime":"2024-03-24T06:36:53.791Z","subEntries":null},{"name":"Block 61","startTime":"2024-03-24T06:42:04.593Z","endTime":"2024-03-24T06:42:05.961Z","subEntries":null},{"name":"Block 62","startTime":"2024-03-24T06:42:30.789Z","endTime":"2024-03-24T06:42:31.609Z","subEntries":null},{"name":"Block 63","startTime":"2024-05-01T08:17:37.721Z","endTime":"2024-05-01T08:20:49.070Z","subEntries":null},{"name":"Test dwada uidawh dawhiud hawhiduia uhiwdiuhhauiw dhawd ihuawuidh awudhiu ahiwudhiu ahwiudihu awhiudhiu hiud ihuawhid hiuahiud ihwad","startTime":"2024-05-01T08:20:56.478Z","endTime":"2024-05-01T08:21:01.643Z","subEntries":null},{"name":"Test","startTime":"2024-05-01T08:21:03.005Z","endTime":"2024-05-01T08:21:35.567Z","subEntries":null},{"name":"tEWADAWDAWDAWD","startTime":"2024-05-01T08:21:37.838Z","endTime":"2024-05-01T08:22:25.344Z","subEntries":null},{"name":"Tewadwdawd","startTime":"2024-05-01T08:22:27.180Z","endTime":"2024-05-01T08:22:28.683Z","subEntries":null},{"name":"awdawd","startTime":"2024-05-01T08:22:30.186Z","endTime":"2024-05-01T08:22:36.241Z","subEntries":null},{"name":"Block 69","startTime":"2024-05-01T08:22:36.760Z","endTime":"2024-05-01T08:22:37.361Z","subEntries":null},{"name":"Block 70","startTime":"2024-05-01T08:22:37.695Z","endTime":"2024-05-01T08:22:38.428Z","subEntries":null},{"name":"Block 71","startTime":"2024-05-01T08:22:39.077Z","endTime":"2024-05-01T08:22:40.410Z","subEntries":null},{"name":"Block 72","startTime":"2024-05-01T08:22:40.991Z","endTime":"2024-05-01T08:22:41.538Z","subEntries":null},{"name":"Block 73","startTime":"2024-05-01T08:22:41.943Z","endTime":"2024-05-01T08:22:42.729Z","subEntries":null},{"name":"Block 74","startTime":"2024-05-01T08:22:43.160Z","endTime":"2024-05-01T08:22:44.806Z","subEntries":null},{"name":"Block 75","startTime":"2024-05-01T08:22:45.423Z","endTime":"2024-05-01T08:22:46.040Z","subEntries":null},{"name":"Block 76","startTime":"2024-05-01T08:22:49.574Z","endTime":"2024-05-01T08:22:50.712Z","subEntries":null},{"name":"Block 77","startTime":"2024-05-01T08:22:51.298Z","endTime":"2024-05-01T08:23:06.033Z","subEntries":null},{"name":"Block 78","startTime":"2024-05-01T08:23:07.620Z","endTime":"2024-05-01T08:23:08.124Z","subEntries":null},{"name":"Block 79","startTime":"2024-05-01T08:23:09.529Z","endTime":"2024-05-01T08:23:09.984Z","subEntries":null},{"name":"Block 80","startTime":"2024-05-01T08:23:44.681Z","endTime":"2024-05-01T08:23:56.772Z","subEntries":null},{"name":"Block 82","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-07-21T07:12:56.433Z","endTime":"2024-07-21T07:13:33.907Z","subEntries":null},{"name":"Part 2","startTime":"2024-07-21T07:24:44.769Z","endTime":"2024-07-21T07:24:48.149Z","subEntries":null},{"name":"Part 3","startTime":"2024-07-21T09:06:54.527Z","endTime":"2024-07-21T09:06:57.360Z","subEntries":null},{"name":"Part 4","startTime":"2024-07-21T09:07:10.820Z","endTime":"2024-07-21T09:07:11.426Z","subEntries":null}]},{"name":"Block 82","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-07-21T07:13:34.743Z","endTime":"2024-07-21T07:14:13.095Z","subEntries":null},{"name":"Part 2","startTime":"2024-07-21T07:14:13.562Z","endTime":"2024-07-21T07:14:14.679Z","subEntries":null},{"name":"Part 3","startTime":"2024-07-21T07:14:15.566Z","endTime":"2024-07-21T07:14:15.970Z","subEntries":null},{"name":"Part 4","startTime":"2024-07-21T09:06:52.510Z","endTime":"2024-07-21T09:06:53.351Z","subEntries":null},{"name":"Part 5","startTime":"2024-07-21T09:07:19.089Z","endTime":"2024-07-21T09:07:20.066Z","subEntries":null}]},{"name":"Block 83","startTime":"2024-07-21T09:07:21.923Z","endTime":"2024-07-21T09:07:22.516Z","subEntries":null},{"name":"Block 81","startTime":"2025-01-07T02:20:00.000Z","endTime":"2025-01-07T03:20:00.000Z","subEntries":null},{"name":"Block 85 [[#Test]] ","startTime":"2025-01-07T04:18:19.000Z","endTime":"2025-01-07T03:18:20.000Z","subEntries":null}]} +{"entries":[{"name":"Block 4 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-03-17T01:33:51.000Z","endTime":"2024-09-17T07:50:55.000Z","subEntries":null},{"name":"Part 2","startTime":"2024-03-24T05:20:54.000Z","endTime":"2024-03-24T05:20:55.000Z","subEntries":null}]},{"name":"Part 2","startTime":"2024-03-24T05:20:51.498Z","endTime":"2024-03-24T05:20:53.370Z","subEntries":null},{"name":"Part 3","startTime":"2024-03-24T05:20:56.000Z","endTime":"2024-03-24T05:21:30.000Z","subEntries":null}]},{"name":"Block 5","startTime":"2024-03-17T01:33:57.375Z","endTime":"2024-03-17T01:33:58.867Z","subEntries":null},{"name":"Block 8","startTime":"2024-03-17T01:37:33.000Z","endTime":"2024-03-17T06:37:35.000Z","subEntries":null},{"name":"Block 9","startTime":"2024-03-17T01:37:44.470Z","endTime":"2024-03-17T01:37:45.141Z","subEntries":null},{"name":"Block 6","startTime":"2024-03-17T02:00:38.491Z","endTime":"2024-03-17T02:00:39.208Z","subEntries":null},{"name":"Block 7","startTime":"2024-03-17T02:00:39.528Z","endTime":"2024-03-17T02:00:39.823Z","subEntries":null},{"name":"Block 8","startTime":"2024-03-17T02:00:40.118Z","endTime":"2024-03-17T02:00:40.428Z","subEntries":null},{"name":"Block 9","startTime":"2024-03-17T02:00:40.814Z","endTime":"2024-03-17T02:00:41.066Z","subEntries":null},{"name":"Block 10","startTime":"2024-03-17T02:00:41.327Z","endTime":"2024-03-17T02:00:41.507Z","subEntries":null},{"name":"Block 11","startTime":"2024-03-17T02:00:41.783Z","endTime":"2024-03-17T02:00:42.009Z","subEntries":null},{"name":"Block 13","startTime":"2024-03-17T02:57:16.000Z","endTime":"2024-03-17T02:57:16.000Z","subEntries":null},{"name":"Block 14","startTime":"2024-03-17T02:57:19.000Z","endTime":"2024-03-17T02:57:19.000Z","subEntries":null},{"name":"Block 15","startTime":"2024-03-17T02:57:24.000Z","endTime":"2024-03-17T02:57:24.000Z","subEntries":null},{"name":"Block 16 Supper reallly long name that leaves the table bounds","startTime":"2024-03-17T02:57:25.000Z","endTime":"2024-03-17T02:57:25.000Z","subEntries":null},{"name":"Block 17","startTime":"2024-03-17T03:29:53.766Z","endTime":"2024-03-17T03:29:54.925Z","subEntries":null},{"name":"Block 18","startTime":"2024-03-17T03:30:26.368Z","endTime":"2024-03-17T03:30:34.257Z","subEntries":null},{"name":"New Block","startTime":"2024-03-17T03:32:42.451Z","endTime":"2024-03-17T03:33:41.318Z","subEntries":null},{"name":"Block 20","startTime":"2024-03-17T03:37:45.552Z","endTime":"2024-03-17T03:37:47.568Z","subEntries":null},{"name":"Block 21","startTime":"2024-03-17T03:38:14.405Z","endTime":"2024-03-17T03:38:18.039Z","subEntries":null},{"name":"Block 21","startTime":"2024-03-17T05:01:30.553Z","endTime":"2024-03-17T05:01:36.012Z","subEntries":null},{"name":"Block 22","startTime":"2024-03-17T05:01:37.365Z","endTime":"2024-03-17T05:01:40.180Z","subEntries":null},{"name":"Block 23","startTime":"2024-03-24T03:44:16.500Z","endTime":"2024-03-24T03:44:18.168Z","subEntries":null},{"name":"Block 24","startTime":"2024-03-24T03:44:34.125Z","endTime":"2024-03-24T03:44:35.421Z","subEntries":null},{"name":"Block 25","startTime":"2024-03-24T03:44:38.073Z","endTime":"2024-03-24T03:44:50.001Z","subEntries":null},{"name":"Block 26","startTime":"2024-03-24T04:05:50.912Z","endTime":"2024-03-24T04:05:53.243Z","subEntries":null},{"name":"This is my block","startTime":"2024-03-24T04:09:02.146Z","endTime":"2024-03-24T04:09:06.662Z","subEntries":null},{"name":"Block 28","startTime":"2024-03-24T04:15:47.980Z","endTime":"2024-03-24T04:15:48.924Z","subEntries":null},{"name":"Block 29","startTime":"2024-03-24T04:15:49.518Z","endTime":"2024-03-24T04:15:50.561Z","subEntries":null},{"name":"Block 30","startTime":"2024-03-24T04:15:51.654Z","endTime":"2024-03-24T04:15:52.560Z","subEntries":null},{"name":"Block 31","startTime":"2024-03-24T04:17:34.822Z","endTime":"2024-03-24T04:17:35.571Z","subEntries":null},{"name":"Block 32","startTime":"2024-03-24T04:17:36.303Z","endTime":"2024-03-24T04:17:36.936Z","subEntries":null},{"name":"Block 33","startTime":"2024-03-24T04:17:43.413Z","endTime":"2024-03-24T04:17:44.142Z","subEntries":null},{"name":"Block 34","startTime":"2024-03-24T04:17:44.916Z","endTime":"2024-03-24T04:17:45.552Z","subEntries":null},{"name":"Block 35","startTime":"2024-03-24T04:17:52.315Z","endTime":"2024-03-24T04:17:52.971Z","subEntries":null},{"name":"Block 36","startTime":"2024-03-24T04:17:53.590Z","endTime":"2024-03-24T04:17:54.318Z","subEntries":null},{"name":"Block 37","startTime":"2024-03-24T04:17:54.858Z","endTime":"2024-03-24T04:17:56.340Z","subEntries":null},{"name":"Block 38","startTime":"2024-03-24T04:21:19.540Z","endTime":"2024-03-24T04:21:20.803Z","subEntries":null},{"name":"Block 39","startTime":"2024-03-24T04:21:21.876Z","endTime":"2024-03-24T04:21:24.420Z","subEntries":null},{"name":"Block 40","startTime":"2024-03-24T04:22:14.073Z","endTime":"2024-03-24T04:22:14.793Z","subEntries":null},{"name":"Block 41","startTime":"2024-03-24T04:22:15.463Z","endTime":"2024-03-24T04:22:16.016Z","subEntries":null},{"name":"Block 42","startTime":"2024-03-24T04:28:14.807Z","endTime":"2024-03-24T04:28:15.565Z","subEntries":null},{"name":"Block 43","startTime":"2024-03-24T04:28:39.885Z","endTime":"2024-03-24T04:28:42.912Z","subEntries":null},{"name":"Block 44","startTime":"2024-03-24T04:28:44.495Z","endTime":"2024-03-24T04:28:45.736Z","subEntries":null},{"name":"Block 44","startTime":"2024-03-24T04:30:43.184Z","endTime":"2024-03-24T04:30:45.307Z","subEntries":null},{"name":"Block 45","startTime":"2024-03-24T04:30:46.565Z","endTime":"2024-03-24T04:30:47.749Z","subEntries":null},{"name":"Block 46","startTime":"2024-03-24T04:30:48.502Z","endTime":"2024-03-24T04:30:49.762Z","subEntries":null},{"name":"Block 47","startTime":"2024-03-24T04:31:01.261Z","endTime":"2024-03-24T04:31:12.831Z","subEntries":null},{"name":"Block 48","startTime":"2024-03-24T04:31:14.603Z","endTime":"2024-03-24T04:31:16.328Z","subEntries":null},{"name":"Block 49","startTime":"2024-03-24T04:31:17.249Z","endTime":"2024-03-24T04:31:18.115Z","subEntries":null},{"name":"Block 50","startTime":"2024-03-24T04:31:19.266Z","endTime":"2024-03-24T04:31:20.199Z","subEntries":null},{"name":"Block 51","startTime":"2024-03-24T04:33:17.046Z","endTime":"2024-03-24T04:33:17.646Z","subEntries":null},{"name":"Block 52","startTime":"2024-03-24T04:33:18.253Z","endTime":"2024-03-24T04:33:18.969Z","subEntries":null},{"name":"Block 53","startTime":"2024-03-24T04:33:19.684Z","endTime":"2024-03-24T04:33:21.706Z","subEntries":null},{"name":"Block 54","startTime":"2024-03-24T04:33:22.293Z","endTime":"2024-03-24T04:33:22.674Z","subEntries":null},{"name":"Block 55","startTime":"2024-03-24T06:35:53.785Z","endTime":"2024-03-24T06:35:54.322Z","subEntries":null},{"name":"Block 56","startTime":"2024-03-24T06:35:54.693Z","endTime":"2024-03-24T06:35:54.864Z","subEntries":null},{"name":"Block 57","startTime":"2024-03-24T06:35:55.031Z","endTime":"2024-03-24T06:35:55.190Z","subEntries":null},{"name":"Block 58","startTime":"2024-03-24T06:35:55.631Z","endTime":"2024-03-24T06:35:57.836Z","subEntries":null},{"name":"Block 59","startTime":"2024-03-24T06:36:06.400Z","endTime":"2024-03-24T06:36:08.452Z","subEntries":null},{"name":"Block 60","startTime":"2024-03-24T06:36:53.108Z","endTime":"2024-03-24T06:36:53.791Z","subEntries":null},{"name":"Block 61","startTime":"2024-03-24T06:42:04.593Z","endTime":"2024-03-24T06:42:05.961Z","subEntries":null},{"name":"Block 62","startTime":"2024-03-24T06:42:30.789Z","endTime":"2024-03-24T06:42:31.609Z","subEntries":null},{"name":"Block 63","startTime":"2024-05-01T08:17:37.721Z","endTime":"2024-05-01T08:20:49.070Z","subEntries":null},{"name":"Test dwada uidawh dawhiud hawhiduia uhiwdiuhhauiw dhawd ihuawuidh awudhiu ahiwudhiu ahwiudihu awhiudhiu hiud ihuawhid hiuahiud ihwad","startTime":"2024-05-01T08:20:56.478Z","endTime":"2024-05-01T08:21:01.643Z","subEntries":null},{"name":"Test","startTime":"2024-05-01T08:21:03.005Z","endTime":"2024-05-01T08:21:35.567Z","subEntries":null},{"name":"tEWADAWDAWDAWD","startTime":"2024-05-01T08:21:37.838Z","endTime":"2024-05-01T08:22:25.344Z","subEntries":null},{"name":"Tewadwdawd","startTime":"2024-05-01T08:22:27.180Z","endTime":"2024-05-01T08:22:28.683Z","subEntries":null},{"name":"awdawd","startTime":"2024-05-01T08:22:30.186Z","endTime":"2024-05-01T08:22:36.241Z","subEntries":null},{"name":"Block 69","startTime":"2024-05-01T08:22:36.760Z","endTime":"2024-05-01T08:22:37.361Z","subEntries":null},{"name":"Block 70","startTime":"2024-05-01T08:22:37.695Z","endTime":"2024-05-01T08:22:38.428Z","subEntries":null},{"name":"Block 71","startTime":"2024-05-01T08:22:39.077Z","endTime":"2024-05-01T08:22:40.410Z","subEntries":null},{"name":"Block 72","startTime":"2024-05-01T08:22:40.991Z","endTime":"2024-05-01T08:22:41.538Z","subEntries":null},{"name":"Block 73","startTime":"2024-05-01T08:22:41.943Z","endTime":"2024-05-01T08:22:42.729Z","subEntries":null},{"name":"Block 74","startTime":"2024-05-01T08:22:43.160Z","endTime":"2024-05-01T08:22:44.806Z","subEntries":null},{"name":"Block 75","startTime":"2024-05-01T08:22:45.423Z","endTime":"2024-05-01T08:22:46.040Z","subEntries":null},{"name":"Block 76","startTime":"2024-05-01T08:22:49.574Z","endTime":"2024-05-01T08:22:50.712Z","subEntries":null},{"name":"Block 77","startTime":"2024-05-01T08:22:51.298Z","endTime":"2024-05-01T08:23:06.033Z","subEntries":null},{"name":"Block 78","startTime":"2024-05-01T08:23:07.620Z","endTime":"2024-05-01T08:23:08.124Z","subEntries":null},{"name":"Block 79","startTime":"2024-05-01T08:23:09.529Z","endTime":"2024-05-01T08:23:09.984Z","subEntries":null},{"name":"Block 80","startTime":"2024-05-01T08:23:44.681Z","endTime":"2024-05-01T08:23:56.772Z","subEntries":null},{"name":"Block 82","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-07-21T07:12:56.433Z","endTime":"2024-07-21T07:13:33.907Z","subEntries":null},{"name":"Part 2","startTime":"2024-07-21T07:24:44.769Z","endTime":"2024-07-21T07:24:48.149Z","subEntries":null},{"name":"Part 3","startTime":"2024-07-21T09:06:54.527Z","endTime":"2024-07-21T09:06:57.360Z","subEntries":null},{"name":"Part 4","startTime":"2024-07-21T09:07:10.820Z","endTime":"2024-07-21T09:07:11.426Z","subEntries":null}]},{"name":"Block 82","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-07-21T07:13:34.743Z","endTime":"2024-07-21T07:14:13.095Z","subEntries":null},{"name":"Part 2","startTime":"2024-07-21T07:14:13.562Z","endTime":"2024-07-21T07:14:14.679Z","subEntries":null},{"name":"Part 3","startTime":"2024-07-21T07:14:15.566Z","endTime":"2024-07-21T07:14:15.970Z","subEntries":null},{"name":"Part 4","startTime":"2024-07-21T09:06:52.510Z","endTime":"2024-07-21T09:06:53.351Z","subEntries":null},{"name":"Part 5","startTime":"2024-07-21T09:07:19.089Z","endTime":"2024-07-21T09:07:20.066Z","subEntries":null}]},{"name":"Block 83","startTime":"2024-07-21T09:07:21.923Z","endTime":"2024-07-21T09:07:22.516Z","subEntries":null},{"name":"Block 81","startTime":"2025-01-07T02:20:00.000Z","endTime":"2025-01-07T03:20:00.000Z","subEntries":null},{"name":"Block 85 [[#Test]] ","startTime":"2025-01-07T04:18:19.000Z","endTime":"2025-01-07T03:18:20.000Z","subEntries":null}]} ``` ```timekeep {"entries":[{"name":"Block 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-03-31T07:27:36.569Z","endTime":"2024-03-31T07:27:37.726Z","subEntries":null},{"name":"Part 2","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-03-31T07:27:42.846Z","endTime":"2024-03-31T07:27:44.468Z","subEntries":null},{"name":"Part 2","startTime":"2024-03-31T07:27:47.517Z","endTime":"2024-03-31T07:27:48.459Z","subEntries":null}]}]},{"name":"Block 2","startTime":"2024-03-31T07:27:38.418Z","endTime":"2024-03-31T07:27:38.917Z","subEntries":null},{"name":"Block 3","startTime":"2024-03-31T07:27:39.480Z","endTime":"2024-03-31T07:27:39.796Z","subEntries":null},{"name":"Block 4","startTime":"2024-03-31T07:27:40.088Z","endTime":"2024-03-31T07:27:40.331Z","subEntries":null},{"name":"Block 5","startTime":"2024-03-31T07:27:40.548Z","endTime":"2024-03-31T07:27:40.795Z","subEntries":null},{"name":"Block 6","startTime":"2024-03-31T07:27:41.037Z","endTime":"2024-03-31T07:27:41.520Z","subEntries":null}]} From c95cc6d097caf158e7f1fa3fc938691c9583f2f2 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 25 Mar 2026 19:50:22 +1300 Subject: [PATCH 3/3] chore: run prettier format --- src/components/pdf/renderer.ts | 592 ++++++++++++++++----------------- 1 file changed, 296 insertions(+), 296 deletions(-) diff --git a/src/components/pdf/renderer.ts b/src/components/pdf/renderer.ts index 29fbbba..f97230a 100644 --- a/src/components/pdf/renderer.ts +++ b/src/components/pdf/renderer.ts @@ -4,11 +4,11 @@ import type { Moment } from "moment"; import pdfMake from "pdfmake"; import type { - DynamicContent, - Table, - TableCell, - TDocumentDefinitions, - TFontDictionary, + DynamicContent, + Table, + TableCell, + TDocumentDefinitions, + TFontDictionary, } from "pdfmake/interfaces"; // Embedded fonts @@ -18,361 +18,361 @@ import RubikBold from "@/fonts/Rubik-Bold.ttf"; import RobotoRegular from "@/fonts/Roboto-Regular.ttf"; import { getEntryDuration, getTotalDuration } from "@/timekeep"; import { - formatDurationLong, - formatDurationShort, - formatPdfDate, - formatPdfRowDate, + formatDurationLong, + formatDurationShort, + formatPdfDate, + formatPdfRowDate, } from "@/utils"; import { Readable } from "stream"; const fonts: TFontDictionary = { - Roboto: { - normal: "Roboto-Regular.ttf", - bold: "Roboto-Bold.ttf", - }, - Rubik: { - normal: "Rubik-Regular.ttf", - bold: "Rubik-Bold.ttf", - }, + Roboto: { + normal: "Roboto-Regular.ttf", + bold: "Roboto-Bold.ttf", + }, + Rubik: { + normal: "Rubik-Regular.ttf", + bold: "Rubik-Bold.ttf", + }, }; function stripDataUrlPrefix(text: string): string { - return text.substring("data:font/ttf;base64,".length); + return text.substring("data:font/ttf;base64,".length); } pdfMake.addVirtualFileSystem({ - "Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular), - "Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold), - "Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular), - "Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold), + "Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular), + "Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold), + "Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular), + "Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold), }); pdfMake.addFonts(fonts); export async function createPdfExport( - timekeep: Timekeep, - settings: TimekeepSettings, - currentTime: Moment + timekeep: Timekeep, + settings: TimekeepSettings, + currentTime: Moment ): Promise { - const definition = createPdfDefinition(timekeep, settings, currentTime); - const pdf = pdfMake.createPdf(definition, {}); - const stream = await pdf.getBuffer(); - return Readable.from([stream]); + const definition = createPdfDefinition(timekeep, settings, currentTime); + const pdf = pdfMake.createPdf(definition, {}); + const stream = await pdf.getBuffer(); + return Readable.from([stream]); } export function createPdfDefinition( - timekeep: Timekeep, - settings: TimekeepSettings, - currentTime: Moment + timekeep: Timekeep, + settings: TimekeepSettings, + currentTime: Moment ): TDocumentDefinitions { - const duration = getTotalDuration(timekeep.entries, currentTime); - const currentDate = formatPdfDate(currentTime, settings); - const totalDuration = formatDurationLong(duration); - const totalDurationShort = formatDurationShort(duration); + const duration = getTotalDuration(timekeep.entries, currentTime); + const currentDate = formatPdfDate(currentTime, settings); + const totalDuration = formatDurationLong(duration); + const totalDurationShort = formatDurationShort(duration); - return { - content: [ - createPdfHeader( - settings.pdfTitle, - currentDate, - totalDuration, - totalDurationShort - ), - createPdfTable(timekeep, totalDuration, settings, currentTime), - ], - footer: createPdfFooter(settings), - defaultStyle: { - font: settings.pdfFontFamily, - }, - pageSize: "A4", - pageMargins: 15, - styles: { - heading: { - marginBottom: 5, - columnGap: 10, - }, - title: { - bold: true, - fontSize: 12, - marginBottom: 5, - }, - details: { - marginBottom: 5, - }, - footNote: { - noWrap: true, - fontSize: 6, - bold: true, - }, - pageNumber: { - noWrap: true, - fontSize: 6, - }, - footer: { - marginLeft: 15, - marginRight: 15, - }, - tableCell: { - fontSize: 8, - }, - tableCellTime: { - alignment: "right", - }, - tableCellHeader: { - bold: true, - }, - }, - }; + return { + content: [ + createPdfHeader( + settings.pdfTitle, + currentDate, + totalDuration, + totalDurationShort + ), + createPdfTable(timekeep, totalDuration, settings, currentTime), + ], + footer: createPdfFooter(settings), + defaultStyle: { + font: settings.pdfFontFamily, + }, + pageSize: "A4", + pageMargins: 15, + styles: { + heading: { + marginBottom: 5, + columnGap: 10, + }, + title: { + bold: true, + fontSize: 12, + marginBottom: 5, + }, + details: { + marginBottom: 5, + }, + footNote: { + noWrap: true, + fontSize: 6, + bold: true, + }, + pageNumber: { + noWrap: true, + fontSize: 6, + }, + footer: { + marginLeft: 15, + marginRight: 15, + }, + tableCell: { + fontSize: 8, + }, + tableCellTime: { + alignment: "right", + }, + tableCellHeader: { + bold: true, + }, + }, + }; } function createPdfHeader( - pdfTitle: string, - currentDate: string, - totalDuration: string, - totalDurationShort: string + pdfTitle: string, + currentDate: string, + totalDuration: string, + totalDurationShort: string ): pdfMake.Content { - return [ - { - columns: [ - { - text: pdfTitle, - alignment: "left", - style: "title", - width: "50%", - }, - { - text: "Timesheet", - alignment: "right", - style: "title", - width: "50%", - }, - ], - style: "heading", - }, - { - stack: [ - createPdfHeaderDetailField("Date", currentDate), - createPdfHeaderDetailField("Total Duration", totalDuration), - createPdfHeaderDetailField( - "Total Duration (hours)", - totalDurationShort - ), - ], + return [ + { + columns: [ + { + text: pdfTitle, + alignment: "left", + style: "title", + width: "50%", + }, + { + text: "Timesheet", + alignment: "right", + style: "title", + width: "50%", + }, + ], + style: "heading", + }, + { + stack: [ + createPdfHeaderDetailField("Date", currentDate), + createPdfHeaderDetailField("Total Duration", totalDuration), + createPdfHeaderDetailField( + "Total Duration (hours)", + totalDurationShort + ), + ], - style: "details", - }, - ]; + style: "details", + }, + ]; } function createPdfHeaderDetailField( - title: string, - value: string + title: string, + value: string ): pdfMake.Content { - return { - text: [ - { text: title + ": ", fontSize: 8, bold: true }, - { text: value, fontSize: 8 }, - ], - marginBottom: 8, - }; + return { + text: [ + { text: title + ": ", fontSize: 8, bold: true }, + { text: value, fontSize: 8 }, + ], + marginBottom: 8, + }; } function createPdfFooter(settings: TimekeepSettings): DynamicContent { - return function (currentPage: number, pageCount: number) { - return { - columns: [ - { - text: `${settings.pdfFootnote}`, - style: "footNote", - width: "50%", - alignment: "left", - }, - { - text: `${currentPage} of ${pageCount}`, - style: "pageNumber", - width: "50%", - alignment: "right", - }, - ], - style: "footer", - }; - }; + return function (currentPage: number, pageCount: number) { + return { + columns: [ + { + text: `${settings.pdfFootnote}`, + style: "footNote", + width: "50%", + alignment: "left", + }, + { + text: `${currentPage} of ${pageCount}`, + style: "pageNumber", + width: "50%", + alignment: "right", + }, + ], + style: "footer", + }; + }; } function createPdfTable( - timekeep: Timekeep, - totalDuration: string, - settings: TimekeepSettings, - currentTime: Moment + timekeep: Timekeep, + totalDuration: string, + settings: TimekeepSettings, + currentTime: Moment ): pdfMake.Content { - const rows = createPdfTableRows(timekeep.entries, settings, currentTime); + const rows = createPdfTableRows(timekeep.entries, settings, currentTime); - return { - table: { - headerRows: 1, - widths: ["*", 70, 70, 50], - body: [ - [ - { - text: "Block Name", - style: ["tableCell", "tableCellHeader"], - border: [true, false, false, true], - }, - { - text: "Start Time", - style: ["tableCell", "tableCellHeader"], - alignment: "center", - border: [false, false, false, true], - }, - { - text: "End Time", - style: ["tableCell", "tableCellHeader"], - alignment: "center", - border: [false, false, false, true], - }, - { - text: "Duration", - style: ["tableCell", "tableCellHeader"], - alignment: "right", - border: [false, false, true, true], - }, - ], - ...rows.map((row) => row.row), - [ - { text: "Total", style: "tableCell" }, - { text: "", style: "tableCell" }, - { text: "", style: "tableCell" }, - { - text: totalDuration, - style: ["tableCell", "tableCellFooter"], - bold: true, - alignment: "right", - }, - ], - ], - }, + return { + table: { + headerRows: 1, + widths: ["*", 70, 70, 50], + body: [ + [ + { + text: "Block Name", + style: ["tableCell", "tableCellHeader"], + border: [true, false, false, true], + }, + { + text: "Start Time", + style: ["tableCell", "tableCellHeader"], + alignment: "center", + border: [false, false, false, true], + }, + { + text: "End Time", + style: ["tableCell", "tableCellHeader"], + alignment: "center", + border: [false, false, false, true], + }, + { + text: "Duration", + style: ["tableCell", "tableCellHeader"], + alignment: "right", + border: [false, false, true, true], + }, + ], + ...rows.map((row) => row.row), + [ + { text: "Total", style: "tableCell" }, + { text: "", style: "tableCell" }, + { text: "", style: "tableCell" }, + { + text: totalDuration, + style: ["tableCell", "tableCellFooter"], + bold: true, + alignment: "right", + }, + ], + ], + }, - layout: { - fillColor: function (rowIndex, node, columnIndex) { - // Header & Footer styling - if (rowIndex === 0 || rowIndex === rows.length + 1) { - return "#ececec"; - } + layout: { + fillColor: function (rowIndex, node, columnIndex) { + // Header & Footer styling + if (rowIndex === 0 || rowIndex === rows.length + 1) { + return "#ececec"; + } - // Group row styling - if (rowIndex > 0 && rowIndex - 1 < rows.length) { - if (rows[rowIndex - 1].group) { - return "#f4f4f4"; - } - } + // Group row styling + if (rowIndex > 0 && rowIndex - 1 < rows.length) { + if (rows[rowIndex - 1].group) { + return "#f4f4f4"; + } + } - // Default styling - return "#ffffff"; - }, + // Default styling + return "#ffffff"; + }, - vLineColor: function (columnIndex, node, rowIndex) { - return "#ececec"; - }, + vLineColor: function (columnIndex, node, rowIndex) { + return "#ececec"; + }, - hLineColor: function (rowIndex, node, columnIndex) { - return "#ececec"; - }, - paddingBottom: cellPadding, - paddingLeft: cellPadding, - paddingTop: cellPadding, - paddingRight: cellPadding, - }, - }; + hLineColor: function (rowIndex, node, columnIndex) { + return "#ececec"; + }, + paddingBottom: cellPadding, + paddingLeft: cellPadding, + paddingTop: cellPadding, + paddingRight: cellPadding, + }, + }; } const cellPadding = () => 8; type TableEntryRow = { - row: TableCell[]; - depth: number; - group: boolean; + row: TableCell[]; + depth: number; + group: boolean; }; function createPdfTableRows( - entries: TimeEntry[], - settings: TimekeepSettings, - currentTime: Moment + entries: TimeEntry[], + settings: TimekeepSettings, + currentTime: Moment ): TableEntryRow[] { - type StackEntry = { entry: TimeEntry; depth: number }; + type StackEntry = { entry: TimeEntry; depth: number }; - const rows: TableEntryRow[] = []; - const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 })); + const rows: TableEntryRow[] = []; + const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 })); - while (stack.length > 0) { - const { entry, depth } = stack.pop()!; + while (stack.length > 0) { + const { entry, depth } = stack.pop()!; - rows.push({ - group: entry.subEntries !== null && entry.subEntries.length > 0, - row: createTableEntryCells(entry, depth, settings, currentTime), - depth, - }); + rows.push({ + group: entry.subEntries !== null && entry.subEntries.length > 0, + row: createTableEntryCells(entry, depth, settings, currentTime), + depth, + }); - if (entry.subEntries && entry.subEntries.length > 0) { - for (let i = entry.subEntries.length - 1; i >= 0; i--) { - stack.push({ - entry: entry.subEntries[i], - depth: depth + 1, - }); - } - } - } + if (entry.subEntries && entry.subEntries.length > 0) { + for (let i = entry.subEntries.length - 1; i >= 0; i--) { + stack.push({ + entry: entry.subEntries[i], + depth: depth + 1, + }); + } + } + } - return rows; + return rows; } function createTableEntryCells( - entry: TimeEntry, - depth: number, - settings: TimekeepSettings, - currentTime: Moment + entry: TimeEntry, + depth: number, + settings: TimekeepSettings, + currentTime: Moment ): TableCell[] { - const duration = getEntryDuration(entry, currentTime); - const durationFormatted = formatDurationLong(duration); + const duration = getEntryDuration(entry, currentTime); + const durationFormatted = formatDurationLong(duration); - let startTime: string; - let endTime: string; + let startTime: string; + let endTime: string; - if (entry.startTime !== null) { - startTime = formatPdfRowDate(entry.startTime, settings); - endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings); - } else { - startTime = ""; - endTime = ""; - } + if (entry.startTime !== null) { + startTime = formatPdfRowDate(entry.startTime, settings); + endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings); + } else { + startTime = ""; + endTime = ""; + } - const marginLeft = depth * 5; + const marginLeft = depth * 5; - if (depth > 0) { - const bg = "#999"; - } + if (depth > 0) { + const bg = "#999"; + } - return [ - { - text: entry.name, - style: ["tableCell", "tableCellBlock"], - marginLeft, - border: [true, false, false, true], - }, - { - text: startTime, - style: ["tableCell", "tableCellTime"], - alignment: "center", - }, - { - text: endTime, - style: ["tableCell", "tableCellTime"], - alignment: "center", - }, - { - text: durationFormatted, - style: ["tableCell", "tableCellTime"], - alignment: "right", - border: [false, false, true, true], - }, - ]; + return [ + { + text: entry.name, + style: ["tableCell", "tableCellBlock"], + marginLeft, + border: [true, false, false, true], + }, + { + text: startTime, + style: ["tableCell", "tableCellTime"], + alignment: "center", + }, + { + text: endTime, + style: ["tableCell", "tableCellTime"], + alignment: "center", + }, + { + text: durationFormatted, + style: ["tableCell", "tableCellTime"], + alignment: "right", + border: [false, false, true, true], + }, + ]; }