feat: PDF work report export + Goal of the Week + weekGoal field
Added @react-pdf/renderer for server-side PDF generation. PDF report (/api/user/export-pdf): - Indigo cover band: title, user name, period, export date - Summary cards: total tasks / calendar weeks / projects - Per-week sections with indigo header bar showing week label + count - Goal of the week banner (blue left-border callout) when set - Task table: priority-color dot, title, then selected columns - Alternating row shading, column headers in grey - Page numbers in footer - A4, respects same field selection as CSV CSV report updated: - Added 'Goal of the week' as a section marker row (italic prefix) - weekGoal field is default-on alongside week grouping Both exports: - New 'weekGoal' field in EXPORT_FIELDS (default on): shows the weekly goal text only when the user has customised it (skips default placeholder) - Language-aware (de/en) labels and date formats Settings UI: - Two download buttons: CSV (plain) + PDF (indigo, prominent) - weekGoal checkbox in field picker grid v1.93.0
This commit is contained in:
parent
51e2b13ec6
commit
9485e78dab
500
package-lock.json
generated
500
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
452
src/app/api/user/export-pdf/route.ts
Normal file
452
src/app/api/user/export-pdf/route.ts
Normal file
@ -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<Record<ExportFieldKey, number>> = {
|
||||
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<string, any[]>;
|
||||
goalByWeek: Map<string, string>;
|
||||
colFields: typeof EXPORT_FIELDS;
|
||||
selectedFields: Set<ExportFieldKey>;
|
||||
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<string>();
|
||||
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<ExportFieldKey> = 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<any>
|
||||
);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@ -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<string, string> = { 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<string, string>();
|
||||
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<string, typeof tasks>();
|
||||
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<ExportFieldKey> = 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<string, typeof tasks>();
|
||||
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`;
|
||||
|
||||
@ -4377,17 +4377,25 @@ function SettingsSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download button */}
|
||||
<a
|
||||
href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
|
||||
target="_blank"
|
||||
className="weekly-auth-button w-full justify-center"
|
||||
style={{ display: "inline-flex", textDecoration: "none", background: "var(--weekly-settings-item-bg)", border: "1px solid var(--weekly-settings-input-border)", color: "var(--weekly-settings-text)", padding: "10px", borderRadius: "4px", fontWeight: 500, transition: "background-color 0.2s" }}
|
||||
>
|
||||
{profile.language === "de"
|
||||
? `Arbeitsbericht exportieren (${exportFields.size} Spalten)`
|
||||
: `Export Work Report (${exportFields.size} columns)`}
|
||||
</a>
|
||||
{/* Download buttons */}
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<a
|
||||
href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
|
||||
target="_blank"
|
||||
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px", textDecoration: "none", background: "var(--weekly-settings-item-bg)", border: "1px solid var(--weekly-settings-input-border)", color: "var(--weekly-settings-text)", padding: "9px 12px", borderRadius: "4px", fontWeight: 500, fontSize: "0.85rem", transition: "background-color 0.2s" }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||||
CSV
|
||||
</a>
|
||||
<a
|
||||
href={`/api/user/export-pdf?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
|
||||
target="_blank"
|
||||
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px", textDecoration: "none", background: "#6366f1", border: "none", color: "#fff", padding: "9px 12px", borderRadius: "4px", fontWeight: 600, fontSize: "0.85rem", transition: "background-color 0.2s" }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><rect x="8" y="13" width="8" height="6" rx="1"/></svg>
|
||||
PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup & Restore Section */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user