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 ? (
-
-
-
setCoverImage(null)}
- className="absolute top-2 right-2 bg-white rounded-full p-1 shadow hover:bg-gray-100"
- >
-
-
+
+ {/* Section 1: Grunddaten */}
+
+
+
+
- ) : (
-
- {isUploadingImage ? (
-
-
-
- {imageUploadPhase === "optimizing"
- ? "Bild wird optimiert…"
- : "Wird hochgeladen…"}
-
-
-
- ) : (
- <>
-
- Bild auswählen (max. 5 MB)
- >
- )}
- Grunddaten
+
+
+ {businesses.length > 1 && (
+
-
- )}
-
-
-
-
-
- {
- setIsStandingOrder(checked === true);
- if (!checked) {
- setSelectedFrequency("");
- setSelectedWeekdays([]);
- setWeekdayTimings({});
- setEndDate("");
- }
- }}
- />
-
- Als Dauerauftrag anlegen
-
-
-
- {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 */}
+
+
+
+
+
+
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 */}
+ {
+ const next = !isStandingOrder;
+ setIsStandingOrder(next);
+ if (!next) {
+ setSelectedFrequency("");
+ setSelectedWeekdays([]);
+ setWeekdayTimings({});
+ setEndDate("");
+ }
+ }}
+ className={`flex items-center gap-3 w-full rounded-xl border p-4 text-left transition-all cursor-pointer ${
+ isStandingOrder
+ ? "border-[#c9a84c]/50 bg-[#c9a84c]/5"
+ : "border-gray-200 bg-white/40 hover:border-gray-300 hover:bg-white/60"
+ }`}
+ >
+
+ {isStandingOrder && }
+
+
+
Als Dauerauftrag anlegen
+
+ Kurs wiederholt sich automatisch nach einem Zeitplan
+
+
+
+
+
+ {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 */}
+
+
+
+
+
+ {!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 */}
+
+
+
+
+
Kursbild (optional)
+ {coverImage ? (
+
+
+ setCoverImage(null)}
+ className="absolute top-2 right-2 bg-white rounded-full p-1 shadow hover:bg-gray-100"
+ >
+
+
+
+ ) : (
+
+ {isUploadingImage ? (
+
+
+
+ {imageUploadPhase === "optimizing"
+ ? "Bild wird optimiert…"
+ : "Wird hochgeladen…"}
+
+
+
+ ) : (
+ <>
+
+ Bild auswählen
+ max. 5 MB
+ >
+ )}
+
+
+ )}
+
+
+
+
+
+
+ {/* Actions */}
+
+
+ handleSubmit("published")}
+ disabled={isSubmitting || isUploadingImage}
+ className="bg-[#c9a84c] hover:bg-[#b8963e] text-white border-0"
+ >
+ {isSubmitting ? (
+ <>
+
+ Wird gespeichert...
+ >
+ ) : mode === "edit" ? (
+ "Änderungen speichern"
+ ) : (
+ "Veröffentlichen"
+ )}
+
+ router.push("/courses")}
+ disabled={isSubmitting || isUploadingImage}
+ >
+ Abbrechen
+
+
handleSubmit("published")}
+ variant="ghost"
+ onClick={() => handleSubmit("draft")}
disabled={isSubmitting || isUploadingImage}
+ className="text-gray-500 hover:text-gray-700"
>
- {isSubmitting
- ? "Wird gespeichert..."
- : mode === "edit"
- ? "Änderungen speichern"
- : "Veröffentlichen"}
-
-
router.push("/courses")}
- disabled={isSubmitting || isUploadingImage}
- >
- Abbrechen
+ {mode === "edit" ? "Als Entwurf speichern" : "Entwurf speichern"}
-
handleSubmit("draft")}
- disabled={isSubmitting || isUploadingImage}
- >
- {mode === "edit" ? "Als Entwurf speichern" : "Entwurf speichern"}
-
{
+ 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
-
-
-
- {selectedBusiness?.name || "Business auswählen"}
-
-
-
+ Business
+
+
+ {selectedBusiness?.name || "Business auswählen"}
+
+
@@ -95,10 +90,10 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
))}
-
+
-
- Standort hinzufügen
+
+ Standort hinzufügen
@@ -106,10 +101,42 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
)}
- {/* DisplayName + Avatar */}
-
-
-
{displayName}
+ {/* Profile dropdown */}
+
+
+
+
+
+
+
{displayName}
+
{userData.email}
+
+
+
+
+
+
+
+
+ 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 (
-
-
-
+
+
+
{state === "collapsed" || isMobile ? (
) : (
@@ -67,13 +64,28 @@ const DashboardNavbar = () => {
)}
-
+
-
{pageTitle}
+
{pageTitle}
-
-
-
+
+
+
+
+ {canCreateCourse && pathname === "/dashboard" && (
+
+
+
+ Neuer Kurs
+
+
+ )}
+
+
);
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) => (
+ setMetric(m.value)}
+ className={`px-3 py-1.5 text-xs font-semibold rounded-lg transition-all whitespace-nowrap ${
+ metric === m.value
+ ? "bg-white text-gray-900 shadow-sm"
+ : "text-gray-500 hover:text-gray-700"
+ }`}
+ style={metric === m.value ? { color: m.color } : {}}
+ >
+ {m.label}
+
+ ))}
+
+
+ {/* 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) => (
+ setRange(r.value)}
+ className={`px-2.5 py-1.5 text-xs font-semibold rounded-lg transition-all ${
+ range === r.value
+ ? "bg-gray-900 text-white shadow-sm"
+ : "text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ {r.label}
+
+ ))}
+
+
+
+
+ {/* 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 */}
+
+
+ Suchen oder navigieren…
+
+ ⌘ K
+
+
+
+
+
+ {/* 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 && (
+ setQuery("")}
+ className="text-xs text-gray-400 hover:text-gray-600 transition-colors px-1"
+ >
+ Löschen
+
+ )}
+
+
+ {/* 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 (
+
navigate(item.href)}
+ onMouseEnter={() => setActiveIndex(globalIdx)}
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors ${
+ isActive ? "bg-gray-50" : "hover:bg-gray-50/60"
+ }`}
+ >
+
+
+
+
+
{item.label}
+
{item.description}
+
+ {isActive && }
+
+ );
+ })}
+
+ ))
+ )}
+
+
+ {/* 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 (
+
+
+
+
{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 (
+
+
+
+
+
+ {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 || "—"}
-
-
-
- )}
-
-
+ ))}
+
+
);
};