diff --git a/package-lock.json b/package-lock.json index a307151..d49d078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", + "recharts": "^3.10.1", "resend": "^6.18.0", "sonner": "^2.0.7", "stripe": "^22.1.1", @@ -1602,6 +1603,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -4146,6 +4158,32 @@ "@react-pdf/stylesheet": "^6.2.1" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "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 + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -5345,6 +5383,12 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@stripe/stripe-js": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.8.0.tgz", @@ -5354,6 +5398,66 @@ "node": ">=12.16" } }, + "node_modules/@supabase/auth-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.112.4.tgz", + "integrity": "sha512-z8DesgwLzKM5PiT0yNmJU8VJyh1zAhYi+20Z7drdJQLXg/wWW4yGt/un+He5ERYUo94Vz66t5aeyr1DIDemI5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.112.4.tgz", + "integrity": "sha512-DQ0aVH8wSQAccVqNoEkec62qCu2QRNyoGN53RqsVZ1k6F1zq4/v8scrlR6LNT2RJmT97apiTmORijPVhErCS2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", + "license": "MIT", + "peer": true + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.112.4.tgz", + "integrity": "sha512-uaubtPSeg2TR4wrtfQoQWgkTAe+a0qWX2KhmwvTfNl5mGN9+U7owiJt6abk3o/V6O899PSRD1yzxs5RlF4xTug==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.112.4.tgz", + "integrity": "sha512-vZ+j079SKrM0Xiq7MJCvQKLDpaH2kfKfLY68xuQE1sqsCsMmx1CyrDBJHsxZ3cX01VOs5SI9igmoZAF3BmdZxw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@supabase/phoenix": "0.4.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@supabase/ssr": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz", @@ -5366,6 +5470,45 @@ "@supabase/supabase-js": "^2.43.4" } }, + "node_modules/@supabase/storage-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.112.4.tgz", + "integrity": "sha512-lQ0JemuTlMIXVKgSci1qez8yPnM5hyDngeAfEBjZS2Om4D+Cus0EE5BE6glFobrxdyii1OF4UzWfF0zcQgDq5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.4.tgz", + "integrity": "sha512-UiCX1udlFY1fQQrO7Z3GU7obQsju0w5Vk9mOOwalfo/+Gy+tahWVenSSuu5E/GTy/q//HxvGv2IrCdW66/61kw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@supabase/auth-js": "2.112.4", + "@supabase/functions-js": "2.112.4", + "@supabase/postgrest-js": "2.112.4", + "@supabase/realtime-js": "2.112.4", + "@supabase/storage-js": "2.112.4" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, "node_modules/@swc/core": { "version": "1.15.43", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", @@ -5605,9 +5748,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -6518,6 +6661,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -6545,7 +6751,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -6559,7 +6764,6 @@ "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -6569,7 +6773,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6579,7 +6782,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -7370,6 +7572,181 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -7392,7 +7769,6 @@ "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7438,6 +7814,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -8039,6 +8457,13 @@ "node": ">=8.0.0" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -8161,6 +8586,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -8556,9 +8991,129 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8684,6 +9239,12 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9117,6 +9678,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -9525,7 +10098,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -9576,7 +10148,6 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, "license": "MIT" }, "node_modules/events": { @@ -9723,6 +10294,23 @@ "fast-string-truncated-width": "^3.0.2" } }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fast-wrap-ansi": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", @@ -10135,7 +10723,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphql": { @@ -10165,7 +10752,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10355,6 +10941,16 @@ "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", "license": "ISC" }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/icu-minify": { "version": "4.13.2", "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.2.tgz", @@ -10400,6 +10996,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10493,6 +11099,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/intl-messageformat": { "version": "11.2.11", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.11.tgz", @@ -11068,6 +11683,37 @@ "restructure": "^3.0.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -11824,6 +12470,13 @@ "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", "license": "MIT" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11870,6 +12523,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -11918,6 +12581,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -12044,6 +12768,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, "node_modules/next": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", @@ -12144,6 +12875,15 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -13306,7 +14046,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT" }, "node_modules/react-phone-number-input": { @@ -13326,6 +14065,29 @@ "react-dom": ">=16.8" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -13449,6 +14211,36 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "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" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -13463,6 +14255,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -13539,6 +14346,12 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resend": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/resend/-/resend-6.18.0.tgz", @@ -13862,6 +14675,63 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/semifies": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", @@ -14168,6 +15038,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -14599,7 +15480,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14630,6 +15510,32 @@ "streamx": "^2.12.5" } }, + "node_modules/terser": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.0.tgz", + "integrity": "sha512-myiQ6aFnxDOjdiXdTlC8ngVccQD88uHXTx5RUQOSEnarDYgJMjDagwAQszcOusjvmf4YqWsxoD1+MY+oAJRmpw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -14645,6 +15551,12 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -14913,7 +15825,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14970,7 +15882,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unicode-properties": { @@ -15166,6 +16077,28 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", @@ -15367,6 +16300,19 @@ "node": ">=18" } }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -15377,6 +16323,98 @@ "node": ">=20" } }, + "node_modules/webpack": { + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", diff --git a/package.json b/package.json index 9775956..0ad3502 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", + "recharts": "^3.10.1", "resend": "^6.18.0", "sonner": "^2.0.7", "stripe": "^22.1.1", diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 9ed0ccb..e2bf1b3 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,5 +1,5 @@ import { SidebarProvider } from "@/src/components/ui/sidebar"; -import DashboardNavbar from "@/src/modules/dashboard/ui/components/DashboardNavbar"; +import DashboardNavbarWrapper from "@/src/modules/dashboard/ui/components/DashboardNavbarWrapper"; import DashboardSidebarWrapper from "@/src/modules/dashboard/ui/components/DashboardSidebarWrapper"; interface Props { @@ -16,8 +16,8 @@ const Layout = ({ children }: Props) => {
-
- +
+
{children}
diff --git a/src/modules/courses/ui/views/course-create-view.tsx b/src/modules/courses/ui/views/course-create-view.tsx index 83351f9..660bf55 100644 --- a/src/modules/courses/ui/views/course-create-view.tsx +++ b/src/modules/courses/ui/views/course-create-view.tsx @@ -6,14 +6,24 @@ import GenericDropdown from "@/src/components/layout/GenericDropdown"; import MultiSelectDropdown from "@/src/components/layout/MultiSelectDropdown"; import RichTextEditor from "@/src/components/layout/RichTextEditor"; import { Button } from "@/src/components/ui/button"; -import { Checkbox } from "@/src/components/ui/checkbox"; -import { Label } from "@/src/components/ui/label"; import { createCourse, updateCourse, getMyBusinesses } from "../../actions/course-actions"; import { getMyRooms } from "@/src/modules/rooms/actions/room-actions"; import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions"; import { toast } from "sonner"; import { Course } from "../../types/course.types"; -import { Trash2, ImagePlus, X, Loader2, AlertTriangle } from "lucide-react"; +import { + Trash2, + ImagePlus, + X, + Loader2, + AlertTriangle, + Info, + CalendarDays, + Settings2, + FileText, + RefreshCw, + Check, +} from "lucide-react"; import Image from "next/image"; import DeleteDialog from "@/src/components/layout/DeleteDialog"; import { @@ -454,12 +464,13 @@ const CourseCreateView = ({ return (
-
+ {/* Page header */} +

{mode === "edit" ? "Kurs bearbeiten" : "Kurs anlegen"}

-

+

{mode === "edit" ? "Ändern Sie die Kursdetails und speichern Sie Ihre Änderungen" : "Alle Kurse können nach dem Speichern angepasst oder gelöscht werden"} @@ -474,359 +485,431 @@ const CourseCreateView = ({ )}

-
- {businesses.length > 1 && ( - - )} -
- setCourseName(e.target.value)} - /> - {errors.name &&

{errors.name}

} -
- { - setSports([...sports, newSport]); - setNewlyCreatedSports([...newlyCreatedSports, newSport.value]); - }} - onDeleteOption={(valueToDelete) => { - setSports(sports.filter((s) => s.value !== valueToDelete)); - setNewlyCreatedSports(newlyCreatedSports.filter((v) => v !== valueToDelete)); - setSelectedSports(selectedSports.filter((v) => v !== valueToDelete)); - }} - deletableValues={newlyCreatedSports} - error={errors.sport} - /> - - - -
- setCourseDate(e.target.value)} - /> - {errors.date &&

{errors.date}

} -
- -
- setTimeFrom(e.target.value)} - /> - {errors.timeFrom &&

{errors.timeFrom}

} -
- -
- setTimeTo(e.target.value)} - /> - {errors.timeTo &&

{errors.timeTo}

} -
- -
- { - const value = e.target.value; - // Remove all non-numeric characters except comma and dot - const cleanValue = value.replace(/[^\d,\.]/g, ""); - // Replace comma with dot for internal storage - const normalizedValue = cleanValue.replace(",", "."); - - setPriceDisplay(value); - // Allow empty string, numbers with optional decimal point and max 2 decimal places - if (normalizedValue === "" || /^\d*\.?\d{0,2}$/.test(normalizedValue)) { - setPrice(normalizedValue); - } - }} - onFocus={() => { - setIsPriceFocused(true); - setPriceDisplay(price.replace(".", ",")); - }} - onBlur={() => { - setIsPriceFocused(false); - }} - placeholder="0,00" - /> - {errors.price &&

{errors.price}

} -
- - {!isLoadingTrainers && trainers.length > 0 && ( -
- -
- )} - - - -
- setMaxParticipants(e.target.value)} - /> - {errors.maxParticipants && ( -

{errors.maxParticipants}

- )} -
- - {/* Kursbild */} -
-

Kursbild (optional)

- {coverImage ? ( -
- Kursbild - +
+ {/* Section 1: Grunddaten */} +
+
+
+
- ) : ( -
+
+ {businesses.length > 1 && ( + - - )} -
- - - -
- { - setIsStandingOrder(checked === true); - if (!checked) { - setSelectedFrequency(""); - setSelectedWeekdays([]); - setWeekdayTimings({}); - setEndDate(""); - } - }} - /> - -
- - {isStandingOrder && ( - <> - { - setSelectedFrequency(value); - if (value !== "custom") { - setSelectedWeekdays([]); - setWeekdayTimings({}); - } - }} - options={FREQUENCIES} - error={errors.frequency} - /> - + )}
setEndDate(e.target.value)} + label="Kursname" + type="text" + id="course-create-name" + value={courseName} + onChange={(e) => setCourseName(e.target.value)} /> - {errors.endDate &&

{errors.endDate}

} + {errors.name &&

{errors.name}

} +
+
+ { + setSports([...sports, newSport]); + setNewlyCreatedSports([...newlyCreatedSports, newSport.value]); + }} + onDeleteOption={(valueToDelete) => { + setSports(sports.filter((s) => s.value !== valueToDelete)); + setNewlyCreatedSports(newlyCreatedSports.filter((v) => v !== valueToDelete)); + setSelectedSports(selectedSports.filter((v) => v !== valueToDelete)); + }} + deletableValues={newlyCreatedSports} + error={errors.sport} + /> + +
+
+
+ + {/* Section 2: Zeitplan */} +
+
+
+ +
+

Zeitplan

+
+
+
+
+ setCourseDate(e.target.value)} + /> + {errors.date &&

{errors.date}

} +
+
+ setTimeFrom(e.target.value)} + /> + {errors.timeFrom &&

{errors.timeFrom}

} +
+
+ setTimeTo(e.target.value)} + /> + {errors.timeTo &&

{errors.timeTo}

} +
- {selectedFrequency === "custom" && ( - <> - { - setSelectedWeekdays(days); - // Nicht mehr ausgewählte Tage aus weekdayTimings entfernen - setWeekdayTimings((prev) => { - const next: Record = {}; - days.forEach((d) => { - next[d] = prev[d] ?? { timeFrom: "", timeTo: "" }; - }); - return next; - }); + {/* Dauerauftrag toggle */} + + + {isStandingOrder && ( +
+ { + setSelectedFrequency(value); + if (value !== "custom") { + setSelectedWeekdays([]); + setWeekdayTimings({}); + } }} - options={WEEKDAYS} - error={errors.weekdays} + options={FREQUENCIES} + error={errors.frequency} /> - {selectedWeekdays.length > 0 && ( -
-

Uhrzeiten pro Tag

- {selectedWeekdays.map((day) => { - const dayLabel = WEEKDAYS.find((w) => w.value === day)?.label ?? day; - return ( -
- {dayLabel} - - setWeekdayTimings((prev) => ({ - ...prev, - [day]: { ...prev[day], timeFrom: e.target.value }, - })) - } - /> - – - - setWeekdayTimings((prev) => ({ - ...prev, - [day]: { ...prev[day], timeTo: e.target.value }, - })) - } - /> -
- ); - })} - {errors.weekdayTimings && ( -

{errors.weekdayTimings}

+
+ setEndDate(e.target.value)} + /> + {errors.endDate &&

{errors.endDate}

} +
+ + {selectedFrequency === "custom" && ( + <> + { + setSelectedWeekdays(days); + setWeekdayTimings((prev) => { + const next: Record = {}; + days.forEach((d) => { + next[d] = prev[d] ?? { timeFrom: "", timeTo: "" }; + }); + return next; + }); + }} + options={WEEKDAYS} + error={errors.weekdays} + /> + + {selectedWeekdays.length > 0 && ( +
+

Uhrzeiten pro Tag

+ {selectedWeekdays.map((day) => { + const dayLabel = WEEKDAYS.find((w) => w.value === day)?.label ?? day; + return ( +
+ {dayLabel} + + setWeekdayTimings((prev) => ({ + ...prev, + [day]: { ...prev[day], timeFrom: e.target.value }, + })) + } + /> + – + + setWeekdayTimings((prev) => ({ + ...prev, + [day]: { ...prev[day], timeTo: e.target.value }, + })) + } + /> +
+ ); + })} + {errors.weekdayTimings && ( +

{errors.weekdayTimings}

+ )} +
)} -
+ )} - +
)} - - )} -
+
+
+ + {/* Section 3: Logistik */} +
+
+
+ +
+

Logistik

+
+
+
+ + {!isLoadingTrainers && trainers.length > 0 && ( + + )} +
+
+
+ { + const value = e.target.value; + const cleanValue = value.replace(/[^\d,\.]/g, ""); + const normalizedValue = cleanValue.replace(",", "."); + setPriceDisplay(value); + if (normalizedValue === "" || /^\d*\.?\d{0,2}$/.test(normalizedValue)) { + setPrice(normalizedValue); + } + }} + onFocus={() => { + setIsPriceFocused(true); + setPriceDisplay(price.replace(".", ",")); + }} + onBlur={() => { + setIsPriceFocused(false); + }} + placeholder="0,00" + /> + {errors.price &&

{errors.price}

} +
+
+ setMaxParticipants(e.target.value)} + /> + {errors.maxParticipants && ( +

{errors.maxParticipants}

+ )} +
+
+
+
+ + {/* Section 4: Inhalt */} +
+
+
+ +
+

Inhalt

+
+
+
+

Kursbild (optional)

+ {coverImage ? ( +
+ Kursbild + +
+ ) : ( + + )} +
+ + +
+
+ + {/* Actions */} +
+
+ + +
-
-
{ + try { + const manager = await requireManager(); + const managerId = manager.id; + + const fn = unstable_cache( + async () => { + const now = new Date(); + const todayStart = startOfDay(now); + const todayEnd = endOfDay(now); + const yesterdayStart = startOfDay(subDays(now, 1)); + const yesterdayEnd = endOfDay(subDays(now, 1)); + const monthStart = startOfMonth(now); + const monthEnd = endOfMonth(now); + const lastMonthStart = startOfMonth(subMonths(now, 1)); + const lastMonthEnd = endOfMonth(subMonths(now, 1)); + + const [ + coursesToday, + participantsToday, + coursesThisMonth, + coursesTodayYesterday, + participantsTodayYesterday, + coursesLastMonth, + revenueThisMonthPaid, + revenueThisMonthPartial, + revenueLastMonthPaid, + revenueLastMonthPartial, + ] = await Promise.all([ + prisma.course.count({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: todayStart, lte: todayEnd }, + }, + }), + prisma.courseBooking.count({ + where: { + course: { + createdBy: managerId, + isStandingOrder: false, + date: { gte: todayStart, lte: todayEnd }, + }, + paymentStatus: "paid", + }, + }), + prisma.course.count({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: monthStart, lte: monthEnd }, + }, + }), + prisma.course.count({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: yesterdayStart, lte: yesterdayEnd }, + }, + }), + prisma.courseBooking.count({ + where: { + course: { + createdBy: managerId, + isStandingOrder: false, + date: { gte: yesterdayStart, lte: yesterdayEnd }, + }, + paymentStatus: "paid", + }, + }), + prisma.course.count({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: lastMonthStart, lte: lastMonthEnd }, + }, + }), + prisma.courseBooking.aggregate({ + where: { + course: { createdBy: managerId }, + paymentStatus: "paid", + createdAt: { gte: monthStart, lte: monthEnd }, + }, + _sum: { amountPaid: true }, + }), + prisma.courseBooking.aggregate({ + where: { + course: { createdBy: managerId }, + paymentStatus: "partially_refunded", + createdAt: { gte: monthStart, lte: monthEnd }, + }, + _sum: { amountPaid: true, refundedAmount: true }, + }), + prisma.courseBooking.aggregate({ + where: { + course: { createdBy: managerId }, + paymentStatus: "paid", + createdAt: { gte: lastMonthStart, lte: lastMonthEnd }, + }, + _sum: { amountPaid: true }, + }), + prisma.courseBooking.aggregate({ + where: { + course: { createdBy: managerId }, + paymentStatus: "partially_refunded", + createdAt: { gte: lastMonthStart, lte: lastMonthEnd }, + }, + _sum: { amountPaid: true, refundedAmount: true }, + }), + ]); + + const revenueThisMonth = + (revenueThisMonthPaid._sum.amountPaid ?? 0) + + (revenueThisMonthPartial._sum.amountPaid ?? 0) - + (revenueThisMonthPartial._sum.refundedAmount ?? 0); + + const revenueLastMonth = + (revenueLastMonthPaid._sum.amountPaid ?? 0) + + (revenueLastMonthPartial._sum.amountPaid ?? 0) - + (revenueLastMonthPartial._sum.refundedAmount ?? 0); + + return { + coursesToday, + participantsToday, + coursesThisMonth, + revenueThisMonth, + coursesTodayYesterday, + participantsTodayYesterday, + coursesLastMonth, + revenueLastMonth, + }; + }, + [`dashboard-stats-${managerId}`], + { tags: [`dashboard-stats-${managerId}`], revalidate: 60 } + ); + + const stats = await fn(); + return { success: true, stats }; + } catch { + return { success: false, error: "Statistiken konnten nicht geladen werden." }; + } +} + +export async function getDashboardRevenueChart(): Promise< + { success: true; data: ChartPoint[] } | { success: false; error: string } +> { + try { + const manager = await requireManager(); + const managerId = manager.id; + + const fn = unstable_cache( + async (): Promise => { + const now = new Date(); + const rangeStart = startOfMonth(subMonths(now, 11)); + const rangeEnd = endOfMonth(now); + + const [paid, partial, courses] = await Promise.all([ + prisma.courseBooking.findMany({ + where: { + course: { createdBy: managerId }, + paymentStatus: "paid", + createdAt: { gte: rangeStart, lte: rangeEnd }, + }, + select: { createdAt: true, amountPaid: true }, + }), + prisma.courseBooking.findMany({ + where: { + course: { createdBy: managerId }, + paymentStatus: "partially_refunded", + createdAt: { gte: rangeStart, lte: rangeEnd }, + }, + select: { createdAt: true, amountPaid: true, refundedAmount: true }, + }), + prisma.course.findMany({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: rangeStart, lte: rangeEnd }, + }, + select: { date: true }, + }), + ]); + + // Initialise all 12 months to zero + const byMonth = new Map(); + for (let i = 11; i >= 0; i--) { + byMonth.set(format(subMonths(now, i), "yyyy-MM"), { + revenue: 0, + bookings: 0, + courses: 0, + }); + } + + for (const b of paid) { + const key = format(new Date(b.createdAt), "yyyy-MM"); + const entry = byMonth.get(key); + if (entry) { + entry.revenue += b.amountPaid ?? 0; + entry.bookings += 1; + } + } + + for (const b of partial) { + const key = format(new Date(b.createdAt), "yyyy-MM"); + const entry = byMonth.get(key); + if (entry) { + entry.revenue += (b.amountPaid ?? 0) - (b.refundedAmount ?? 0); + entry.bookings += 1; + } + } + + for (const c of courses) { + const key = format(new Date(c.date), "yyyy-MM"); + const entry = byMonth.get(key); + if (entry) entry.courses += 1; + } + + return Array.from(byMonth.entries()).map(([key, vals]) => { + const [year, month] = key.split("-"); + const date = new Date(Number(year), Number(month) - 1, 1); + return { label: format(date, "MMM yy", { locale: de }), ...vals }; + }); + }, + [`dashboard-chart-${managerId}`], + { tags: [`dashboard-chart-${managerId}`], revalidate: 300 } + ); + + const data = await fn(); + return { success: true, data }; + } catch { + return { success: false, error: "Diagrammdaten konnten nicht geladen werden." }; + } +} + +export async function getDashboardTodayCourses(): Promise< + { success: true; courses: TodayCourse[] } | { success: false; error: string } +> { + try { + const userData = await getUserData(); + if ( + !userData || + (!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view)) + ) { + return { success: false, error: "Keine Berechtigung" }; + } + const managerId = getEffectiveManagerId(userData); + if (!managerId) return { success: false, error: "Kein Manager-Account verknüpft" }; + + const fn = unstable_cache( + async () => { + const now = new Date(); + const todayStart = startOfDay(now); + const todayEnd = endOfDay(now); + + const courses = await prisma.course.findMany({ + where: { + createdBy: managerId, + isStandingOrder: false, + status: "published", + date: { gte: todayStart, lte: todayEnd }, + }, + include: { + _count: { + select: { bookings: { where: { paymentStatus: "paid" } } }, + }, + }, + orderBy: { timeFrom: "asc" }, + }); + + const roomIds = [...new Set(courses.map((c) => c.room).filter(Boolean))]; + const rooms = roomIds.length + ? await prisma.room.findMany({ + where: { id: { in: roomIds } }, + select: { id: true, name: true }, + }) + : []; + const roomNameById = new Map(rooms.map((r) => [r.id, r.name])); + + return courses.map((c) => ({ + id: c.id, + name: c.name, + timeFrom: c.timeFrom, + timeTo: c.timeTo, + room: roomNameById.get(c.room) ?? c.room, + sport: c.sport as string[], + participantCount: c._count.bookings, + maxParticipants: c.maxParticipants, + })); + }, + [`dashboard-today-courses-${managerId}`], + { tags: [`dashboard-today-courses-${managerId}`], revalidate: 60 } + ); + + const courses = await fn(); + return { success: true, courses }; + } catch { + return { success: false, error: "Heutige Kurse konnten nicht geladen werden." }; + } +} + +export async function getDashboardRecentBookings(): Promise< + { success: true; bookings: RecentBooking[] } | { success: false; error: string } +> { + try { + const manager = await requireManager(); + const managerId = manager.id; + + const fn = unstable_cache( + async () => { + const bookings = await prisma.courseBooking.findMany({ + where: { + course: { createdBy: managerId }, + paymentStatus: { in: ["paid", "partially_refunded", "pending", "refunded"] }, + }, + include: { + course: { select: { name: true } }, + user: { select: { name: true, email: true } }, + guestLead: { select: { name: true, email: true } }, + }, + orderBy: { createdAt: "desc" }, + take: 6, + }); + + return bookings.map((b) => ({ + id: b.id, + customerName: + b.guestLead?.name ?? b.user?.name ?? b.guestLead?.email ?? b.user?.email ?? "Anonym", + courseName: b.course.name, + amountPaid: b.amountPaid, + paymentStatus: b.paymentStatus, + createdAt: b.createdAt.toISOString(), + })); + }, + [`dashboard-recent-bookings-${managerId}`], + { tags: [`dashboard-recent-bookings-${managerId}`], revalidate: 60 } + ); + + const bookings = await fn(); + return { success: true, bookings }; + } catch { + return { success: false, error: "Buchungen konnten nicht geladen werden." }; + } +} diff --git a/src/modules/dashboard/ui/components/DashboardFooter.tsx b/src/modules/dashboard/ui/components/DashboardFooter.tsx index 2116ec6..b4ccf3e 100644 --- a/src/modules/dashboard/ui/components/DashboardFooter.tsx +++ b/src/modules/dashboard/ui/components/DashboardFooter.tsx @@ -1,5 +1,5 @@ "use client"; -import { HelpCircle, Settings, Plus } from "lucide-react"; +import { HelpCircle, Settings, Plus, LogOut, User, ChevronDown } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useState } from "react"; @@ -8,33 +8,28 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; -import { ChevronDown } from "lucide-react"; import { Button } from "@/src/components/ui/button"; +import { useAuthStore } from "@/src/store/authStore"; const footerMenu = [ - { - icon: Settings, - label: "Einstellungen", - href: "/settings", - }, - { - icon: HelpCircle, - label: "Hilfe", - href: "/help", - }, + { icon: Settings, label: "Einstellungen", href: "/settings" }, + { icon: HelpCircle, label: "Hilfe", href: "/help" }, ]; const getDiceBearUrl = (seed: string) => `https://api.dicebear.com/9.x/thumbs/svg?seed=${encodeURIComponent(seed)}`; -const AvatarImage = ({ userData }: { userData: AuthUserData }) => { +const AvatarImage = ({ userData, size = 10 }: { userData: AuthUserData; size?: number }) => { const pbSrc = "pbSrc" in userData ? userData.pbSrc : null; const src = pbSrc || getDiceBearUrl(userData.id); - return ( -
+
); @@ -51,34 +46,34 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => { const [selectedBusinessId, setSelectedBusinessId] = useState( businesses.length > 0 ? businesses[0].id : "" ); - + const signOut = useAuthStore((state) => state.signOut); const selectedBusiness = businesses.find((b) => b.id === selectedBusinessId); return ( -
+
{footerMenu.map((item) => ( -
- - - {item.label} - -
+ + + {item.label} + ))} {/* Business Dropdown - nur für Manager */} {isManager && businesses.length > 0 && ( -
+
- Business -
@@ -95,10 +90,10 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
))} - + @@ -106,10 +101,42 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
)} - {/* DisplayName + Avatar */} -
- -

{displayName}

+ {/* Profile dropdown */} +
+ + + + + + + + + Profil bearbeiten + + + + + + Einstellungen + + + + + + Abmelden + + +
); diff --git a/src/modules/dashboard/ui/components/DashboardNavbar.tsx b/src/modules/dashboard/ui/components/DashboardNavbar.tsx index 70224f3..32076ef 100644 --- a/src/modules/dashboard/ui/components/DashboardNavbar.tsx +++ b/src/modules/dashboard/ui/components/DashboardNavbar.tsx @@ -2,12 +2,14 @@ import { Button } from "@/src/components/ui/button"; import { useSidebar } from "@/src/components/ui/sidebar"; -import { PanelLeftCloseIcon, PanelLeftIcon } from "lucide-react"; +import { PanelLeftCloseIcon, PanelLeftIcon, Plus } from "lucide-react"; +import Link from "next/link"; import { usePathname } from "next/navigation"; import NotificationBell from "@/src/modules/notifications/ui/components/NotificationBell"; +import DashboardSearch from "./DashboardSearch"; const menuTitles: Record = { - "/dashboard": "Dashboard", + "/dashboard": "Übersicht", "/courses/myCourses": "Meine Kurse", "/courses": "Kurs Übersicht", "/courses/create": "Erstelle einen Kurs", @@ -19,47 +21,42 @@ const menuTitles: Record = { "/employee/create": "Mitarbeiter erstellen", "/settings": "Einstellungen", "/settings/public-page": "Einstellungen", + "/kunden": "Kunden", + "/zahlungen": "Zahlungen", }; -const dynamicRoutes: Array<{ - prefix: string; - titleKey: string; -}> = [ - { - prefix: "/courses/edit", - titleKey: "/courses/edit", - }, - { - prefix: "/rooms/edit", - titleKey: "/rooms/edit", - }, +const dynamicRoutes: Array<{ prefix: string; titleKey: string }> = [ + { prefix: "/courses/edit", titleKey: "/courses/edit" }, + { prefix: "/rooms/edit", titleKey: "/rooms/edit" }, + { prefix: "/kunden/", titleKey: "/kunden" }, ]; const getPageTitle = (pathname: string) => { - if (menuTitles[pathname]) { - return menuTitles[pathname]; - } - + if (menuTitles[pathname]) return menuTitles[pathname]; for (const route of dynamicRoutes) { - if (pathname.startsWith(route.prefix)) { - return menuTitles[route.titleKey]; - } + if (pathname.startsWith(route.prefix)) return menuTitles[route.titleKey]; } - - // Fallback return "Dashboard"; }; -const DashboardNavbar = () => { +interface DashboardNavbarProps { + canCreateCourse?: boolean; + userRole?: string; +} + +const DashboardNavbar = ({ canCreateCourse, userRole }: DashboardNavbarProps) => { const { state, toggleSidebar, isMobile } = useSidebar(); const pathname = usePathname(); - const pageTitle = getPageTitle(pathname); return ( -
-
- -
+
-

{pageTitle}

+

{pageTitle}

-
- -
+
+ + + + {canCreateCourse && pathname === "/dashboard" && ( + + )} + +
); diff --git a/src/modules/dashboard/ui/components/DashboardNavbarWrapper.tsx b/src/modules/dashboard/ui/components/DashboardNavbarWrapper.tsx new file mode 100644 index 0000000..420e9ad --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardNavbarWrapper.tsx @@ -0,0 +1,13 @@ +import { requireAuthWithData, isManager, isEmployee } from "@/src/lib/auth/getUser"; +import DashboardNavbar from "./DashboardNavbar"; + +const DashboardNavbarWrapper = async () => { + const userData = await requireAuthWithData(); + + const canCreateCourse = + isManager(userData) || (isEmployee(userData) && userData.permissions.courses.create); + + return ; +}; + +export default DashboardNavbarWrapper; diff --git a/src/modules/dashboard/ui/components/DashboardRecentPayments.tsx b/src/modules/dashboard/ui/components/DashboardRecentPayments.tsx new file mode 100644 index 0000000..6c4a77d --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardRecentPayments.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import type { RecentBooking } from "../../actions/dashboard-actions"; +import { PAYMENT_STATUS } from "@/src/modules/zahlungen/ui/components/zahlungen-status-badge"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; + +function formatEur(cents: number | null) { + if (cents == null) return "—"; + return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(cents / 100); +} + +interface DashboardRecentPaymentsProps { + bookings: RecentBooking[]; +} + +const DashboardRecentPayments = ({ bookings }: DashboardRecentPaymentsProps) => { + return ( +
+
+
+

+ Letzte Buchungen +

+

Zahlungen

+
+ + Alle ansehen + +
+ + {bookings.length === 0 ? ( +

Noch keine Buchungen vorhanden

+ ) : ( +
+
+ Name + Kurs + Status + Betrag +
+ + {bookings.map((b) => { + const statusStyle = PAYMENT_STATUS[b.paymentStatus] ?? PAYMENT_STATUS.pending; + return ( +
+
+

{b.customerName}

+

+ {format(new Date(b.createdAt), "d. MMM", { locale: de })} +

+
+

{b.courseName}

+ + {statusStyle.label} + +

+ {formatEur(b.amountPaid)} +

+
+ ); + })} +
+ )} +
+ ); +}; + +export default DashboardRecentPayments; diff --git a/src/modules/dashboard/ui/components/DashboardRevenueChart.tsx b/src/modules/dashboard/ui/components/DashboardRevenueChart.tsx new file mode 100644 index 0000000..b497034 --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardRevenueChart.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { + ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, +} from "recharts"; +import type { ChartPoint } from "../../actions/dashboard-actions"; + +type Range = 3 | 6 | 12; +type Metric = "revenue" | "bookings" | "courses"; + +function formatEur(cents: number) { + return new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + maximumFractionDigits: 0, + }).format(cents / 100); +} + +function formatYAxis(value: number, metric: Metric) { + if (metric === "revenue") { + const euros = value / 100; + if (euros >= 1000) return `${(euros / 1000).toFixed(1).replace(".", ",")} T`; + return `${euros.toFixed(0)} €`; + } + if (value >= 1000) return `${(value / 1000).toFixed(1)}k`; + return String(value); +} + +function formatTotal(total: number, metric: Metric) { + if (metric === "revenue") return formatEur(total); + return String(total); +} + +type TooltipEntry = { value?: number }; +const CustomTooltip = ({ + active, + payload, + label, + metric, + color, +}: { + active?: boolean; + payload?: TooltipEntry[]; + label?: string; + metric: Metric; + color: string; +}) => { + if (!active || !payload?.length) return null; + const val = payload[0].value ?? 0; + return ( +
+

{label}

+

+ {metric === "revenue" + ? formatEur(val) + : `${val} ${metric === "bookings" ? "Buchungen" : "Kurse"}`} +

+
+ ); +}; + +interface DashboardChartProps { + data: ChartPoint[]; +} + +const RANGES: { label: string; value: Range }[] = [ + { label: "12 M", value: 12 }, + { label: "6 M", value: 6 }, + { label: "3 M", value: 3 }, +]; + +const METRICS: { label: string; value: Metric; color: string }[] = [ + { label: "Umsatz", value: "revenue", color: "#c9a84c" }, + { label: "Buchungen", value: "bookings", color: "#3b82f6" }, + { label: "Kurse", value: "courses", color: "#10b981" }, +]; + +const DashboardRevenueChart = ({ data }: DashboardChartProps) => { + const [range, setRange] = useState(12); + const [metric, setMetric] = useState("revenue"); + + const sliced = useMemo(() => data.slice(-range), [data, range]); + + const activeMetric = METRICS.find((m) => m.value === metric)!; + + const total = sliced.reduce((sum, p) => sum + p[metric], 0); + const prevSlice = data.slice(-(range * 2), -range); + const prevTotal = prevSlice.reduce((sum, p) => sum + p[metric], 0); + const pctChange = prevTotal === 0 ? null : ((total - prevTotal) / prevTotal) * 100; + + const metricLabel: Record = { + revenue: `Umsatz – letzte ${range} Monate`, + bookings: `Buchungen – letzte ${range} Monate`, + courses: `Kurse – letzte ${range} Monate`, + }; + + return ( +
+ {/* Header row */} +
+ {/* Metric tabs */} +
+ {METRICS.map((m) => ( + + ))} +
+ + {/* Value + range row */} +
+
+
+

+ {formatTotal(total, metric)} +

+ {pctChange !== null && ( + = 0 ? "bg-green-100 text-green-700" : "bg-red-100 text-red-600" + }`} + > + {pctChange >= 0 ? "+" : ""} + {pctChange.toFixed(1)} % + + )} +
+

{metricLabel[metric]}

+
+ +
+ {RANGES.map((r) => ( + + ))} +
+
+
+ + {/* Chart */} +
+ + + + + formatYAxis(v, metric)} + tick={{ fontSize: 11, fill: "#9ca3af" }} + axisLine={false} + tickLine={false} + width={metric === "revenue" ? 56 : 36} + /> + } + cursor={{ stroke: "#e5e7eb", strokeWidth: 1 }} + /> + + + +
+
+ ); +}; + +export default DashboardRevenueChart; diff --git a/src/modules/dashboard/ui/components/DashboardSearch.tsx b/src/modules/dashboard/ui/components/DashboardSearch.tsx new file mode 100644 index 0000000..dff27c3 --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardSearch.tsx @@ -0,0 +1,416 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { + LayoutDashboard, + Dumbbell, + IdCardLanyard, + User, + CreditCard, + DoorOpen, + Settings, + Plus, + Search, + ArrowRight, + Palette, + Globe, + Bell, + XCircle, + Tag, + UserCircle, +} from "lucide-react"; +import { Dialog, DialogContent } from "@/src/components/ui/dialog"; + +type NavItem = { + label: string; + description: string; + href: string; + icon: React.ComponentType<{ className?: string }>; + category: string; + roles: string[]; + keywords?: string[]; +}; + +const navItems: NavItem[] = [ + // ── Navigation ────────────────────────────────────────────────── + { + label: "Dashboard", + description: "Statistiken, Umsatz & heutige Kurse", + href: "/dashboard", + icon: LayoutDashboard, + category: "Navigation", + roles: ["manager", "employee", "user"], + keywords: ["übersicht", "home", "start"], + }, + { + label: "Kurse", + description: "Alle Kurse ansehen, filtern & verwalten", + href: "/courses", + icon: Dumbbell, + category: "Navigation", + roles: ["manager", "employee"], + keywords: ["kurs", "training", "sport"], + }, + { + label: "Meine Kurse", + description: "Gebuchte Trainings anzeigen", + href: "/courses/myCourses", + icon: Dumbbell, + category: "Navigation", + roles: ["user"], + keywords: ["buchung", "training"], + }, + { + label: "Kunden", + description: "Kundenliste, Buchungshistorie & Details", + href: "/kunden", + icon: User, + category: "Navigation", + roles: ["manager"], + keywords: ["kunde", "teilnehmer", "mitglied", "user"], + }, + { + label: "Zahlungen", + description: "Transaktionen, Rechnungen & Erstattungen", + href: "/zahlungen", + icon: CreditCard, + category: "Navigation", + roles: ["manager"], + keywords: ["zahlung", "rechnung", "stripe", "geld", "umsatz", "erstattung", "refund"], + }, + { + label: "Räume", + description: "Räume & Locations verwalten", + href: "/rooms", + icon: DoorOpen, + category: "Navigation", + roles: ["manager", "employee"], + keywords: ["raum", "location", "studio", "halle"], + }, + { + label: "Mitarbeiter", + description: "Team & Berechtigungen verwalten", + href: "/employee", + icon: IdCardLanyard, + category: "Navigation", + roles: ["manager", "employee"], + keywords: ["mitarbeiter", "team", "trainer", "angestellter"], + }, + + // ── Schnellaktionen ───────────────────────────────────────────── + { + label: "Neuen Kurs erstellen", + description: "Einzeltermin oder Dauerauftrag anlegen", + href: "/courses/create", + icon: Plus, + category: "Schnellaktionen", + roles: ["manager", "employee"], + keywords: ["neu", "erstellen", "kurs", "anlegen"], + }, + { + label: "Neuen Raum erstellen", + description: "Raum oder Location hinzufügen", + href: "/rooms/create", + icon: Plus, + category: "Schnellaktionen", + roles: ["manager"], + keywords: ["neu", "raum", "erstellen"], + }, + { + label: "Mitarbeiter einladen", + description: "Neues Teammitglied per E-Mail einladen", + href: "/employee/create", + icon: Plus, + category: "Schnellaktionen", + roles: ["manager"], + keywords: ["einladen", "mitarbeiter", "neu", "trainer"], + }, + + // ── Einstellungen ──────────────────────────────────────────────── + { + label: "Persönliches Profil", + description: "Name, Avatar & Passwort ändern", + href: "/settings/personal", + icon: UserCircle, + category: "Einstellungen", + roles: ["manager", "employee", "user"], + keywords: ["profil", "account", "passwort", "avatar", "bild"], + }, + { + label: "Business-Einstellungen", + description: "Firmenname, Adresse & Kontaktdaten", + href: "/settings/business", + icon: Settings, + category: "Einstellungen", + roles: ["manager"], + keywords: ["firma", "business", "adresse", "kontakt", "einstellungen"], + }, + { + label: "Branding & Design", + description: "Logo, Farben & E-Mail-Gestaltung", + href: "/settings/branding", + icon: Palette, + category: "Einstellungen", + roles: ["manager"], + keywords: ["logo", "farbe", "design", "email", "brand", "ci"], + }, + { + label: "Öffentliche Buchungsseite", + description: "Buchungsseite aktivieren & anpassen", + href: "/settings/public-page", + icon: Globe, + category: "Einstellungen", + roles: ["manager"], + keywords: ["öffentlich", "buchung", "seite", "link", "public"], + }, + { + label: "Zahlungseinstellungen", + description: "Stripe Connect & Bankkonto verknüpfen", + href: "/settings/payments", + icon: CreditCard, + category: "Einstellungen", + roles: ["manager"], + keywords: ["stripe", "zahlung", "bank", "auszahlung", "connect"], + }, + { + label: "Benachrichtigungen", + description: "E-Mail-Benachrichtigungen konfigurieren", + href: "/settings/notifications", + icon: Bell, + category: "Einstellungen", + roles: ["manager", "employee", "user"], + keywords: ["benachrichtigung", "email", "notification", "push"], + }, + { + label: "Stornierungsrichtlinien", + description: "Fristen, Erstattungsregeln & Kulanzoptionen", + href: "/settings/cancellation", + icon: XCircle, + category: "Einstellungen", + roles: ["manager"], + keywords: ["storno", "erstattung", "refund", "frist", "richtlinie", "policy"], + }, + { + label: "Rabatte & Gutscheine", + description: "Aktionscodes erstellen & verwalten", + href: "/settings/discounts", + icon: Tag, + category: "Einstellungen", + roles: ["manager"], + keywords: ["rabatt", "gutschein", "code", "discount", "aktions"], + }, +]; + +interface DashboardSearchProps { + userRole?: string; +} + +const CATEGORY_ORDER = ["Navigation", "Schnellaktionen", "Einstellungen"]; + +const DashboardSearch = ({ userRole = "manager" }: DashboardSearchProps) => { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [activeIndex, setActiveIndex] = useState(0); + const router = useRouter(); + const inputRef = useRef(null); + + const visibleItems = navItems.filter((item) => item.roles.includes(userRole)); + + const filtered = + query.trim() === "" + ? visibleItems + : visibleItems.filter((item) => { + const q = query.toLowerCase(); + return ( + item.label.toLowerCase().includes(q) || + item.description.toLowerCase().includes(q) || + item.category.toLowerCase().includes(q) || + item.keywords?.some((k) => k.includes(q)) + ); + }); + + const grouped = CATEGORY_ORDER.reduce<{ category: string; items: NavItem[] }[]>((acc, cat) => { + const items = filtered.filter((i) => i.category === cat); + if (items.length > 0) acc.push({ category: cat, items }); + return acc; + }, []); + + // Flat index for keyboard nav + const flat = grouped.flatMap((g) => g.items); + + const openSearch = () => { + setQuery(""); + setActiveIndex(0); + setOpen(true); + }; + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setOpen((prev) => { + if (!prev) { + setQuery(""); + setActiveIndex(0); + } + return !prev; + }); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + useEffect(() => { + if (open) { + setTimeout(() => inputRef.current?.focus(), 50); + } + }, [open]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveIndex((i) => Math.min(i + 1, flat.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIndex((i) => Math.max(i - 1, 0)); + } else if (e.key === "Enter") { + const item = flat[activeIndex]; + if (item) navigate(item.href); + } + }; + + const navigate = (href: string) => { + setOpen(false); + router.push(href); + }; + + let flatCounter = 0; + + return ( + <> + {/* Mobile: icon only */} + + + {/* Desktop: full bar */} + + + + + {/* Search input */} +
+ + { + setQuery(e.target.value); + setActiveIndex(0); + }} + onKeyDown={handleKeyDown} + placeholder="Suchen oder navigieren…" + className="flex-1 text-sm bg-transparent outline-none placeholder:text-gray-400" + /> + {query && ( + + )} +
+ + {/* Results */} +
+ {flat.length === 0 ? ( +

+ Keine Ergebnisse für „{query}“ +

+ ) : ( + grouped.map(({ category, items }) => ( +
+

+ {category} +

+ {items.map((item) => { + const globalIdx = flatCounter++; + const Icon = item.icon; + const isActive = globalIdx === activeIndex; + return ( + + ); + })} +
+ )) + )} +
+ + {/* Footer hints */} +
+ + + ↑↓ + + navigieren + + + + ↵ + + öffnen + + + + Esc + + schließen + +
+
+
+ + ); +}; + +export default DashboardSearch; diff --git a/src/modules/dashboard/ui/components/DashboardStatsGrid.tsx b/src/modules/dashboard/ui/components/DashboardStatsGrid.tsx new file mode 100644 index 0000000..9ddf990 --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardStatsGrid.tsx @@ -0,0 +1,133 @@ +import { TrendingUp, TrendingDown, Minus, Users, CalendarDays, Dumbbell } from "lucide-react"; +import type { DashboardStats } from "../../actions/dashboard-actions"; + +function formatEur(cents: number) { + return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(cents / 100); +} + +function ChangeChip({ current, previous }: { current: number; previous: number }) { + if (previous === 0 && current === 0) { + return ( + + – + + ); + } + if (previous === 0) { + return ( + + Neu + + ); + } + const pct = ((current - previous) / previous) * 100; + const up = pct >= 0; + return ( + + {up ? : } + {up ? "+" : ""} + {Math.abs(pct).toFixed(1)} % + + ); +} + +interface StatTileProps { + label: string; + subLabel: string; + value: string; + current: number; + previous: number; + icon: React.ComponentType<{ className?: string }>; + accent?: string; +} + +function StatTile({ + label, + subLabel, + value, + current, + previous, + icon: Icon, + accent, +}: StatTileProps) { + return ( +
+
+
+
+ + {label} +
+ +
+

{value}

+

{subLabel}

+
+ ); +} + +interface DashboardStatsGridProps { + stats: DashboardStats; +} + +const DashboardStatsGrid = ({ stats }: DashboardStatsGridProps) => { + const tiles: StatTileProps[] = [ + { + label: "Kurse", + subLabel: "Kurse heute · vs. gestern", + value: String(stats.coursesToday), + current: stats.coursesToday, + previous: stats.coursesTodayYesterday, + icon: Dumbbell, + accent: "oklch(0.88 0.09 85 / 0.5)", + }, + { + label: "Teilnehmer", + subLabel: "Heute · vs. gestern", + value: String(stats.participantsToday), + current: stats.participantsToday, + previous: stats.participantsTodayYesterday, + icon: Users, + accent: "oklch(0.88 0.09 85 / 0.5)", + }, + { + label: "Umsatz", + subLabel: "Diesen Monat · vs. letzten Monat", + value: formatEur(stats.revenueThisMonth), + current: stats.revenueThisMonth, + previous: stats.revenueLastMonth, + icon: TrendingUp, + accent: "oklch(0.82 0.06 240 / 0.4)", + }, + { + label: "Kurse", + subLabel: "Diesen Monat · vs. letzten Monat", + value: String(stats.coursesThisMonth), + current: stats.coursesThisMonth, + previous: stats.coursesLastMonth, + icon: CalendarDays, + accent: "oklch(0.82 0.06 240 / 0.4)", + }, + ]; + + return ( +
+ {tiles.map((tile) => ( + + ))} +
+ ); +}; + +export default DashboardStatsGrid; diff --git a/src/modules/dashboard/ui/components/DashboardTodaySchedule.tsx b/src/modules/dashboard/ui/components/DashboardTodaySchedule.tsx new file mode 100644 index 0000000..e4b5881 --- /dev/null +++ b/src/modules/dashboard/ui/components/DashboardTodaySchedule.tsx @@ -0,0 +1,130 @@ +import Link from "next/link"; +import { Clock, Users, ArrowRight, CalendarDays } from "lucide-react"; +import type { TodayCourse } from "../../actions/dashboard-actions"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; + +interface DashboardTodayScheduleProps { + courses: TodayCourse[]; +} + +const DashboardTodaySchedule = ({ courses }: DashboardTodayScheduleProps) => { + const today = format(new Date(), "EEEE, d. MMMM", { locale: de }); + + return ( +
+
+
+

+ Heute +

+

{today}

+
+
+ + {courses.length} {courses.length === 1 ? "Kurs" : "Kurse"} +
+
+ + {courses.length === 0 ? ( +
+ +

Keine Kurse heute

+

Genieß den freien Tag!

+ + Kurs erstellen → + +
+ ) : ( +
+ {courses.map((course) => { + const occupancy = + course.maxParticipants > 0 + ? Math.round((course.participantCount / course.maxParticipants) * 100) + : 0; + const isFull = + course.maxParticipants > 0 && course.participantCount >= course.maxParticipants; + + return ( + + {/* Time column */} +
+
+ + {course.timeFrom} +
+
+ {course.timeTo} +
+ +
+ + {/* Course info */} +
+

{course.name}

+
+ {course.room && ( + {course.room} + )} + {course.sport.length > 0 && ( + <> + {course.room && ·} + + {course.sport[0]} + + + )} +
+
+ + {/* Occupancy */} +
+
+ + + {course.participantCount} + {course.maxParticipants > 0 && `/${course.maxParticipants}`} + +
+ {course.maxParticipants > 0 && ( +
+
75 ? "bg-amber-400" : "bg-green-400" + }`} + style={{ width: `${Math.min(occupancy, 100)}%` }} + /> +
+ )} + {isFull && ( + Ausgebucht + )} +
+ + + + ); + })} +
+ )} + + {courses.length > 0 && ( + + Alle Kurse ansehen + + )} +
+ ); +}; + +export default DashboardTodaySchedule; diff --git a/src/modules/dashboard/ui/views/dashboard-view.tsx b/src/modules/dashboard/ui/views/dashboard-view.tsx index a840c72..7995370 100644 --- a/src/modules/dashboard/ui/views/dashboard-view.tsx +++ b/src/modules/dashboard/ui/views/dashboard-view.tsx @@ -1,57 +1,95 @@ -import { LogoutButton } from "@/src/components/layout/LogoutButton"; -import { isManager, isEmployee, isUser, type AuthUserData } from "@/src/lib/auth/getUser"; +import { isManager, isEmployee, type AuthUserData } from "@/src/lib/auth/getUser"; +import DashboardStatsGrid from "../components/DashboardStatsGrid"; +import DashboardRevenueChart from "../components/DashboardRevenueChart"; +import DashboardTodaySchedule from "../components/DashboardTodaySchedule"; +import DashboardRecentPayments from "../components/DashboardRecentPayments"; +import { + getDashboardStats, + getDashboardRevenueChart, + getDashboardTodayCourses, + getDashboardRecentBookings, +} from "../../actions/dashboard-actions"; interface DashboardViewProps { userData: AuthUserData; } -const DashboardView = ({ userData }: DashboardViewProps) => { +const DashboardView = async ({ userData }: DashboardViewProps) => { const displayName = isManager(userData) - ? `${userData.firstName} ${userData.lastName}` + ? userData.firstName : isEmployee(userData) - ? `${userData.firstName ?? ""} ${userData.lastName ?? ""}`.trim() || userData.email - : userData.name || userData.email; + ? (userData.firstName ?? userData.email.split("@")[0]) + : userData.name || userData.email.split("@")[0]; - return ( -
-
-
-

- Dashboard -

-

Willkommen zurück!

-

{displayName}

-
- {isManager(userData) && ( - - Manager - - )} - {isEmployee(userData) && ( + if (isManager(userData)) { + const [statsResult, chartResult, todayResult, bookingsResult] = await Promise.all([ + getDashboardStats(), + getDashboardRevenueChart(), + getDashboardTodayCourses(), + getDashboardRecentBookings(), + ]); + + const stats = statsResult.success ? statsResult.stats : null; + const chartData = chartResult.success ? chartResult.data : []; + const todayCourses = todayResult.success ? todayResult.courses : []; + const recentBookings = bookingsResult.success ? bookingsResult.bookings : []; + + return ( +
+ {/* Welcome header */} +
+
+

+ Dashboard +

+

Hallo {displayName} 👋

+

+ Hier ist ein Überblick über dein Business. +

+
+ + {/* Stats tiles */} + {stats && } + + {/* Revenue chart */} + {chartData.length > 0 && } + + {/* Today's schedule + recent payments */} +
+ + +
+
+ ); + } + + if (isEmployee(userData)) { + const todayResult = await getDashboardTodayCourses(); + const todayCourses = todayResult.success ? todayResult.courses : []; + + return ( +
+
+
+

+ Dashboard +

+

Hallo {displayName} 👋

+
Mitarbeiter - )} -
-
- -
-
-

Profil

-

Verwalte deine persönlichen Daten

-
- - {isManager(userData) ? ( -
-

Meine Businesses

-

- {userData.businesses.length}{" "} - {userData.businesses.length === 1 ? "Business" : "Businesses"} -

- ) : isEmployee(userData) ? ( -
-

Meine Berechtigungen

+
+ +
+ + +
+

+ Berechtigungen +

+

Meine Zugänge

{(() => { const p = userData.permissions; const RESOURCE_LABELS: Record = { @@ -71,146 +109,77 @@ const DashboardView = ({ userData }: DashboardViewProps) => { .filter(([, v]) => v) .map(([a]) => ACTION_LABELS[a] ?? a); return active.length > 0 - ? `${RESOURCE_LABELS[res] ?? res}: ${active.join(", ")}` + ? { resource: RESOURCE_LABELS[res] ?? res, actions: active } : null; }) - .filter(Boolean); + .filter(Boolean) as { resource: string; actions: string[] }[]; + return rows.length > 0 ? ( -
    +
    {rows.map((row) => ( -
  • • {row}
  • +
    + + {row.resource} + +
    + {row.actions.map((a) => ( + + {a} + + ))} +
    +
    ))} -
+
) : ( -

Keine erweiterten Berechtigungen

+

Keine erweiterten Berechtigungen

); })()}
- ) : ( -
-

Trainings

-

Deine gebuchten Trainingseinheiten

-
- )} - -
-

Einstellungen

-

Passe deine Einstellungen an

+ ); + } - {isUser(userData) && ( -
-

Benutzerdaten

-
-
-
Name
-
{userData.name || "—"}
-
-
-
E-Mail
-
{userData.email}
-
-
-
Geburtsdatum
-
- {userData.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"} -
-
-
-
Wohnort
-
- {userData.plz && userData.city ? `${userData.plz} ${userData.city}` : "—"} -
-
-
-
Geschlecht
-
{userData.gender || "—"}
-
-
-
Registriert seit
-
- {userData.createdAt - ? new Date(userData.createdAt).toLocaleDateString("de-DE") - : "—"} -
-
-
-
- )} + // Regular user + return ( +
+
+
+

+ Dashboard +

+

Hallo {displayName} 👋

+

Willkommen zurück!

+
- {isManager(userData) && ( -
-

Manager-Daten

-
-
-
Vorname
-
{userData.firstName}
+
+

Meine Daten

+
+ {[ + { label: "Name", value: userData.name || "—" }, + { label: "E-Mail", value: userData.email }, + { + label: "Geburtsdatum", + value: userData.birthday + ? new Date(userData.birthday).toLocaleDateString("de-DE") + : "—", + }, + { + label: "Wohnort", + value: userData.plz && userData.city ? `${userData.plz} ${userData.city}` : "—", + }, + ].map(({ label, value }) => ( +
+
{label}
+
{value}
-
-
Nachname
-
{userData.lastName}
-
-
-
E-Mail
-
{userData.email}
-
-
-
Telefon
-
{userData.tel || "—"}
-
-
-
Registriert seit
-
- {userData.createdAt - ? new Date(userData.createdAt).toLocaleDateString("de-DE") - : "—"} -
-
-
-
- )} - - {isManager(userData) && userData.businesses.length > 0 && ( -
-

Meine Businesses

-
- {userData.businesses.map((business) => ( -
-

{business.name}

-

{business.address}

-

{business.email}

-
- ))} -
-
- )} - - {isEmployee(userData) && ( -
-

Mitarbeiter-Daten

-
-
-
Vorname
-
{userData.firstName || "—"}
-
-
-
Nachname
-
{userData.lastName || "—"}
-
-
-
E-Mail
-
{userData.email}
-
-
-
Qualifikation
-
{userData.qualification || "—"}
-
-
-
- )} - - + ))} +
+
); };