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:
mARTin 2026-04-07 09:37:57 +02:00
parent 51e2b13ec6
commit 9485e78dab
5 changed files with 1084 additions and 99 deletions

500
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.57.16", "version": "1.92.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.57.16", "version": "1.92.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",
@ -19,6 +19,7 @@
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@notionhq/client": "^5.14.0", "@notionhq/client": "^5.14.0",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"@react-pdf/renderer": "^4.4.0",
"@tiptap/extension-link": "^3.20.0", "@tiptap/extension-link": "^3.20.0",
"@tiptap/extension-placeholder": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0",
"@tiptap/extension-underline": "^3.20.0", "@tiptap/extension-underline": "^3.20.0",
@ -2239,6 +2240,30 @@
"node": ">= 10" "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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2412,6 +2437,182 @@
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT" "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": { "node_modules/@remirror/core-constants": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
@ -3700,6 +3901,12 @@
"win32" "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": { "node_modules/acorn": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@ -4288,6 +4495,15 @@
"bcrypt": "bin/bcrypt" "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": { "node_modules/bignumber.js": {
"version": "9.3.1", "version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
@ -4338,6 +4554,24 @@
"node": ">=8" "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": { "node_modules/browserslist": {
"version": "4.28.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
@ -4660,6 +4894,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "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": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -4696,6 +4939,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "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": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -5027,6 +5280,12 @@
"node": ">=8" "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": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -5130,6 +5389,12 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT" "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": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -5815,6 +6080,15 @@
"node": ">=0.10.0" "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": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -6045,6 +6319,32 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -6656,6 +6956,21 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "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": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@ -6695,6 +7010,12 @@
"node": ">=10.17.0" "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": { "node_modules/ical.js": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz", "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz",
@ -7205,6 +7526,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-weakmap": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@ -7371,6 +7698,15 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@ -8829,6 +9165,12 @@
"url": "https://github.com/sponsors/panva" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -9036,6 +9378,25 @@
"url": "https://github.com/sponsors/antonk52" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "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==", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -9537,6 +9904,15 @@
"node": ">=0.10.0" "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": { "node_modules/npm-run-path": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@ -9849,6 +10225,12 @@
"node": ">=6" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -9881,6 +10263,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -10323,7 +10711,6 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/preact": { "node_modules/preact": {
@ -10717,6 +11104,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -10892,6 +11288,15 @@
"node": ">=0.10.0" "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": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@ -10966,6 +11371,12 @@
"node": ">=10" "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": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@ -11312,6 +11723,21 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -11418,6 +11844,15 @@
"node": ">=10.0.0" "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": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -11740,6 +12175,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/tailwindcss": {
"version": "3.4.19", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
@ -11861,6 +12302,12 @@
"node": ">=0.8" "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": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -12247,6 +12694,32 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT" "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": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@ -12351,7 +12824,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": { "node_modules/uuid": {
@ -12378,6 +12850,20 @@
"node": ">=10.12.0" "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": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@ -12784,6 +13270,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zustand": {
"version": "4.5.7", "version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -31,6 +31,7 @@
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@notionhq/client": "^5.14.0", "@notionhq/client": "^5.14.0",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"@react-pdf/renderer": "^4.4.0",
"@tiptap/extension-link": "^3.20.0", "@tiptap/extension-link": "^3.20.0",
"@tiptap/extension-placeholder": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0",
"@tiptap/extension-underline": "^3.20.0", "@tiptap/extension-underline": "^3.20.0",

View 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 });
}
}

View File

@ -4,9 +4,9 @@ import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
// ── Field registry ───────────────────────────────────────────────────────── // ── 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 = export type ExportFieldKey =
| 'week'
| 'weekGoal'
| 'completedDate' | 'completedDate'
| 'scheduledDate' | 'scheduledDate'
| 'createdDate' | 'createdDate'
@ -19,34 +19,35 @@ export type ExportFieldKey =
| 'delegatedTo' | 'delegatedTo'
| 'delegationNote' | 'delegationNote'
| 'startTime' | 'startTime'
| 'kanbanStage' | 'kanbanStage';
| 'week';
export const EXPORT_FIELDS: { export const EXPORT_FIELDS: {
key: ExportFieldKey; key: ExportFieldKey;
labelDe: string; labelDe: string;
labelEn: string; labelEn: string;
defaultOn: boolean; 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: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true },
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true }, { key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true },
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true }, { key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false }, { key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true }, { key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true }, { key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true }, { key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false }, { key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false }, { key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false }, { key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false }, { key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false }, { key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false }, { key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', 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 d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7; const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum); d.setUTCDate(d.getUTCDate() + 4 - dayNum);
@ -55,41 +56,108 @@ function isoWeek(date: Date): string {
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
} }
function weekLabel(date: Date, de: boolean): string { export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } {
const week = isoWeek(date); const [year, w] = weekKey.split('-W');
const [year, w] = week.split('-W');
const d = new Date(Date.UTC(parseInt(year), 0, 1)); const d = new Date(Date.UTC(parseInt(year), 0, 1));
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1); d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6); 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 { mon: d, sun };
return de
? `KW ${w}${year} (${fmt(d)} ${fmt(sun)})`
: `Week ${w}${year} (${fmt(d)} ${fmt(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 ''; if (!d) return '';
return de return de
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) ? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', 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 != null && task.importance != null) {
if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now'; 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 ? 'Planen' : 'Schedule';
if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate'; if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
return de ? 'Eliminieren' : 'Eliminate'; return de ? 'Eliminieren' : 'Eliminate';
} }
if (task.priority) return task.priority; // A / B / C / D / E if (task.priority) return task.priority;
return ''; 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 { function csvCell(value: string): string {
return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`; 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) { export async function GET(request: Request) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
@ -99,13 +167,12 @@ export async function GET(request: Request) {
const lang = searchParams.get('lang') || 'de'; const lang = searchParams.get('lang') || 'de';
const de = lang === 'de'; const de = lang === 'de';
// Parse selected fields; fall back to all defaultOn fields
const fieldsParam = searchParams.get('fields'); const fieldsParam = searchParams.get('fields');
const selectedFields: Set<ExportFieldKey> = fieldsParam const selectedFields: Set<ExportFieldKey> = fieldsParam
? new Set(fieldsParam.split(',') as ExportFieldKey[]) ? new Set(fieldsParam.split(',') as ExportFieldKey[])
: new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)); : 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 }); 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 } }); const user = await prisma.user.findUnique({ where: { email: session.user.email } });
if (!user) return new NextResponse('User not found', { status: 404 }); if (!user) return new NextResponse('User not found', { status: 404 });
const where: any = { userId: user.id, completed: true, deletedAt: null }; const { byWeek, goalByWeek } = await fetchExportData(user.id, startDate, endDate);
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);
}
}
// 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]; const rows: string[][] = [headerRow];
// Group by ISO week (most recent first) for (const [weekKey, weekTasks] of byWeek) {
const byWeek = new Map<string, typeof tasks>(); // Week section header
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
if (selectedFields.has('week')) { if (selectedFields.has('week')) {
const label = weekLabel(weekTasks[0].updatedAt, de); rows.push([weekLabel(weekKey, de), ...Array(headerRow.length - 1).fill('')]);
// Fill entire row width with the label in first cell, rest blank }
rows.push([label, ...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) { for (const t of weekTasks) {
const row: string[] = [t.title || '']; const row: string[] = [t.title || ''];
for (const f of colFields) {
for (const f of EXPORT_FIELDS) {
if (!selectedFields.has(f.key) || f.key === 'week') continue;
switch (f.key) { switch (f.key) {
case 'completedDate': row.push(fmtDate(t.updatedAt, de)); break; case 'completedDate': row.push(fmtDate(t.updatedAt, de)); break;
case 'scheduledDate': row.push(fmtDate(t.scheduledDate ?? undefined, 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; case 'kanbanStage': row.push(t.kanbanStage || ''); break;
} }
} }
rows.push(row); rows.push(row);
} }
// Blank spacer between weeks
rows.push(Array(headerRow.length).fill('')); rows.push(Array(headerRow.length).fill(''));
} }
const csvContent = rows.map(row => row.map(csvCell).join(',')).join('\r\n'); const csvContent = rows.map(row => row.map(csvCell).join(',')).join('\r\n');
const bom = '\uFEFF'; 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 const filename = de
? `arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv` ? `arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv`
: `work-report${period}_${new Date().toISOString().split('T')[0]}.csv`; : `work-report${period}_${new Date().toISOString().split('T')[0]}.csv`;

View File

@ -4377,17 +4377,25 @@ function SettingsSidebar({
</div> </div>
</div> </div>
{/* Download button */} {/* Download buttons */}
<a <div style={{ display: "flex", gap: "8px" }}>
href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`} <a
target="_blank" href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
className="weekly-auth-button w-full justify-center" target="_blank"
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" }} 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" }}
> >
{profile.language === "de" <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>
? `Arbeitsbericht exportieren (${exportFields.size} Spalten)` CSV
: `Export Work Report (${exportFields.size} columns)`} </a>
</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> </div>
{/* Backup & Restore Section */} {/* Backup & Restore Section */}