From b95e4874f1b41b013385a965ed8170aebf174dad Mon Sep 17 00:00:00 2001 From: Athena Date: Tue, 19 May 2026 17:02:50 +0200 Subject: [PATCH] Add dashboard, API routes, Olga route, and supporting files Co-Authored-By: Claude Sonnet 4.6 --- package.json | 3 +- pnpm-lock.yaml | 313 +++++++ pnpm-workspace.yaml | 5 +- public/olga.jpg | Bin 0 -> 11843 bytes public/pm-kritisch.json | 5 + src/app/api/notion-pm/route.ts | 17 + .../dashboard/components/DashboardClient.tsx | 877 ++++++++++++++++++ src/app/dashboard/page.tsx | 42 + src/app/olga/layout.tsx | 50 + src/app/olga/page.tsx | 640 +++++++++++++ src/lib/notion-pm.ts | 108 +++ 11 files changed, 2058 insertions(+), 2 deletions(-) create mode 100755 public/olga.jpg create mode 100644 public/pm-kritisch.json create mode 100644 src/app/api/notion-pm/route.ts create mode 100644 src/app/dashboard/components/DashboardClient.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/olga/layout.tsx create mode 100644 src/app/olga/page.tsx create mode 100644 src/lib/notion-pm.ts diff --git a/package.json b/package.json index c7632bd..05af426 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dependencies": { "next": "16.2.4", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "recharts": "^3.8.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26a4883..3771da8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -429,9 +432,26 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -530,6 +550,33 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -550,6 +597,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.59.1': resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -841,6 +891,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -861,6 +915,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -893,6 +991,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -958,6 +1059,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1086,6 +1190,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1235,6 +1342,12 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.8: + resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1247,6 +1360,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -1677,10 +1794,38 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -1689,6 +1834,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1842,6 +1990,9 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -1913,6 +2064,14 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2303,8 +2462,24 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.8 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2383,6 +2558,30 @@ snapshots: tslib: 2.8.1 optional: true + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -2401,6 +2600,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2706,6 +2907,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2724,6 +2927,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -2752,6 +2993,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -2888,6 +3131,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.46.1: {} + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -3097,6 +3342,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3241,6 +3488,10 @@ snapshots: ignore@7.0.5: {} + immer@10.2.0: {} + + immer@11.1.8: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3254,6 +3505,8 @@ snapshots: hasown: 2.0.3 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -3664,8 +3917,43 @@ snapshots: react-is@16.13.1: {} + react-redux@9.3.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + react@19.2.4: {} + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.46.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 16.13.1 + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -3686,6 +3974,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3898,6 +4188,8 @@ snapshots: tapable@2.3.3: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -4013,6 +4305,27 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 581a9d5..dfa998a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ -ignoredBuiltDependencies: +allowBuilds: + sharp: set this to true or false + unrs-resolver: set this to true or false +onlyBuiltDependencies: - sharp - unrs-resolver diff --git a/public/olga.jpg b/public/olga.jpg new file mode 100755 index 0000000000000000000000000000000000000000..6a67c8f9a0ca1dee715b72e70527bdc1074ddd3a GIT binary patch literal 11843 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 literal 0 HcmV?d00001 diff --git a/public/pm-kritisch.json b/public/pm-kritisch.json new file mode 100644 index 0000000..3ee1d1b --- /dev/null +++ b/public/pm-kritisch.json @@ -0,0 +1,5 @@ +{ + "stand": "2026-05-18T22:00:00Z", + "kritisch": [], + "allesOk": true +} diff --git a/src/app/api/notion-pm/route.ts b/src/app/api/notion-pm/route.ts new file mode 100644 index 0000000..5364e12 --- /dev/null +++ b/src/app/api/notion-pm/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server' +import { fetchAllTodos } from '@/lib/notion-pm' + +export const revalidate = 300 + +export async function GET() { + try { + const todos = await fetchAllTodos() + return NextResponse.json({ + todos, + timestamp: new Date().toISOString(), + count: todos.length, + }) + } catch (err) { + return NextResponse.json({ error: String(err) }, { status: 500 }) + } +} diff --git a/src/app/dashboard/components/DashboardClient.tsx b/src/app/dashboard/components/DashboardClient.tsx new file mode 100644 index 0000000..1ad75f4 --- /dev/null +++ b/src/app/dashboard/components/DashboardClient.tsx @@ -0,0 +1,877 @@ +'use client' + +import { useState } from 'react' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, Cell, +} from 'recharts' +import { Todo } from '@/lib/notion-pm' +import { KritischData } from '../page' + +// ── MC CI ───────────────────────────────────────────────────────────────────── +const MC_BLUE = '#3B82F6' +const MC_PURPLE = '#8B5CF6' + +const KAT_COLOR: Record = { + 'Kundenarbeit': '#3B82F6', + 'Produkt & Launch': '#8B5CF6', + 'Marketing & Content': '#EC4899', + 'Vertrieb & Akquise': '#F97316', + 'Technik & Tools': '#6B7280', + 'Finanzen & Recht': '#EAB308', + 'Organisation & Admin': '#A16207', + 'Privat': '#22C55E', + 'Diana': '#EF4444', + 'Me Time': '#94A3B8', +} +function kc(k: string | null) { return k ? (KAT_COLOR[k] ?? MC_BLUE) : MC_BLUE } + +// ── Capacity constants ──────────────────────────────────────────────────────── +// Business: Mo–Fr 7h/Tag × 5 = 35h + WE Standard 5h = 40h/Woche +// Privat: Mo–Fr 1.5h/Tag × 5 = 7.5h + WE 7h (3–4h/Tag) = 14.5h/Woche +const BUSINESS_CAP = 40 +const PRIVAT_CAP = 14.5 +const REAL_CAP = BUSINESS_CAP + PRIVAT_CAP // 54.5h gesamt (für Machbarkeit) + +const BUSINESS_KAT = new Set([ + 'Kundenarbeit', 'Produkt & Launch', 'Marketing & Content', + 'Vertrieb & Akquise', 'Technik & Tools', 'Finanzen & Recht', 'Organisation & Admin', +]) +const PRIVAT_KAT = new Set(['Privat', 'Diana', 'Me Time']) + +// ── Helpers ─────────────────────────────────────────────────────────────────── +const isErledigt = (t: Todo) => (t.status ?? '').toLowerCase().includes('erledigt') +const daysFromNow = (d: string) => { + const now = new Date(); now.setHours(0,0,0,0) + return Math.floor((new Date(d).getTime() - now.getTime()) / 86_400_000) +} +const round1 = (n: number) => Math.round(n * 10) / 10 + +// Wochenplan-Filter: Geplanter Tag muss in den nächsten 7 Tagen liegen. +// Kein Geplanter Tag aber Im Wochenplan = true → manuell gesetzt, zählt trotzdem. +const isThisWeek = (t: Todo) => { + if (!t.geplanterTag) return t.imWochenplan + const d = daysFromNow(t.geplanterTag) + return d >= 0 && d <= 6 +} + +// ── Analytics ───────────────────────────────────────────────────────────────── + +function kapazitaet(todos: Todo[]) { + const week = todos.filter(t => isThisWeek(t) && !isErledigt(t)) + + const businessH = round1(week.filter(t => BUSINESS_KAT.has(t.kategorie ?? '')).reduce((s,t) => s + (t.zeitH ?? 0), 0)) + const privatH = round1(week.filter(t => PRIVAT_KAT.has(t.kategorie ?? '')).reduce((s,t) => s + (t.zeitH ?? 0), 0)) + + const businessPct = Math.round((businessH / BUSINESS_CAP) * 100) + const privatPct = Math.round((privatH / PRIVAT_CAP) * 100) + + const bColor = businessPct <= 80 ? '#22C55E' : businessPct <= 100 ? '#EAB308' : '#EF4444' + const pColor = privatPct <= 80 ? '#22C55E' : privatPct <= 100 ? '#EAB308' : '#EF4444' + + // legacy compat for other functions that use .geplant/.pct/.color/.label + const geplant = round1(businessH + privatH) + const pct = Math.round((geplant / REAL_CAP) * 100) + const color = pct <= 80 ? '#22C55E' : pct <= 100 ? '#EAB308' : '#EF4444' + const label = pct <= 80 ? 'Im Rahmen' : pct <= 100 ? 'Grenzwertig' : 'Überlastet' + + return { businessH, privatH, businessPct, privatPct, bColor, pColor, geplant, pct, color, label } +} + +function machbarkeit(todos: Todo[]) { + const week = todos + .filter(t => isThisWeek(t) && !isErledigt(t)) + .sort((a,b) => { + // höchste Prio zuerst + const prio = (x: Todo) => x.prioritaet === 'Hoch' ? 0 : x.prioritaet === 'Mittel' ? 1 : 2 + return prio(a) - prio(b) + }) + + let budget = REAL_CAP + const machbar: Todo[] = [] + const overflow: Todo[] = [] + + for (const t of week) { + const h = t.zeitH ?? 0.5 + if (budget >= h) { machbar.push(t); budget -= h } + else overflow.push(t) + } + + const score = week.length === 0 ? 100 + : Math.round((machbar.length / week.length) * 100) + + return { machbar, overflow, score, restH: round1(budget) } +} + +function energiebloecke(todos: Todo[]) { + const open = todos.filter(t => !isErledigt(t) && isThisWeek(t)) + const fokus = open.filter(t => (t.zeitH ?? 0) >= 2 || t.prioritaet === 'Hoch') + const leicht = open.filter(t => !fokus.includes(t) && (t.zeitH ?? 1) < 1) + const mittel = open.filter(t => !fokus.includes(t) && !leicht.includes(t)) + return { fokus, mittel, leicht } +} + +function carryoverRate(todos: Todo[]) { + // Todos mit vergangener Deadline die NICHT erledigt sind + const withDeadline = todos.filter(t => t.deadline) + const overdue = withDeadline.filter(t => !isErledigt(t) && daysFromNow(t.deadline!) < 0) + const rate = withDeadline.length === 0 ? 0 + : Math.round((overdue.length / withDeadline.length) * 100) + return { rate, overdueCount: overdue.length, total: withDeadline.length } +} + +function deadlineRisiko(todos: Todo[]) { + const projekte: Record = {} + for (const t of todos.filter(t => !isErledigt(t))) { + if (!projekte[t.projekt]) projekte[t.projekt] = { riskCount: 0, total: 0 } + projekte[t.projekt].total++ + if (t.deadline) { + const d = daysFromNow(t.deadline) + if (d < 0 || d <= 7) projekte[t.projekt].riskCount++ + } + } + return Object.entries(projekte) + .map(([p, v]) => ({ + projekt: p, + risikoPct: v.total === 0 ? 0 : Math.round((v.riskCount / v.total) * 100), + riskCount: v.riskCount, + })) + .filter(r => r.riskCount > 0) + .sort((a,b) => b.risikoPct - a.risikoPct) + .slice(0, 5) +} + +interface AthenaEmpfehlung { + icon: string + text: string + color: string + items?: string[] // aufklappbare Liste — eine Aufgabe pro Zeile +} + +// Sortiert nach nächstem anstehendem Datum (geplanterTag → deadline → hinten) +function sortByNextDate(a: Todo, b: Todo): number { + const dateOf = (t: Todo) => t.geplanterTag ?? t.deadline ?? null + const da = dateOf(a), db = dateOf(b) + if (da && db) return new Date(da).getTime() - new Date(db).getTime() + if (da) return -1 + if (db) return 1 + return 0 +} + +function athenaEmpfehlungen(todos: Todo[]): AthenaEmpfehlung[] { + const tips: AthenaEmpfehlung[] = [] + const kap = kapazitaet(todos) + const mb = machbarkeit(todos) + const cr = carryoverRate(todos) + + // 1. Überlastung + if (kap.pct > 100) { + const overflow = mb.overflow + if (overflow.length > 0) { + tips.push({ + icon: '↩', + text: `${overflow.length} Todo${overflow.length > 1 ? 's' : ''} auf nächste Woche verschieben`, + color: '#EF4444', + items: overflow.map(t => `${t.name.slice(0,50)} (${t.zeitH ?? '?'}h)`), + }) + } + } + + // 2. Hohe Carry-over Rate + if (cr.rate > 30) { + tips.push({ + icon: '⚡', + text: `Carry-over Rate ${cr.rate}% — ${cr.overdueCount} überfällige Todos priorisieren`, + color: '#EAB308', + }) + } + + // 3. Kundenarbeit unterrepräsentiert + const weekH = (kat: string) => todos + .filter(t => isThisWeek(t) && !isErledigt(t) && t.kategorie === kat) + .reduce((s,t) => s + (t.zeitH ?? 0), 0) + const kundenH = weekH('Kundenarbeit') + const totalWeekH = todos.filter(t => isThisWeek(t) && !isErledigt(t)) + .reduce((s,t) => s + (t.zeitH ?? 0), 0) + if (totalWeekH > 0 && kundenH / totalWeekH < 0.25) { + tips.push({ + icon: '👥', + text: `Kundenarbeit nur ${Math.round((kundenH/totalWeekH)*100)}% — prüfe ob Kunden-Todos fehlen`, + color: MC_BLUE, + }) + } + + // 4. Fokus-Todos ohne feste Zeit + const fokusOhneZeit = todos.filter(t => + isThisWeek(t) && !isErledigt(t) && t.prioritaet === 'Hoch' && !t.zeitH + ) + if (fokusOhneZeit.length > 0) { + tips.push({ + icon: '🎯', + text: `${fokusOhneZeit.length} Hoch-Prio Todos ohne Zeitschätzung`, + color: MC_PURPLE, + items: fokusOhneZeit.map(t => t.name.slice(0, 52)), + }) + } + + // 5. Me Time fehlt + const meTodos = todos.filter(t => !isErledigt(t) && t.kategorie === 'Me Time') + if (meTodos.length === 0) { + tips.push({ + icon: '🧘', + text: `Me Time Zeitslot noch offen — bitte definieren`, + color: '#94A3B8', + }) + } + + // 6. Business-Puffer: nächstanstehende Todos vorschlagen + const businessRest = round1(BUSINESS_CAP - kap.businessH) + if (businessRest >= 1) { + const kandidaten = todos + .filter(t => !isErledigt(t) && !isThisWeek(t) && BUSINESS_KAT.has(t.kategorie ?? '') && (t.zeitH ?? 0) <= businessRest) + .sort(sortByNextDate) + .slice(0, 8) + if (kandidaten.length > 0) { + tips.push({ + icon: '➕', + text: `Business: ${businessRest}h Puffer frei — nächstanstehende Vorschläge`, + color: '#22C55E', + items: kandidaten.map(t => { + const datum = t.geplanterTag ?? t.deadline + const datumStr = datum ? ` · ${datum.slice(5)}` : '' + return `${t.name.slice(0,50)} (${t.zeitH ?? '?'}h${datumStr})` + }), + }) + } + } + + // 7. Privat-Puffer: nächstanstehende Todos vorschlagen + const privatRest = round1(PRIVAT_CAP - kap.privatH) + if (privatRest >= 0.5) { + const kandidaten = todos + .filter(t => !isErledigt(t) && !isThisWeek(t) && PRIVAT_KAT.has(t.kategorie ?? '') && (t.zeitH ?? 0) <= privatRest) + .sort(sortByNextDate) + .slice(0, 6) + if (kandidaten.length > 0) { + tips.push({ + icon: '🏠', + text: `Privat: ${privatRest}h Puffer frei — nächstanstehende Vorschläge`, + color: '#8B5CF6', + items: kandidaten.map(t => { + const datum = t.geplanterTag ?? t.deadline + const datumStr = datum ? ` · ${datum.slice(5)}` : '' + return `${t.name.slice(0,50)} (${t.zeitH ?? '?'}h${datumStr})` + }), + }) + } + } + + // 8. Alles gut + if (tips.length === 0) { + tips.push({ icon: '✓', text: `Alles im Rahmen — Kapazität ${kap.pct}%, Carry-over ${cr.rate}%. Weiter so!`, color: '#22C55E' }) + } + + return tips +} + +function kategorienData(todos: Todo[]) { + const map: Record = {} + for (const t of todos) { + if (!t.kategorie || isErledigt(t)) continue + map[t.kategorie] = (map[t.kategorie] ?? 0) + (t.zeitH ?? 0) + } + return Object.entries(map) + .map(([name, stunden]) => ({ name, stunden: round1(stunden) })) + .sort((a,b) => b.stunden - a.stunden) +} + +function top5(todos: Todo[]) { + return todos + .filter(t => !isErledigt(t) && t.deadline) + .sort((a,b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime()) + .slice(0, 5) + .map(t => ({ ...t, daysLeft: daysFromNow(t.deadline!) })) +} + +function fruehwarnung(todos: Todo[]) { + const active = todos.filter(t => !isErledigt(t) && t.deadline) + return { + overdue: active.filter(t => daysFromNow(t.deadline!) < 0), + upcoming: active.filter(t => { const d = daysFromNow(t.deadline!); return d >= 0 && d <= 3 }), + } +} + +function zeitdifferenz(todos: Todo[]) { + const map: Record = {} + for (const t of todos) { + if (!map[t.projekt]) map[t.projekt] = { geplant: 0, tatsaechlich: 0 } + map[t.projekt].geplant += t.zeitH ?? 0 + map[t.projekt].tatsaechlich += t.tatsaechlicheZeitH ?? 0 + } + return Object.entries(map) + .map(([p, v]) => ({ + projekt: p, + geplant: round1(v.geplant), + tatsaechlich: round1(v.tatsaechlich), + diff: round1(v.geplant - v.tatsaechlich), + })) + .filter(r => r.geplant > 0 || r.tatsaechlich > 0) +} + +function meilensteinKetten(todos: Todo[]) { + const map: Record = {} + for (const t of todos.filter(t => !isErledigt(t) && t.meilenstein)) { + const key = `${t.projekt} › ${t.meilenstein}` + if (!map[key]) map[key] = { projekt: t.projekt, todos: [], overdueCount: 0 } + map[key].todos.push(t) + if (t.deadline && daysFromNow(t.deadline) < 0) map[key].overdueCount++ + } + return Object.entries(map) + .map(([label, v]) => ({ label, ...v })) + .sort((a,b) => b.overdueCount - a.overdueCount) + .slice(0, 6) +} + +// ── UI Primitives ───────────────────────────────────────────────────────────── + +function Card({ children, className }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +function Label({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function Badge({ color, children }: { color: string; children: React.ReactNode }) { + return ( + + {children} + + ) +} + +// ── Section: Kapazität + Puffer ─────────────────────────────────────────────── +function KapazitaetsAmpel({ todos }: { todos: Todo[] }) { + const { businessH, privatH, businessPct, privatPct, bColor, pColor } = kapazitaet(todos) + + function Bar({ label, geplant, cap, pct, color, hint }: { + label: string; geplant: number; cap: number; pct: number; color: string; hint: string + }) { + const barW = Math.min(pct, 100) + const statusLabel = pct <= 80 ? 'Im Rahmen' : pct <= 100 ? 'Grenzwertig' : 'Überlastet' + return ( +
+
+ {label} + {hint} +
+
+ {geplant}h + / {cap}h +
+
+
+
+
+ {statusLabel} ({pct}%) + {pct <= 80 ? '🟢' : pct <= 100 ? '🟡' : '🔴'} +
+
+ ) + } + + return ( + + + + +
+
+ Business (35h Mo–Fr + 5h WE){BUSINESS_CAP}h + Privat (7.5h Mo–Fr + 7h WE){PRIVAT_CAP}h + 3 Wochenendtage/Monat freiblockiert +
+
+
+ ) +} + +// ── Section: Machbarkeitsanalyse ────────────────────────────────────────────── +function Machbarkeit({ todos }: { todos: Todo[] }) { + const { machbar, overflow, score } = machbarkeit(todos) + const scoreColor = score >= 80 ? '#22C55E' : score >= 50 ? '#EAB308' : '#EF4444' + const scoreLabel = score >= 80 ? 'Machbar' : score >= 50 ? 'Knapp' : 'Nicht machbar' + + return ( + + +
+
+
{score}%
+
{scoreLabel}
+
+
+
+ {machbar.length} Todos passen rein + {overflow.length > 0 && · {overflow.length} fallen raus} +
+
+
+
+
+
+ + {overflow.length > 0 && ( +
+

+ Passt diese Woche nicht rein: +

+ {overflow.slice(0, 4).map(t => ( +
+ {t.name.slice(0,42)} + {t.zeitH ?? '?'}h +
+ ))} + {overflow.length > 4 &&

+{overflow.length-4} weitere…

} +
+ )} + + ) +} + +// ── Section: ATHENA Empfehlungen ────────────────────────────────────────────── +function AthenaEmpfehlungen({ todos }: { todos: Todo[] }) { + const tips = athenaEmpfehlungen(todos) + const [expanded, setExpanded] = useState>({}) + const SHOW = 3 + + return ( + + +
+ {tips.map((tip, i) => { + const hasItems = tip.items && tip.items.length > 0 + const isOpen = expanded[i] ?? false + const visible = hasItems ? (isOpen ? tip.items! : tip.items!.slice(0, SHOW)) : [] + const hiddenCount = hasItems ? tip.items!.length - SHOW : 0 + + return ( +
+
+ {tip.icon} + {tip.text} +
+ {hasItems && ( +
+ {visible.map((item, j) => ( +
+ {item} +
+ ))} + {hiddenCount > 0 && !isOpen && ( + + )} + {isOpen && tip.items!.length > SHOW && ( + + )} +
+ )} +
+ ) + })} +
+
+ ) +} + +// ── Section: Predictive Analytics ──────────────────────────────────────────── +function PredictiveAnalytics({ todos }: { todos: Todo[] }) { + const cr = carryoverRate(todos) + const risks = deadlineRisiko(todos) + const trend = cr.rate === 0 ? { label:'Kein Rückstand', color:'#22C55E', icon:'↗' } + : cr.rate < 20 ? { label:'Stabil', color:'#22C55E', icon:'→' } + : cr.rate < 40 ? { label:'Leichter Rückstand', color:'#EAB308', icon:'↘' } + : { label:'Kritischer Rückstand', color:'#EF4444', icon:'↓' } + + return ( + + + +
+ {/* Carry-over Rate */} +
+

Carry-over Rate

+

{cr.rate}%

+

{cr.overdueCount} / {cr.total} Todos überfällig

+
+ {/* Trend */} +
+

Trend

+

{trend.icon}

+

{trend.label}

+
+
+ + {/* Deadline-Risiko je Projekt */} + {risks.length > 0 && ( + <> +

Deadline-Risiko je Projekt

+ {risks.map(r => ( +
+ {r.projekt} +
+
+
50 ? '#EF4444' : '#EAB308' }} /> +
+ 50 ? '#EF4444' : '#EAB308', fontWeight:700, width:'2.5rem', textAlign:'right' }}>{r.risikoPct}% +
+
+ ))} + + )} + + ) +} + +// ── Section: Energieblöcke ──────────────────────────────────────────────────── +function Energiebloecke({ todos }: { todos: Todo[] }) { + const { fokus, mittel, leicht } = energiebloecke(todos) + + function Block({ label, color, items, hint }: { label:string; color:string; items:Todo[]; hint:string }) { + return ( +
+
+
+ {label} + ({items.length}) +
+
{hint}
+ {items.slice(0,3).map(t => ( +
+ {t.name.slice(0,32)} + {t.zeitH ? `${t.zeitH}h` : '?h'} +
+ ))} + {items.length > 3 &&

+{items.length-3} weitere

} +
+ ) + } + + return ( + + +
+ + + +
+
+ ) +} + +// ── Section: Abhängigkeits-Tracker ──────────────────────────────────────────── +function AbhaengigkeitsTracker({ todos }: { todos: Todo[] }) { + const ketten = meilensteinKetten(todos) + return ( + + + {ketten.length === 0 + ?

Keine offenen Meilensteine

+ : ketten.map(k => ( +
+
+ {k.label} + {k.overdueCount > 0 && {k.overdueCount} überfällig} +
+
+ {k.todos.slice(0,3).map(t => ( +
+ + {t.name.slice(0,44)} +
+ ))} + {k.todos.length > 3 &&

+{k.todos.length-3} weitere

} +
+
+ )) + } +
+ ) +} + +// ── Section: Kategorien-Chart ───────────────────────────────────────────────── +function KategorienChart({ todos }: { todos: Todo[] }) { + const data = kategorienData(todos) + return ( + + + {data.length === 0 + ?

Keine Daten

+ : + + + + + [`${v}h`, 'Stunden']} /> + + {data.map(e => )} + + + + } +
+ ) +} + +// ── Section: Top 5 Deadlines ────────────────────────────────────────────────── +function Top5({ todos }: { todos: Todo[] }) { + const items = top5(todos) + function dl(days: number) { + if (days < 0) return { text:`${Math.abs(days)}d überfällig`, color:'#EF4444' } + if (days === 0) return { text:'Heute', color:'#EAB308' } + if (days <= 3) return { text:`in ${days}d`, color:'#EAB308' } + return { text:`in ${days}d`, color:'#22C55E' } + } + return ( + + + {items.length === 0 + ?

Keine offenen Todos mit Deadline

+ : items.map((t,i) => { + const d = dl(t.daysLeft) + return ( +
+ {i+1}. +
+

{t.name.slice(0,48)}

+

{t.projekt}{t.meilenstein ? ` · ${t.meilenstein}` : ''}

+
+ {d.text} +
+ ) + }) + } +
+ ) +} + +// ── Section: Frühwarnsystem ─────────────────────────────────────────────────── +function Fruehwarnsystem({ todos }: { todos: Todo[] }) { + const { overdue, upcoming } = fruehwarnung(todos) + const kap = kapazitaet(todos) + const warnings = [] + if (overdue.length > 0) warnings.push({ text:`${overdue.length} überfällige Todos`, color:'#EF4444' }) + if (upcoming.length > 0) warnings.push({ text:`${upcoming.length} Todos fällig in ≤ 3 Tagen`, color:'#EAB308' }) + if (kap.pct > 100) warnings.push({ text:`Überlastet: ${kap.pct}% der Kapazität`, color:'#EF4444' }) + else if (kap.pct > 80) warnings.push({ text:`Grenzwertig: ${kap.pct}% der Kapazität`, color:'#EAB308' }) + + return ( + + + {warnings.length === 0 + ?

✓ Alles im Rahmen

+ :
+ {warnings.map((w,i) => ( +
+ {w.text} +
+ ))} +
+ } + {overdue.slice(0,3).map(t => ( +

· {t.name.slice(0,46)} ({t.projekt})

+ ))} +
+ ) +} + +// ── Section: Diana ──────────────────────────────────────────────────────────── +function DianaSection({ todos }: { todos: Todo[] }) { + const diana = todos.filter(t => t.kategorie === 'Diana' && !isErledigt(t)) + if (diana.length === 0) return null + return ( + + + {diana.map(t => ( +
+ {t.name.slice(0,48)} + {t.deadline && {t.deadline.slice(5)}} +
+ ))} +
+ ) +} + +// ── Section: Me Time ────────────────────────────────────────────────────────── +function MeTime({ todos }: { todos: Todo[] }) { + const meTodos = todos.filter(t => t.kategorie === 'Me Time' && !isErledigt(t)) + return ( + + +
+

⚠ Noch nicht definiert

+

Zeitslot für Me Time ist noch offen — bitte planen

+
+ {meTodos.length === 0 + ?

Keine Me-Time-Todos diese Woche

+ :

{meTodos.length} Me-Time-Todos eingetragen

+ } +
+ ) +} + +// ── Section: Zeitdifferenz ──────────────────────────────────────────────────── +function Zeitdifferenz({ todos }: { todos: Todo[] }) { + const rows = zeitdifferenz(todos).sort((a,b) => Math.abs(b.diff) - Math.abs(a.diff)) + return ( + + + {rows.map(r => { + const dc = r.diff >= 0 ? '#22C55E' : '#EF4444' + const ds = r.diff >= 0 ? '+' : '' + return ( +
+ {r.projekt} + {r.geplant}h + {r.tatsaechlich}h + {ds}{r.diff}h +
+ ) + })} +
+ GeplantTatsächl.Diff +
+
+ ) +} + +// ── Section: Kritische Stellen ──────────────────────────────────────────────── +function KritischeStellen({ data }: { data: KritischData }) { + const uhrzeit = new Date(data.stand).toLocaleString('de-DE', { + day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' + }) + + if (data.allesOk) { + return ( +
+
+ + Alles im Rahmen — keine kritischen Stellen +
+ Stand: {uhrzeit} Uhr +
+ ) + } + + const farbenMap: Record = { rot: '#EF4444', gelb: '#EAB308' } + const typIcon: Record = { + ueberfallig: '🔴', deadline: '⚠', meilenstein: '🎯', kapazitaet: '📊' + } + + return ( +
+
+ + Kritische Stellen ({data.kritisch.length}) + + Stand: {uhrzeit} Uhr +
+
+ {data.kritisch.map((e, i) => { + const color = farbenMap[e.farbe] ?? '#EF4444' + return ( +
+ {typIcon[e.typ] ?? '⚠'} +
+ {e.projekt} + {e.text} +
+
+ ) + })} +
+
+ ) +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +export default function DashboardClient({ todos, fetchedAt, kritischData }: { todos: Todo[]; fetchedAt: string; kritischData: KritischData }) { + const totalOpen = todos.filter(t => !isErledigt(t)).length + const uhrzeit = new Date(fetchedAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + + return ( +
+ + {/* Header */} +
+
+
+

ATHENA PM Dashboard

+

Market Compass · Live Notion PM

+
+
+

Aktualisiert: {uhrzeit} Uhr

+

{totalOpen} offene Todos · 10 Projekte

+
+
+
+ + {/* Grid */} +
+ + {/* Row 0: Kritische Stellen */} + + + {/* Row 1: Kapazität + Machbarkeit + ATHENA Empfehlungen */} +
+ + + +
+ + {/* Row 2: Predictive Analytics + Energieblöcke + Me Time */} +
+ + +
+ + +
+
+ + {/* Row 3: Kategorien-Chart + Top 5 */} +
+ + +
+ + {/* Row 4: Frühwarnsystem + Zeitdifferenz + Abhängigkeiten */} +
+ + + +
+ +
+
+ ) +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..00e8597 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,42 @@ +import { readFileSync } from 'fs' +import { join } from 'path' +import { fetchAllTodos } from '@/lib/notion-pm' +import DashboardClient from './components/DashboardClient' + +export const revalidate = 300 + +export const metadata = { + title: 'ATHENA PM Dashboard | Market Compass', +} + +export interface KritischEintrag { + typ: 'deadline' | 'meilenstein' | 'kapazitaet' | 'ueberfallig' + projekt: string + text: string + farbe: 'rot' | 'gelb' +} + +export interface KritischData { + stand: string + kritisch: KritischEintrag[] + allesOk: boolean +} + +function loadKritischData(): KritischData { + try { + const filePath = join(process.cwd(), 'public', 'pm-kritisch.json') + const raw = readFileSync(filePath, 'utf-8') + return JSON.parse(raw) as KritischData + } catch { + return { stand: new Date().toISOString(), kritisch: [], allesOk: true } + } +} + +export default async function DashboardPage() { + const [todos, kritischData] = await Promise.all([ + fetchAllTodos(), + Promise.resolve(loadKritischData()), + ]) + const timestamp = new Date().toISOString() + return +} diff --git a/src/app/olga/layout.tsx b/src/app/olga/layout.tsx new file mode 100644 index 0000000..55c52eb --- /dev/null +++ b/src/app/olga/layout.tsx @@ -0,0 +1,50 @@ +import type { Metadata } from "next" +import { Cormorant_Garamond, DM_Sans } from "next/font/google" + +const cormorant = Cormorant_Garamond({ + variable: "--font-cormorant", + subsets: ["latin"], + weight: ["300", "400", "500", "600"], + style: ["normal", "italic"], +}) + +const dmSans = DM_Sans({ + variable: "--font-dm-sans", + subsets: ["latin"], + weight: ["300", "400", "500", "600"], +}) + +export const metadata: Metadata = { + title: "Skin Signal Deep Scan — Olga Izieva", + description: + "In 90 Minuten weißt du endlich, was dein Körper dir die ganze Zeit sagen wollte. Skin Signal Deep Scan von Olga Izieva — systemische Körperanalyse für Frauen 35+.", +} + +export default function OlgaLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ + {children} +
+ ) +} diff --git a/src/app/olga/page.tsx b/src/app/olga/page.tsx new file mode 100644 index 0000000..b7bf8d1 --- /dev/null +++ b/src/app/olga/page.tsx @@ -0,0 +1,640 @@ +"use client" + +import { useState } from "react" +import Image from "next/image" + +const C = { + dark: "#1C2420", + green: "#2D4A3E", + greenLight: "#3D5C4E", + cognac: "#A06040", + cognacLight: "#C4845A", + parchment: "#F0EBE1", + parchmentMid: "#E8E0D4", + parchmentDark: "#DDD4C4", + brown: "#1A1210", + textDark: "#2A2218", + textMuted: "#7A6E62", + white: "#FDFAF6", + sage: "#EBF0ED", +} + +function StrataDecor({ color = C.cognac, opacity = 0.2 }: { color?: string; opacity?: number }) { + return ( + + ) +} + +const body: React.CSSProperties = { + fontFamily: "var(--font-dm-sans, system-ui, sans-serif)", + color: C.textDark, + lineHeight: 1.7, +} + +const serif: React.CSSProperties = { + fontFamily: "var(--font-cormorant, Georgia, serif)", +} + +export default function OlgaDeepScanPage() { + const [formData, setFormData] = useState({ name: "", email: "", phone: "", message: "" }) + const [submitted, setSubmitted] = useState(false) + + function handleChange(e: React.ChangeEvent) { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })) + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setSubmitted(true) + } + + return ( +
+ + {/* ── HEADER ─────────────────────────────────────────────── */} +
+
+

+ Olga Izieva  ·  Skin Signal Method™ +

+
+
+ + {/* ── HERO ───────────────────────────────────────────────── */} +
+
+ + + Nur 5 Plätze verfügbar + + +

+ In 90 Minuten weißt du endlich, was dein Körper dir die ganze Zeit sagen wollte +

+ +

+ Der Skin Signal Deep Scan — eine systemische Körperanalyse, die Haut, Hormone, Darm, Schlaf und Stress als ein zusammenhängendes Bild liest. +

+ +

+ Olga Izieva  ·  33 Jahre klinische Erfahrung  ·  Heilpraktikerin  ·  Medical Cosmetology +

+ + + Meinen Platz sichern → + + +

+ Pilotpreis 197 €  ·  persönlich bei Olga  ·  5 Plätze +

+ +
+ +
+
+
+ + {/* ── PROBLEM ────────────────────────────────────────────── */} +
+
+ +

+ Kommt dir das bekannt vor? +

+

+ Du tust alles richtig — und trotzdem stimmt irgendetwas nicht. +

+ +
+ {[ + ["Haut die müde wirkt", "Trotz Pflege, Supplements und clean eating — sie strahlt einfach nicht mehr so wie früher."], + ["Ärzte sagen: alles normal", "Die Blutwerte sind unauffällig. Aber du fühlst, dass etwas nicht stimmt — und niemand kann dir sagen was."], + ["Du hast schon alles probiert", "Keto, Yoga, Schlaftracking, Vitamin D, Kollagen. Jedes Mal kurze Besserung. Dann wieder Ausgangszustand."], + ["Niemand sieht das Gesamtbild", "Jeder schaut auf seinen Bereich. Haut oder Hormone oder Darm. Nie alles zusammen. Nie du."], + ].map(([title, text], i) => ( +
+
+ {String(i + 1).padStart(2, "0")} +
+

{title}

+

{text}

+
+ ))} +
+
+
+ + {/* ── EPIPHANY ───────────────────────────────────────────── */} +
+
+ +
+ +
+ „Die Haut ist keine Krankheit. Sie ist die Botschaft." +
+ +

+ Olga Izieva sieht seit 33 Jahren, was Ärzte nicht sehen wollen: Hautprobleme sind keine Oberflächenfrage. Sie zeigen, was im Körper wirklich passiert. +

+

+ Als ihre eigene Haut durch Stress zusammenbrach und niemand helfen konnte, entwickelte sie die Skin Signal Method™. Ein systemischer Ansatz, der Haut als das liest, was sie ist: das ehrlichste Diagnoseinstrument, das wir haben. +

+
+
+ + {/* ── VALUE STACK ────────────────────────────────────────── */} +
+
+ +

+ Was du bekommst +

+

+ Skin Signal Deep Scan +

+

+ 90 Minuten. Vollständige systemische Analyse. Alles schriftlich. +

+ +
+ {[ + ["Visuelle Körperanalyse", "Haut, Zunge, Nägel, Haare, Lippen — Olga liest alle äußeren Signale deines Körpers", "150 €"], + ["Systemische Anamnese", "Hormone, Darm, Schlaf, Stress, Ernährung, Bewegung, Medikamente — beide vollständigen Fragebögen", "180 €"], + ["Schriftliche System-Hypothese", "Olgas Einschätzung was systemisch hinter deinen Symptomen steckt — benannt, nicht vermutet", "220 €"], + ["7-Tage-Sofortplan", "Drei bis fünf konkrete Schritte die heute noch beginnen können — schriftlich", "102 €"], + ["Klare Einschätzung nächster Schritt", "Du weißt danach genau was als nächstes kommt — Olga schlägt die konkrete Richtung vor", "50 €"], + ].map(([title, desc, value], i) => ( +
+
+

{title}

+

{desc}

+
+

+ {value} +

+
+ ))} +
+ + {/* Preisblock */} +
+
+

Gesamtwert

+

702 €

+
+
+

Pilotpreis — für 5 Bestandskundinnen

+

197 €

+
+
+ +

+ Das ist der erste strukturierte Durchlauf der Skin Signal Method™. Du bekommst die vollständige Analyse. Im Gegenzug nimmt sich Olga die Zeit, die sie im regulären Praxiskontext nicht hätte. +

+ + +
+
+ + {/* ── TRANSFORMATION ─────────────────────────────────────── */} +
+
+

+ Was danach anders ist +

+ +
+

+ „Nach diesem Gespräch weißt du endlich, was dein Körper dir die ganze Zeit sagen wollte." +

+

+ Die Haut die müde wirkt, die Energie die fehlt, das Gewicht das sich trotz allem nicht bewegt — du verstehst jetzt das Muster dahinter. Olga hat dich als Gesamtbild gelesen: Haut, Zunge, Nägel, Haare, Darm, Hormone, Schlaf, Stress — alles zusammen. +

+

+ Du verlässt das Gespräch nicht mit einem weiteren Ratschlag. Sondern mit einer klaren Einschätzung was systemisch hinter deinen Symptomen steckt — und einem 7-Tage-Plan der heute noch startet.{" "} + Dein Körper war nie dein Feind. Er hat nur auf Klarheit gewartet. +

+
+
+
+ + {/* ── ÜBER OLGA ──────────────────────────────────────────── */} +
+
+

+ Über Olga Izieva +

+ +
+ {/* Foto */} +
+
+ Olga Izieva +
+
+ 33 Jahre
Erfahrung +
+
+ + {/* Text */} +
+

+ Die Frau, die deinen Körper liest wie niemand sonst +

+ +

+ Olga Izieva hat 33 Jahre als Kosmetikerin gearbeitet — und dabei täglich gesehen, was Ärzte nicht sehen wollten: Die Haut ist kein kosmetisches Problem. Sie ist ein systemisches Signal. +

+

+ Sie ist Heilpraktikerin, hat Zertifikate in Ernährungswissenschaft, Psychologie und Medical Cosmetology. Sie koordiniert Ärzte, Spezialisten und Coaches — der einzige Hub, der den visuellen Diagnosezugang der Kosmetik mit systemischer Medizin verbindet. +

+ +
+ {[ + "33 Jahre klinische Praxis als Kosmetikerin", + "Ausgebildete Heilpraktikerin", + "Zertifikate in Ernährungswissenschaft & Psychologie", + "Medical Cosmetology", + "Skin Signal Method™ — aus der eigenen Praxis heraus entwickelt", + ].map((point, i) => ( +
+ +

{point}

+
+ ))} +
+
+
+
+
+ + {/* ── GARANTIE ───────────────────────────────────────────── */} +
+
+

+ Olgas Versprechen +

+

+ Ohne Wenn und Aber. +

+ +
+ {[ + { + label: "Klarheits-Garantie", + text: "Wenn du nach unserem Gespräch nicht weißt, was dein nächster konkreter Schritt ist — wir machen es nochmal. Kostenlos.", + }, + { + label: "7-Tage-Garantie", + text: "Setz den 7-Tage-Sofortplan um. Wenn du nach einer Woche keine spürbare Veränderung merkst, bekommst du ein kostenloses 20-Minuten-Nachgespräch dazu.", + }, + ].map((g, i) => ( +
+

+ {g.label} +

+

+ „{g.text}" +

+
+ ))} +
+
+
+ + {/* ── ORDER FORM ─────────────────────────────────────────── */} +
+
+

+ Platz sichern +

+

+ Dein Platz im Skin Signal Deep Scan +

+

+ Füll das Formular aus — Olga meldet sich persönlich bei dir für die Terminvereinbarung. +

+

+ Pilotpreis 197 €  ·  90 Minuten  ·  5 Plätze +

+ + {submitted ? ( +
+

+ Anfrage eingegangen. +

+

+ Olga meldet sich innerhalb von 24 Stunden persönlich bei dir mit allen Details zum Termin. +

+
+ ) : ( +
+ {[ + { name: "name", label: "Dein Name", type: "text", placeholder: "Vorname Nachname", required: true }, + { name: "email", label: "E-Mail-Adresse", type: "email", placeholder: "deine@email.de", required: true }, + { name: "phone", label: "Telefonnummer (optional)", type: "tel", placeholder: "+49 ...", required: false }, + ].map((field) => ( +
+ + +
+ ))} + +
+ +