diff --git a/package-lock.json b/package-lock.json index ccd5a59..37c12f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "my-weekly-todo-list", - "version": "1.57.16", + "version": "1.92.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "my-weekly-todo-list", - "version": "1.57.16", + "version": "1.92.0", "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", @@ -19,6 +19,7 @@ "@next-auth/prisma-adapter": "^1.0.7", "@notionhq/client": "^5.14.0", "@prisma/client": "5.22.0", + "@react-pdf/renderer": "^4.4.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0", "@tiptap/extension-underline": "^3.20.0", @@ -2239,6 +2240,30 @@ "node": ">= 10" } }, + "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2412,6 +2437,182 @@ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", "license": "MIT" }, + "node_modules/@react-pdf/fns": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.3.tgz", + "integrity": "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.6.tgz", + "integrity": "sha512-1RxR/hTyZcbgjESUjrMms574xuS9PLB4ovqQx6jvgdrIHXUyeUtSH6i3Szw1qVfUnA9MfaEm1FBuydQeJD39BQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^5.0.0", + "@react-pdf/types": "^2.10.0", + "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.5.0", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.5.0.tgz", + "integrity": "sha512-XlGLzcZFdEYQHK6b9z9uPOVywbjv6pQ92D4RvqRmYNjpf7lQLnhdHr63tbiF7fB5k8Lg9/lGBXkHbzkeQW5geQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/image": "^3.0.4", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/stylesheet": "^6.1.4", + "@react-pdf/textkit": "^6.1.1", + "@react-pdf/types": "^2.10.0", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-5.0.0.tgz", + "integrity": "sha512-FcQBWGtfhMGuOB0G3NcnF/cBq/JnFVs22i1tuafiT1XlmG6KjCxgTGng5bVh+b9RtTuwNpUGmCtB6CmG6B4ZVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "@react-pdf/png-js": "^3.0.0", + "browserify-zlib": "^0.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "js-md5": "^0.8.3", + "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.2.0", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.2.0.tgz", + "integrity": "sha512-onlXLcA6SpsD7SX9HOyt55qdRRJCfauegPlo4ZNw0hA/IipaZTbT9MJliWKtEXm03ibGxAQyp/BgTuXm91fo0A==", + "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/reconciler/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/@react-pdf/render": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.4.0.tgz", + "integrity": "sha512-+gGa9ymGosN6Ld3hFFSIVCV03Vva5S+asW7vmKetZY4LnhX5ea2gYNy6wL3e9VsLHqFDAefIXGtGDLH6XxQHag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/textkit": "^6.1.1", + "@react-pdf/types": "^2.10.0", + "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.4.0", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.4.0.tgz", + "integrity": "sha512-TtCcz1vyYD6fddhvBagwr9Aj3gRFLCqvduERQyNhTzHJi3zAKayHvJFr2PxcP4sRyIDRhibDW6ApqNhTqIVPoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/font": "^4.0.6", + "@react-pdf/layout": "^4.5.0", + "@react-pdf/pdfkit": "^5.0.0", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/reconciler": "^2.0.0", + "@react-pdf/render": "^4.4.0", + "@react-pdf/types": "^2.10.0", + "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.4", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.4.tgz", + "integrity": "sha512-jiwovO7lUwgccAh3JbVcXnh90AiSKZetdz2ETcWsKApPPLzLUzPkEs6wCVvZqh3lcGOAPFV3AfdMkFnLwv1ryg==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/types": "^2.10.0", + "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.1", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.1.1.tgz", + "integrity": "sha512-HAHoa407q0UHLzwe/oL6VwgJj2cGKs5vORSVY+cRG/GC0kt7nxUV9N+2hA6VcqJA37gSRg7BTLsVr8Tt+4l5ow==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.10.0.tgz", + "integrity": "sha512-iz0NusqQ/9ZHQirWhJqOaxY1UkpvuNkEDtH4/SPCnhZJKBO/IhlFLFHuzbHkmWByBoX6X3m8GCc2b/1QH6QNlA==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.6", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/stylesheet": "^6.1.4" + } + }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -3700,6 +3901,12 @@ "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", @@ -4288,6 +4495,15 @@ "bcrypt": "bin/bcrypt" } }, + "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/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -4338,6 +4554,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "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", @@ -4660,6 +4894,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4696,6 +4939,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "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/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -5027,6 +5280,12 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -5130,6 +5389,12 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "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/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5815,6 +6080,15 @@ "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", @@ -6045,6 +6319,32 @@ "dev": true, "license": "ISC" }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fontkit/node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6656,6 +6956,21 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "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", @@ -6695,6 +7010,12 @@ "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/ical.js": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz", @@ -7205,6 +7526,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7371,6 +7698,15 @@ "@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": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -8829,6 +9165,12 @@ "url": "https://github.com/sponsors/panva" } }, + "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", @@ -9036,6 +9378,25 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9184,6 +9545,12 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, + "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", @@ -9537,6 +9904,15 @@ "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", @@ -9849,6 +10225,12 @@ "node": ">=6" } }, + "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/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9881,6 +10263,12 @@ "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", @@ -10323,7 +10711,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -10717,6 +11104,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10892,6 +11288,15 @@ "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": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10966,6 +11371,12 @@ "node": ">=10" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -11312,6 +11723,21 @@ "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/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11418,6 +11844,15 @@ "node": ">=10.0.0" } }, + "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", @@ -11740,6 +12175,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -11861,6 +12302,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12247,6 +12694,32 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -12351,7 +12824,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -12378,6 +12850,20 @@ "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", @@ -12784,6 +13270,12 @@ "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/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/package.json b/package.json index 478e4d2..7cd94bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.92.0", + "version": "1.93.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { @@ -31,6 +31,7 @@ "@next-auth/prisma-adapter": "^1.0.7", "@notionhq/client": "^5.14.0", "@prisma/client": "5.22.0", + "@react-pdf/renderer": "^4.4.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0", "@tiptap/extension-underline": "^3.20.0", diff --git a/src/app/api/user/export-pdf/route.ts b/src/app/api/user/export-pdf/route.ts new file mode 100644 index 0000000..8fe77de --- /dev/null +++ b/src/app/api/user/export-pdf/route.ts @@ -0,0 +1,452 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from "@/lib/auth"; +import { prisma } from '@/lib/prisma'; +import React from 'react'; +import { + Document, Page, Text, View, StyleSheet, renderToBuffer, Font, +} from '@react-pdf/renderer'; +import { + EXPORT_FIELDS, ExportFieldKey, + fetchExportData, weekLabel, fmtDate, priorityLabel, priorityColor, +} from '../export/route'; + +// ── PDF styles ───────────────────────────────────────────────────────────── + +const INDIGO = '#6366f1'; +const SLATE = '#1e293b'; +const MUTED = '#64748b'; +const BORDER = '#e2e8f0'; +const STRIPE = '#f8fafc'; +const WHITE = '#ffffff'; +const SUCCESS = '#059669'; + +const styles = StyleSheet.create({ + page: { + fontFamily: 'Helvetica', + fontSize: 9, + color: SLATE, + paddingTop: 48, + paddingBottom: 48, + paddingHorizontal: 44, + backgroundColor: WHITE, + }, + // ── Cover header + coverBand: { + backgroundColor: INDIGO, + borderRadius: 8, + paddingVertical: 20, + paddingHorizontal: 24, + marginBottom: 24, + }, + coverTitle: { + fontFamily: 'Helvetica-Bold', + fontSize: 20, + color: WHITE, + marginBottom: 4, + }, + coverSub: { + fontSize: 10, + color: 'rgba(255,255,255,0.78)', + }, + coverMeta: { + fontSize: 9, + color: 'rgba(255,255,255,0.6)', + marginTop: 10, + }, + // ── Summary row + summaryRow: { + flexDirection: 'row', + gap: 10, + marginBottom: 20, + }, + summaryCard: { + flex: 1, + borderRadius: 6, + borderWidth: 1, + borderColor: BORDER, + paddingVertical: 10, + paddingHorizontal: 12, + backgroundColor: STRIPE, + }, + summaryValue: { + fontFamily: 'Helvetica-Bold', + fontSize: 18, + color: INDIGO, + marginBottom: 2, + }, + summaryLabel: { + fontSize: 8, + color: MUTED, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + // ── Week section + weekBlock: { + marginBottom: 18, + }, + weekHeader: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: INDIGO, + borderRadius: 5, + paddingVertical: 6, + paddingHorizontal: 10, + marginBottom: 6, + }, + weekHeaderText: { + fontFamily: 'Helvetica-Bold', + fontSize: 10, + color: WHITE, + flex: 1, + }, + weekCount: { + fontSize: 8, + color: 'rgba(255,255,255,0.7)', + }, + // ── Goal banner + goalBanner: { + flexDirection: 'row', + alignItems: 'flex-start', + backgroundColor: '#eff6ff', + borderLeftWidth: 3, + borderLeftColor: '#3b82f6', + borderRadius: 4, + paddingVertical: 5, + paddingHorizontal: 8, + marginBottom: 6, + gap: 5, + }, + goalLabel: { + fontFamily: 'Helvetica-Bold', + fontSize: 8, + color: '#1d4ed8', + marginBottom: 1, + }, + goalText: { + fontSize: 8.5, + color: '#1e40af', + fontFamily: 'Helvetica-Oblique', + }, + // ── Table + tableHeader: { + flexDirection: 'row', + backgroundColor: '#f1f5f9', + borderTopLeftRadius: 4, + borderTopRightRadius: 4, + paddingVertical: 4, + paddingHorizontal: 6, + borderBottomWidth: 1, + borderBottomColor: BORDER, + }, + tableHeaderCell: { + fontFamily: 'Helvetica-Bold', + fontSize: 7.5, + color: MUTED, + textTransform: 'uppercase', + letterSpacing: 0.3, + }, + tableRow: { + flexDirection: 'row', + paddingVertical: 5, + paddingHorizontal: 6, + borderBottomWidth: 1, + borderBottomColor: BORDER, + alignItems: 'flex-start', + }, + tableRowAlt: { + backgroundColor: STRIPE, + }, + cell: { + fontSize: 8.5, + color: SLATE, + paddingRight: 4, + }, + cellMuted: { + fontSize: 8, + color: MUTED, + paddingRight: 4, + }, + priorityDot: { + width: 6, + height: 6, + borderRadius: 3, + marginTop: 1, + marginRight: 4, + flexShrink: 0, + }, + checkMark: { + fontSize: 8, + color: SUCCESS, + marginRight: 4, + flexShrink: 0, + }, + // ── Footer + footer: { + position: 'absolute', + bottom: 20, + left: 44, + right: 44, + flexDirection: 'row', + justifyContent: 'space-between', + borderTopWidth: 1, + borderTopColor: BORDER, + paddingTop: 6, + }, + footerText: { + fontSize: 7.5, + color: '#94a3b8', + }, +}); + +// ── Column width config ──────────────────────────────────────────────────── + +const COL_WIDTHS: Partial> = { + completedDate: 60, + scheduledDate: 60, + createdDate: 60, + project: 70, + list: 70, + priority: 65, + url: 80, + notes: 100, + description: 80, + delegatedTo: 60, + delegationNote:80, + startTime: 40, + kanbanStage: 55, +}; + +// ── PDF Document component ───────────────────────────────────────────────── + +function WorkReportPDF({ + byWeek, + goalByWeek, + colFields, + selectedFields, + de, + userName, + startDate, + endDate, +}: { + byWeek: Map; + goalByWeek: Map; + colFields: typeof EXPORT_FIELDS; + selectedFields: Set; + de: boolean; + userName: string; + startDate: string | null; + endDate: string | null; +}) { + const totalTasks = Array.from(byWeek.values()).reduce((s, t) => s + t.length, 0); + const totalWeeks = byWeek.size; + const projectSet = new Set(); + for (const tasks of byWeek.values()) { + for (const t of tasks) if (t.project?.name) projectSet.add(t.project.name); + } + + const exportedOn = new Date().toLocaleDateString(de ? 'de-DE' : 'en-GB', { + day: '2-digit', month: 'long', year: 'numeric', + }); + + const periodLabel = startDate && endDate + ? `${fmtDate(new Date(startDate), de)} – ${fmtDate(new Date(endDate), de)}` + : startDate ? `${de ? 'Ab' : 'From'} ${fmtDate(new Date(startDate), de)}` + : endDate ? `${de ? 'Bis' : 'Until'} ${fmtDate(new Date(endDate), de)}` + : de ? 'Gesamter Zeitraum' : 'All time'; + + // Total title column width = page width minus padding minus sum of other cols + const usableWidth = 595 - 88; // A4 minus 2×44 padding + const otherColsWidth = colFields.reduce((s, f) => s + (COL_WIDTHS[f.key] || 70), 0); + const titleWidth = Math.max(80, usableWidth - otherColsWidth - 10 /* dot/check */); + + return React.createElement(Document, { title: de ? 'Arbeitsbericht' : 'Work Report' }, + React.createElement(Page, { size: 'A4', style: styles.page }, + + // ── Cover band + React.createElement(View, { style: styles.coverBand }, + React.createElement(Text, { style: styles.coverTitle }, + de ? 'Arbeitsbericht' : 'Work Report' + ), + React.createElement(Text, { style: styles.coverSub }, userName), + React.createElement(Text, { style: styles.coverMeta }, + `${periodLabel} · ${de ? 'Exportiert am' : 'Exported'} ${exportedOn}` + ), + ), + + // ── Summary cards + React.createElement(View, { style: styles.summaryRow }, + React.createElement(View, { style: styles.summaryCard }, + React.createElement(Text, { style: styles.summaryValue }, String(totalTasks)), + React.createElement(Text, { style: styles.summaryLabel }, de ? 'Erledigte Aufgaben' : 'Completed tasks'), + ), + React.createElement(View, { style: styles.summaryCard }, + React.createElement(Text, { style: styles.summaryValue }, String(totalWeeks)), + React.createElement(Text, { style: styles.summaryLabel }, de ? 'Kalenderwochen' : 'Calendar weeks'), + ), + React.createElement(View, { style: styles.summaryCard }, + React.createElement(Text, { style: styles.summaryValue }, String(projectSet.size)), + React.createElement(Text, { style: styles.summaryLabel }, de ? 'Projekte' : 'Projects'), + ), + ), + + // ── Week sections + ...Array.from(byWeek.entries()).map(([weekKey, tasks]) => { + const goal = selectedFields.has('weekGoal') ? goalByWeek.get(weekKey) : undefined; + + return React.createElement(View, { key: weekKey, style: styles.weekBlock, wrap: false }, + + // Week header bar + React.createElement(View, { style: styles.weekHeader }, + React.createElement(Text, { style: styles.weekHeaderText }, + selectedFields.has('week') ? weekLabel(weekKey, de) : weekKey + ), + React.createElement(Text, { style: styles.weekCount }, + `${tasks.length} ${de ? 'Aufgaben' : 'tasks'}` + ), + ), + + // Goal banner + ...(goal ? [ + React.createElement(View, { style: styles.goalBanner }, + React.createElement(View, null, + React.createElement(Text, { style: styles.goalLabel }, + de ? 'Wochenziel' : 'Goal of the week' + ), + React.createElement(Text, { style: styles.goalText }, goal), + ), + ), + ] : []), + + // Table header + React.createElement(View, { style: styles.tableHeader }, + React.createElement(View, { style: { width: 10 } }), // dot/check space + React.createElement(Text, { style: [styles.tableHeaderCell, { flex: 1, minWidth: titleWidth }] }, + de ? 'Aufgabe' : 'Task' + ), + ...colFields.map(f => + React.createElement(Text, { + key: f.key, + style: [styles.tableHeaderCell, { width: COL_WIDTHS[f.key] || 70 }], + }, de ? f.labelDe : f.labelEn) + ), + ), + + // Task rows + ...tasks.map((t, i) => { + const pColor = priorityLabel(t, de) ? priorityColor(t) : undefined; + return React.createElement(View, { + key: t.id, + style: [styles.tableRow, i % 2 === 1 ? styles.tableRowAlt : {}], + }, + // Priority dot or checkmark + pColor + ? React.createElement(View, { style: [styles.priorityDot, { backgroundColor: pColor }] }) + : React.createElement(Text, { style: styles.checkMark }, '✓'), + + // Title + React.createElement(Text, { style: [styles.cell, { flex: 1, minWidth: titleWidth }] }, t.title || ''), + + // Dynamic columns + ...colFields.map(f => { + let val = ''; + switch (f.key) { + case 'completedDate': val = fmtDate(t.updatedAt, de); break; + case 'scheduledDate': val = fmtDate(t.scheduledDate, de); break; + case 'createdDate': val = fmtDate(t.createdAt, de); break; + case 'project': val = t.project?.name || ''; break; + case 'list': val = t.somedayList?.title || ''; break; + case 'priority': val = priorityLabel(t, de); break; + case 'url': val = t.url || ''; break; + case 'notes': val = (t.markdownContent || '').slice(0, 200); break; + case 'description': val = (t.description || '').slice(0, 200); break; + case 'delegatedTo': val = t.delegatedTo || ''; break; + case 'delegationNote': val = t.delegationNote || ''; break; + case 'startTime': val = t.startTime || ''; break; + case 'kanbanStage': val = t.kanbanStage || ''; break; + } + return React.createElement(Text, { + key: f.key, + style: [styles.cellMuted, { width: COL_WIDTHS[f.key] || 70 }], + }, val); + }), + ); + }), + ); + }), + + // ── Footer + React.createElement(View, { style: styles.footer, fixed: true }, + React.createElement(Text, { style: styles.footerText }, + de ? 'Arbeitsbericht — My Weekly ToDo' : 'Work Report — My Weekly ToDo' + ), + React.createElement(Text, { + style: styles.footerText, + render: ({ pageNumber, totalPages }: { pageNumber: number; totalPages: number }) => + `${pageNumber} / ${totalPages}`, + }), + ), + ) + ); +} + +// ── Route ────────────────────────────────────────────────────────────────── + +export async function GET(request: Request) { + const session = await getServerSession(authOptions); + const { searchParams } = new URL(request.url); + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + const lang = searchParams.get('lang') || 'de'; + const de = lang === 'de'; + + const fieldsParam = searchParams.get('fields'); + const selectedFields: Set = fieldsParam + ? new Set(fieldsParam.split(',') as ExportFieldKey[]) + : new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)); + + if (!session?.user?.email) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + try { + const user = await prisma.user.findUnique({ + where: { email: session.user.email }, + select: { id: true, name: true, email: true }, + }); + if (!user) return new NextResponse('User not found', { status: 404 }); + + const { byWeek, goalByWeek } = await fetchExportData(user.id, startDate, endDate); + + // Column fields = selected fields that are actual table columns (not csvOnly section markers) + const colFields = EXPORT_FIELDS.filter(f => selectedFields.has(f.key) && !f.csvOnly); + + const pdfBuffer = await renderToBuffer( + React.createElement(WorkReportPDF, { + byWeek, + goalByWeek, + colFields, + selectedFields, + de, + userName: user.name || user.email || '', + startDate, + endDate, + }) as React.ReactElement + ); + + const period = startDate && endDate ? `_${startDate}_${endDate}` : startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : ''; + const filename = de + ? `arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.pdf` + : `work-report${period}_${new Date().toISOString().split('T')[0]}.pdf`; + + return new NextResponse(new Uint8Array(pdfBuffer), { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + }, + }); + } catch (error) { + console.error('PDF export error:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/src/app/api/user/export/route.ts b/src/app/api/user/export/route.ts index 2ecef89..f94effd 100644 --- a/src/app/api/user/export/route.ts +++ b/src/app/api/user/export/route.ts @@ -4,9 +4,9 @@ import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; // ── Field registry ───────────────────────────────────────────────────────── -// All optional columns the user can include. "key" is what the UI sends in -// the ?fields= query param. "always" fields are always present. export type ExportFieldKey = + | 'week' + | 'weekGoal' | 'completedDate' | 'scheduledDate' | 'createdDate' @@ -19,34 +19,35 @@ export type ExportFieldKey = | 'delegatedTo' | 'delegationNote' | 'startTime' - | 'kanbanStage' - | 'week'; + | 'kanbanStage'; export const EXPORT_FIELDS: { key: ExportFieldKey; labelDe: string; labelEn: string; defaultOn: boolean; + csvOnly?: boolean; // not a table column, only used as a section in PDF/CSV }[] = [ - { key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true }, - { key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true }, - { key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true }, - { key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false }, - { key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true }, - { key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true }, - { key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true }, - { key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false }, - { key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false }, - { key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false }, - { key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false }, - { key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false }, - { key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false }, - { key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false }, + { key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true }, + { key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true }, + { key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true }, + { key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true }, + { key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false }, + { key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true }, + { key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true }, + { key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true }, + { key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false }, + { key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false }, + { key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false }, + { key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false }, + { key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false }, + { key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false }, + { key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false }, ]; -// ── Helpers ──────────────────────────────────────────────────────────────── +// ── Shared helpers (also used by export-pdf route) ───────────────────────── -function isoWeek(date: Date): string { +export function isoWeek(date: Date): string { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); @@ -55,41 +56,108 @@ function isoWeek(date: Date): string { return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; } -function weekLabel(date: Date, de: boolean): string { - const week = isoWeek(date); - const [year, w] = week.split('-W'); +export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } { + const [year, w] = weekKey.split('-W'); const d = new Date(Date.UTC(parseInt(year), 0, 1)); d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1); const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6); - const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' }); - return de - ? `KW ${w} — ${year} (${fmt(d)} – ${fmt(sun)})` - : `Week ${w} — ${year} (${fmt(d)} – ${fmt(sun)})`; + return { mon: d, sun }; } -function fmtDate(d: Date | null | undefined, de: boolean): string { +export function weekLabel(weekKey: string, de: boolean): string { + const [, w] = weekKey.split('-W'); + const year = weekKey.split('-W')[0]; + const { mon, sun } = weekMondaySunday(weekKey); + const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' }); + return de + ? `KW ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})` + : `Week ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`; +} + +export function fmtDate(d: Date | null | undefined, de: boolean): string { if (!d) return ''; return de ? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); } -function priorityLabel(task: any, de: boolean): string { +export function priorityLabel(task: any, de: boolean): string { if (task.urgency != null && task.importance != null) { if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now'; if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule'; if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate'; return de ? 'Eliminieren' : 'Eliminate'; } - if (task.priority) return task.priority; // A / B / C / D / E + if (task.priority) return task.priority; return ''; } +export function priorityColor(task: any): string { + if (task.urgency != null && task.importance != null) { + if (task.urgency && task.importance) return '#ef4444'; + if (!task.urgency && task.importance) return '#3b82f6'; + if (task.urgency && !task.importance) return '#f97316'; + return '#9ca3af'; + } + const map: Record = { A: '#ef4444', B: '#f97316', C: '#eab308', D: '#3b82f6', E: '#9ca3af' }; + return task.priority ? (map[task.priority] || '#6b7280') : '#6b7280'; +} + function csvCell(value: string): string { return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`; } -// ── Route ────────────────────────────────────────────────────────────────── +const DEFAULT_GOAL = 'your goal of this week'; + +// ── Fetch shared export data (also used by PDF route) ────────────────────── + +export async function fetchExportData(userId: string, startDate: string | null, endDate: string | null) { + const where: any = { userId, completed: true, deletedAt: null }; + if (startDate || endDate) { + where.updatedAt = {}; + if (startDate) where.updatedAt.gte = new Date(startDate); + if (endDate) { + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); + where.updatedAt.lte = end; + } + } + + const [tasks, goals] = await Promise.all([ + prisma.task.findMany({ + where, + orderBy: { updatedAt: 'desc' }, + include: { + project: { select: { name: true, color: true } }, + somedayList: { select: { title: true } }, + }, + }), + prisma.weeklyGoal.findMany({ + where: { userId }, + select: { weekStart: true, text: true }, + }), + ]); + + // Build week → goal map (weekStart is always Monday) + const goalByWeek = new Map(); + for (const g of goals) { + if (g.text && g.text !== DEFAULT_GOAL) { + goalByWeek.set(isoWeek(g.weekStart), g.text); + } + } + + // Group tasks by ISO week + const byWeek = new Map(); + for (const task of tasks) { + const key = isoWeek(task.updatedAt); + if (!byWeek.has(key)) byWeek.set(key, []); + byWeek.get(key)!.push(task); + } + + return { byWeek, goalByWeek }; +} + +// ── CSV route ────────────────────────────────────────────────────────────── export async function GET(request: Request) { const session = await getServerSession(authOptions); @@ -99,13 +167,12 @@ export async function GET(request: Request) { const lang = searchParams.get('lang') || 'de'; const de = lang === 'de'; - // Parse selected fields; fall back to all defaultOn fields const fieldsParam = searchParams.get('fields'); const selectedFields: Set = fieldsParam ? new Set(fieldsParam.split(',') as ExportFieldKey[]) : new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)); - if (!session || !session.user?.email) { + if (!session?.user?.email) { return new NextResponse('Unauthorized', { status: 401 }); } @@ -113,57 +180,28 @@ export async function GET(request: Request) { const user = await prisma.user.findUnique({ where: { email: session.user.email } }); if (!user) return new NextResponse('User not found', { status: 404 }); - const where: any = { userId: user.id, completed: true, deletedAt: null }; - if (startDate || endDate) { - where.updatedAt = {}; - if (startDate) where.updatedAt.gte = new Date(startDate); - if (endDate) { - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); - where.updatedAt.lte = end; - } - } - - const tasks = await prisma.task.findMany({ - where, - orderBy: { updatedAt: 'desc' }, - include: { - project: { select: { name: true } }, - somedayList: { select: { title: true } }, - }, - }); - - // Build header row — Title is always first - const headerRow: string[] = [de ? 'Aufgabe' : 'Task']; - for (const f of EXPORT_FIELDS) { - if (selectedFields.has(f.key) && f.key !== 'week') { - headerRow.push(de ? f.labelDe : f.labelEn); - } - } + const { byWeek, goalByWeek } = await fetchExportData(user.id, startDate, endDate); + // Build header — Title always first, then selected non-section fields + const colFields = EXPORT_FIELDS.filter(f => selectedFields.has(f.key) && !f.csvOnly); + const headerRow = [de ? 'Aufgabe' : 'Task', ...colFields.map(f => de ? f.labelDe : f.labelEn)]; const rows: string[][] = [headerRow]; - // Group by ISO week (most recent first) - const byWeek = new Map(); - for (const task of tasks) { - const key = isoWeek(task.updatedAt); - if (!byWeek.has(key)) byWeek.set(key, []); - byWeek.get(key)!.push(task); - } - - for (const [, weekTasks] of byWeek) { - // Section header row when week column is selected + for (const [weekKey, weekTasks] of byWeek) { + // Week section header if (selectedFields.has('week')) { - const label = weekLabel(weekTasks[0].updatedAt, de); - // Fill entire row width with the label in first cell, rest blank - rows.push([label, ...Array(headerRow.length - 1).fill('')]); + rows.push([weekLabel(weekKey, de), ...Array(headerRow.length - 1).fill('')]); + } + // Goal row + const goal = goalByWeek.get(weekKey); + if (selectedFields.has('weekGoal') && goal) { + const goalLabel = de ? `Wochenziel: ${goal}` : `Goal: ${goal}`; + rows.push([goalLabel, ...Array(headerRow.length - 1).fill('')]); } for (const t of weekTasks) { const row: string[] = [t.title || '']; - - for (const f of EXPORT_FIELDS) { - if (!selectedFields.has(f.key) || f.key === 'week') continue; + for (const f of colFields) { switch (f.key) { case 'completedDate': row.push(fmtDate(t.updatedAt, de)); break; case 'scheduledDate': row.push(fmtDate(t.scheduledDate ?? undefined, de)); break; @@ -180,21 +218,15 @@ export async function GET(request: Request) { case 'kanbanStage': row.push(t.kanbanStage || ''); break; } } - rows.push(row); } - // Blank spacer between weeks rows.push(Array(headerRow.length).fill('')); } const csvContent = rows.map(row => row.map(csvCell).join(',')).join('\r\n'); const bom = '\uFEFF'; - - const period = startDate && endDate - ? `_${startDate}_${endDate}` - : startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : ''; - + const period = startDate && endDate ? `_${startDate}_${endDate}` : startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : ''; const filename = de ? `arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv` : `work-report${period}_${new Date().toISOString().split('T')[0]}.csv`; diff --git a/src/components/SettingsSidebar.tsx b/src/components/SettingsSidebar.tsx index 4528486..a9fb158 100644 --- a/src/components/SettingsSidebar.tsx +++ b/src/components/SettingsSidebar.tsx @@ -4377,17 +4377,25 @@ function SettingsSidebar({ - {/* Download button */} - - {profile.language === "de" - ? `Arbeitsbericht exportieren (${exportFields.size} Spalten)` - : `Export Work Report (${exportFields.size} columns)`} - + {/* Download buttons */} + {/* Backup & Restore Section */}