mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
changed useEffect Fetching to RSC + Streaming
This commit is contained in:
parent
d8c02d398e
commit
9011c4699e
38 changed files with 661 additions and 619 deletions
262
package-lock.json
generated
262
package-lock.json
generated
|
|
@ -302,7 +302,7 @@
|
|||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -312,7 +312,7 @@
|
|||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -346,7 +346,7 @@
|
|||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
|
|
@ -438,7 +438,7 @@
|
|||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
|
|
@ -996,9 +996,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1015,9 +1012,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1034,9 +1028,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1053,9 +1044,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1072,9 +1060,6 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1091,9 +1076,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1110,9 +1092,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1129,9 +1108,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1148,9 +1124,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1173,9 +1146,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1198,9 +1168,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1223,9 +1190,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1248,9 +1212,6 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1273,9 +1234,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1298,9 +1256,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1323,9 +1278,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1653,9 +1605,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1672,9 +1621,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1691,9 +1637,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1710,9 +1653,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1911,18 +1851,6 @@
|
|||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
||||
|
|
@ -3554,9 +3482,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3574,9 +3499,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3594,9 +3516,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3614,9 +3533,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3634,9 +3550,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3654,9 +3567,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3776,66 +3686,6 @@
|
|||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.108.2.tgz",
|
||||
"integrity": "sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.108.2.tgz",
|
||||
"integrity": "sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/phoenix": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.4.tgz",
|
||||
"integrity": "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.108.2.tgz",
|
||||
"integrity": "sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.108.2.tgz",
|
||||
"integrity": "sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/phoenix": "^0.4.2",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz",
|
||||
|
|
@ -3848,37 +3698,6 @@
|
|||
"@supabase/supabase-js": "^2.43.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.108.2.tgz",
|
||||
"integrity": "sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iceberg-js": "^0.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.108.2.tgz",
|
||||
"integrity": "sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.108.2",
|
||||
"@supabase/functions-js": "2.108.2",
|
||||
"@supabase/postgrest-js": "2.108.2",
|
||||
"@supabase/realtime-js": "2.108.2",
|
||||
"@supabase/storage-js": "2.108.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
|
|
@ -4030,9 +3849,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4050,9 +3866,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4070,9 +3883,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4090,9 +3900,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4827,7 +4634,7 @@
|
|||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
|
|
@ -4847,6 +4654,7 @@
|
|||
"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"
|
||||
|
|
@ -4856,6 +4664,7 @@
|
|||
"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"
|
||||
|
|
@ -5253,9 +5062,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5270,9 +5076,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5287,9 +5090,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5304,9 +5104,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5321,9 +5118,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5338,9 +5132,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5355,9 +5146,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5372,9 +5160,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5389,9 +5174,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5406,9 +5188,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -6550,6 +6329,7 @@
|
|||
"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/damerau-levenshtein": {
|
||||
|
|
@ -8248,16 +8028,6 @@
|
|||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"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/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
@ -9207,9 +8977,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9231,9 +8998,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9255,9 +9019,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9279,9 +9040,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -12046,7 +11804,7 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -12103,7 +11861,7 @@
|
|||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
|
|
|
|||
32
src/app/(main)/courses/loading.tsx
Normal file
32
src/app/(main)/courses/loading.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function CoursesLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-3 w-16 mb-2" />
|
||||
<Skeleton className="h-7 w-28" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-5 space-y-3"
|
||||
>
|
||||
<Skeleton className="h-32 w-full rounded-xl" />
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<div className="flex justify-between pt-1">
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import { getMyCourses } from "@/src/modules/courses/actions/course-actions";
|
||||
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
|
||||
import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions";
|
||||
import CourseView from "@/src/modules/courses/ui/views/course-view";
|
||||
import CoursesLoading from "./loading";
|
||||
|
||||
const Course = async () => {
|
||||
const userData = await requireManagerOrPermission((p) => p.courses.view);
|
||||
|
|
@ -9,7 +14,50 @@ const Course = async () => {
|
|||
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.edit);
|
||||
const canDelete =
|
||||
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.delete);
|
||||
return <CourseView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const twoWeeksAgo = new Date(today);
|
||||
twoWeeksAgo.setDate(today.getDate() - 14);
|
||||
const sixWeeksLater = new Date(today);
|
||||
sixWeeksLater.setDate(today.getDate() + 42);
|
||||
|
||||
const [coursesResult, roomsResult, trainersResult] = await Promise.all([
|
||||
getMyCourses({ dateFrom: twoWeeksAgo, dateTo: sixWeeksLater }),
|
||||
getMyRooms(),
|
||||
getMyTrainers(),
|
||||
]);
|
||||
|
||||
const initialCourses = coursesResult.success ? coursesResult.courses : [];
|
||||
|
||||
const roomsMap: Record<string, string> = {};
|
||||
if (roomsResult.success) {
|
||||
roomsResult.rooms.forEach((r) => {
|
||||
roomsMap[r.id] = r.name;
|
||||
});
|
||||
}
|
||||
|
||||
const trainersMap: Record<string, { label: string; pbSrc?: string }> = {};
|
||||
if (trainersResult.success) {
|
||||
trainersResult.trainers.forEach((t) => {
|
||||
trainersMap[t.value] = { label: t.label, pbSrc: t.pbSrc ?? undefined };
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CoursesLoading />}>
|
||||
<CourseView
|
||||
initialCourses={initialCourses}
|
||||
initialDateFrom={twoWeeksAgo}
|
||||
initialDateTo={sixWeeksLater}
|
||||
roomsMap={roomsMap}
|
||||
trainersMap={trainersMap}
|
||||
canCreate={canCreate}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default Course;
|
||||
|
|
|
|||
28
src/app/(main)/dashboard/loading.tsx
Normal file
28
src/app/(main)/dashboard/loading.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-8 relative overflow-hidden">
|
||||
<Skeleton className="h-3 w-24 mb-3" />
|
||||
<Skeleton className="h-10 w-72 mb-2" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<div className="mt-4">
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-6"
|
||||
>
|
||||
<Skeleton className="h-4 w-36 mb-2" />
|
||||
<Skeleton className="h-3 w-52" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,14 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireAuthWithData } from "@/src/lib/auth/getUser";
|
||||
import DashboardView from "@/src/modules/dashboard/ui/views/dashboard-view";
|
||||
import DashboardLoading from "./loading";
|
||||
|
||||
export default function DashboardPage() {
|
||||
return <DashboardView />;
|
||||
export default async function DashboardPage() {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<DashboardLoading />}>
|
||||
<DashboardView userData={userData} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
37
src/app/(main)/employee/loading.tsx
Normal file
37
src/app/(main)/employee/loading.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function EmployeeLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-3 w-20 mb-2" />
|
||||
<Skeleton className="h-7 w-40" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-36 rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-white/40">
|
||||
<Skeleton className="h-9 w-64 rounded-xl" />
|
||||
</div>
|
||||
<div className="divide-y divide-white/30">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4">
|
||||
<Skeleton className="h-10 w-10 rounded-full shrink-0" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-4 w-36" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import { getMyEmployees } from "@/src/modules/employee/actions/employee-actions";
|
||||
import EmployeeView from "@/src/modules/employee/ui/views/employee-view";
|
||||
import EmployeeLoading from "./loading";
|
||||
|
||||
const Employee = async () => {
|
||||
const userData = await requireManagerOrPermission((p) => p.employees.view);
|
||||
|
|
@ -9,7 +12,20 @@ const Employee = async () => {
|
|||
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.edit);
|
||||
const canDelete =
|
||||
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.delete);
|
||||
return <EmployeeView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
|
||||
|
||||
const result = await getMyEmployees();
|
||||
const employees = result.success ? result.employees : [];
|
||||
|
||||
return (
|
||||
<Suspense fallback={<EmployeeLoading />}>
|
||||
<EmployeeView
|
||||
employees={employees}
|
||||
canCreate={canCreate}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default Employee;
|
||||
|
|
|
|||
33
src/app/(main)/kunden/loading.tsx
Normal file
33
src/app/(main)/kunden/loading.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function KundenLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-3 w-16 mb-2" />
|
||||
<Skeleton className="h-7 w-28" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-white/40">
|
||||
<Skeleton className="h-9 w-64 rounded-xl" />
|
||||
</div>
|
||||
<div className="divide-y divide-white/30">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4">
|
||||
<Skeleton className="h-10 w-10 rounded-full shrink-0" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-3 w-52" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { getKunden } from "@/src/modules/kunden/actions/kunden-actions";
|
||||
import KundenView from "@/src/modules/kunden/ui/views/kunden-view";
|
||||
import KundenLoading from "./loading";
|
||||
|
||||
const KundenPage = async () => {
|
||||
await requireManager();
|
||||
|
|
@ -9,7 +11,11 @@ const KundenPage = async () => {
|
|||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return <KundenView initialKunden={result.kunden} />;
|
||||
return (
|
||||
<Suspense fallback={<KundenLoading />}>
|
||||
<KundenView initialKunden={result.kunden} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default KundenPage;
|
||||
|
|
|
|||
30
src/app/(main)/loading.tsx
Normal file
30
src/app/(main)/loading.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function MainLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
{/* Page header */}
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-8">
|
||||
<Skeleton className="h-3 w-20 mb-3" />
|
||||
<Skeleton className="h-9 w-64 mb-2" />
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<div className="mt-4">
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-6"
|
||||
>
|
||||
<Skeleton className="h-4 w-32 mb-2" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/(main)/rooms/loading.tsx
Normal file
34
src/app/(main)/rooms/loading.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function RoomsLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-3 w-16 mb-2" />
|
||||
<Skeleton className="h-7 w-32" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-white/40">
|
||||
<Skeleton className="h-9 w-64 rounded-xl" />
|
||||
</div>
|
||||
<div className="divide-y divide-white/30">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4">
|
||||
<Skeleton className="h-10 w-10 rounded-xl shrink-0" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-3 w-56" />
|
||||
</div>
|
||||
<Skeleton className="h-7 w-16 rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
|
||||
import RoomsView from "@/src/modules/rooms/ui/views/rooms-view";
|
||||
import RoomsLoading from "./loading";
|
||||
|
||||
const Rooms = async () => {
|
||||
const userData = await requireManagerOrPermission((p) => p.rooms.view);
|
||||
|
|
@ -8,7 +11,15 @@ const Rooms = async () => {
|
|||
const canEdit = isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.edit);
|
||||
const canDelete =
|
||||
isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.delete);
|
||||
return <RoomsView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
|
||||
|
||||
const result = await getMyRooms();
|
||||
const rooms = result.success ? result.rooms : [];
|
||||
|
||||
return (
|
||||
<Suspense fallback={<RoomsLoading />}>
|
||||
<RoomsView rooms={rooms} canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rooms;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsBrandingPageView from "@/src/modules/settings/ui/views/settings-branding-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsBrandingPage() {
|
||||
await requireManager();
|
||||
return <SettingsBrandingPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsBrandingPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsBusinessPageView from "@/src/modules/settings/ui/views/settings-business-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsBusinessPage() {
|
||||
await requireManager();
|
||||
return <SettingsBusinessPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsBusinessPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsCancellationPageView from "@/src/modules/settings/ui/views/settings-cancellation-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsCancellationPage() {
|
||||
await requireManager();
|
||||
return <SettingsCancellationPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsCancellationPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsDiscountsPageView from "@/src/modules/settings/ui/views/settings-discounts-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsDiscountPage() {
|
||||
await requireManager();
|
||||
return <SettingsDiscountsPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsDiscountsPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
23
src/app/(main)/settings/loading.tsx
Normal file
23
src/app/(main)/settings/loading.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
|
||||
export default function SettingsLoading() {
|
||||
return (
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-8">
|
||||
<Skeleton className="h-3 w-20 mb-3" />
|
||||
<Skeleton className="h-8 w-48 mb-2" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-6 space-y-6">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-3 w-28" />
|
||||
<Skeleton className="h-10 w-full rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
<Skeleton className="h-10 w-32 rounded-xl mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsNotificationPageView from "@/src/modules/settings/ui/views/settings-notification-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsNotificationPage() {
|
||||
await requireManager();
|
||||
return <SettingsNotificationPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsNotificationPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireAuthWithData } from "@/src/lib/auth/getUser";
|
||||
import SettingsOverviewView from "@/src/modules/settings/ui/views/settings-overview-view";
|
||||
import SettingsLoading from "./loading";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return <SettingsOverviewView />;
|
||||
export default async function SettingsPage() {
|
||||
await requireAuthWithData();
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsOverviewView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { getBusinessesWithStripeStatus } from "@/src/modules/settings/actions/stripe-connect-actions";
|
||||
import SettingsPaymentsView from "@/src/modules/settings/ui/views/settings-payments-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsPaymentsPage() {
|
||||
await requireManager();
|
||||
return <SettingsPaymentsView />;
|
||||
const result = await getBusinessesWithStripeStatus();
|
||||
const businesses = result.success ? result.businesses : [];
|
||||
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsPaymentsView businesses={businesses} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireAuthWithData } from "@/src/lib/auth/getUser";
|
||||
import SettingsPersonalPageView from "@/src/modules/settings/ui/views/settings-personal-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsPersonalPage() {
|
||||
return <SettingsPersonalPageView />;
|
||||
await requireAuthWithData();
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsPersonalPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Suspense } from "react";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import SettingsPublicPageView from "@/src/modules/settings/ui/views/settings-public-page-view";
|
||||
import SettingsLoading from "../loading";
|
||||
|
||||
export default async function SettingsPublicPage() {
|
||||
await requireManager();
|
||||
return <SettingsPublicPageView />;
|
||||
return (
|
||||
<Suspense fallback={<SettingsLoading />}>
|
||||
<SettingsPublicPageView />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export async function middleware(request: NextRequest) {
|
|||
}
|
||||
);
|
||||
|
||||
// IMPORTANT: do not destroy supabaseResponse between createServerClient and supabase.auth.getUser()
|
||||
// Any logic between these two calls that creates a new NextResponse will break session refresh.
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { deleteCourse } from "../actions/course-actions";
|
|||
import { toast } from "sonner";
|
||||
|
||||
interface UseDeleteCourseOptions {
|
||||
onSuccess?: (deletedId: string) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function useDeleteCourse({ onSuccess }: UseDeleteCourseOptions = {}) {
|
||||
|
|
@ -27,9 +27,8 @@ export function useDeleteCourse({ onSuccess }: UseDeleteCourseOptions = {}) {
|
|||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
const deletedId = courseToDelete.id;
|
||||
setCourseToDelete(null);
|
||||
onSuccess?.(deletedId);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
setCourseToDelete(null);
|
||||
toast.error(result.error || "Fehler beim Löschen des Kurses");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useEffect, useSyncExternalStore } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Clock, MapPin, Users, Mail, Phone, DoorOpen, Info } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/src/components/ui/dialog";
|
||||
|
|
@ -409,9 +410,9 @@ export const BookingDialog = ({
|
|||
<div className="border-t border-gray-100 pt-3 flex flex-col gap-2">
|
||||
<p className="text-xs text-gray-500">
|
||||
Ohne Konto buchen – oder{" "}
|
||||
<a href="/login" className="underline text-gray-700 hover:text-gray-900">
|
||||
<Link href="/login" className="underline text-gray-700 hover:text-gray-900">
|
||||
anmelden
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { getCourseById } from "../../actions/course-actions";
|
||||
import { toast } from "sonner";
|
||||
import CourseCreateView from "./course-create-view";
|
||||
|
|
@ -40,8 +41,15 @@ const CourseEditView = ({ courseId, customSports }: CourseEditViewProps) => {
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Kurs wird geladen...</p>
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-8 space-y-6 mt-4">
|
||||
<Skeleton className="h-7 w-48" />
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-10 w-full rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { getMyCourses } from "../../actions/course-actions";
|
||||
import { useDeleteCourse } from "../../hooks/useDeleteCourse";
|
||||
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
|
||||
import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
|
@ -33,22 +33,38 @@ type Course = {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
coverImage: string | null;
|
||||
_count?: {
|
||||
bookings: number;
|
||||
};
|
||||
_count?: { bookings: number };
|
||||
};
|
||||
|
||||
interface CourseListViewProps {
|
||||
initialCourses: Course[];
|
||||
initialDateFrom: Date;
|
||||
initialDateTo: Date;
|
||||
roomsMap: Record<string, string>;
|
||||
trainersMap: Record<string, { label: string; pbSrc?: string }>;
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps) => {
|
||||
const CourseListView = ({
|
||||
initialCourses,
|
||||
initialDateFrom,
|
||||
initialDateTo,
|
||||
roomsMap,
|
||||
trainersMap,
|
||||
canCreate,
|
||||
canEdit,
|
||||
canDelete,
|
||||
}: CourseListViewProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [courses, setCourses] = useState<Course[]>(initialCourses);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadedDateFrom, setLoadedDateFrom] = useState<Date>(initialDateFrom);
|
||||
const [loadedDateTo, setLoadedDateTo] = useState<Date>(initialDateTo);
|
||||
|
||||
const [filterStatus, setFilterStatus] = useState<"all" | "draft" | "published">("all");
|
||||
const [filterSport, setFilterSport] = useState<string>("all");
|
||||
const [filterTrainer, setFilterTrainer] = useState<string>(searchParams.get("trainer") ?? "all");
|
||||
|
|
@ -61,10 +77,6 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
const [priceMin, setPriceMin] = useState<number | null>(null);
|
||||
const [priceMax, setPriceMax] = useState<number | null>(null);
|
||||
const [deleteMode, setDeleteMode] = useState(false);
|
||||
const [roomsMap, setRoomsMap] = useState<Record<string, string>>({});
|
||||
const [trainersMap, setTrainersMap] = useState<Record<string, { label: string; pbSrc?: string }>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
deleteDialogOpen,
|
||||
|
|
@ -75,47 +87,10 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
} = useDeleteCourse({
|
||||
onSuccess: async () => {
|
||||
if (loadedDateFrom && loadedDateTo) {
|
||||
await loadCourses({ dateFrom: loadedDateFrom, dateTo: loadedDateTo });
|
||||
}
|
||||
},
|
||||
onSuccess: () => router.refresh(),
|
||||
});
|
||||
|
||||
const [loadedDateFrom, setLoadedDateFrom] = useState<Date | null>(null);
|
||||
const [loadedDateTo, setLoadedDateTo] = useState<Date | null>(null);
|
||||
|
||||
// Load rooms once on mount
|
||||
useEffect(() => {
|
||||
const loadRooms = async () => {
|
||||
const result = await getMyRooms();
|
||||
if (result.success) {
|
||||
const map: Record<string, string> = {};
|
||||
result.rooms.forEach((room) => {
|
||||
map[room.id] = room.name;
|
||||
});
|
||||
setRoomsMap(map);
|
||||
}
|
||||
};
|
||||
loadRooms();
|
||||
}, []);
|
||||
|
||||
// Load trainers once on mount
|
||||
useEffect(() => {
|
||||
const loadTrainers = async () => {
|
||||
const result = await getMyTrainers();
|
||||
if (result.success) {
|
||||
const map: Record<string, { label: string; pbSrc?: string }> = {};
|
||||
result.trainers.forEach((t) => {
|
||||
map[t.value] = { label: t.label, pbSrc: t.pbSrc ?? undefined };
|
||||
});
|
||||
setTrainersMap(map);
|
||||
}
|
||||
};
|
||||
loadTrainers();
|
||||
}, []);
|
||||
|
||||
const loadCourses = async (options?: { dateFrom?: Date; dateTo?: Date }) => {
|
||||
const loadCourses = async (options: { dateFrom: Date; dateTo: Date }) => {
|
||||
const result = await getMyCourses(options);
|
||||
if (result.success) {
|
||||
setCourses(result.courses);
|
||||
|
|
@ -124,45 +99,15 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial load: Load courses from -2 weeks to +6 weeks from today
|
||||
*/
|
||||
// Extend loaded date range only when filter dates go outside the server-fetched range
|
||||
useEffect(() => {
|
||||
const fetchCourses = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Calculate default date range: -2 weeks to +6 weeks from today
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const twoWeeksAgo = new Date(today);
|
||||
twoWeeksAgo.setDate(today.getDate() - 14); // -2 weeks
|
||||
|
||||
const sixWeeksLater = new Date(today);
|
||||
sixWeeksLater.setDate(today.getDate() + 42); // +6 weeks
|
||||
|
||||
// Store the loaded date range
|
||||
setLoadedDateFrom(twoWeeksAgo);
|
||||
setLoadedDateTo(sixWeeksLater);
|
||||
|
||||
await loadCourses({ dateFrom: twoWeeksAgo, dateTo: sixWeeksLater });
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Extend loaded date range when filter dates are outside the current range
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!loadedDateFrom || !loadedDateTo || isLoading) return;
|
||||
if (isLoading) return;
|
||||
|
||||
const extendRange = async () => {
|
||||
let needsReload = false;
|
||||
let newDateFrom = loadedDateFrom;
|
||||
let newDateTo = loadedDateTo;
|
||||
|
||||
// Check if dateFrom filter is before loaded range
|
||||
if (dateFrom) {
|
||||
const filterFromDate = new Date(dateFrom);
|
||||
filterFromDate.setHours(0, 0, 0, 0);
|
||||
|
|
@ -172,7 +117,6 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
}
|
||||
}
|
||||
|
||||
// Check if dateTo filter is after loaded range
|
||||
if (dateTo) {
|
||||
const filterToDate = new Date(dateTo);
|
||||
filterToDate.setHours(0, 0, 0, 0);
|
||||
|
|
@ -192,22 +136,18 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
};
|
||||
|
||||
extendRange();
|
||||
}, [dateFrom, dateTo, loadedDateFrom, loadedDateTo, isLoading]);
|
||||
}, [dateFrom, dateTo]);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString("de-DE", {
|
||||
const formatDate = (date: Date) =>
|
||||
new Date(date).toLocaleDateString("de-DE", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateKey = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString("de-DE");
|
||||
};
|
||||
const formatDateKey = (date: Date) => new Date(date).toLocaleDateString("de-DE");
|
||||
|
||||
// Helper to check if a course is in the past
|
||||
const isPastCourse = (courseDate: Date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
|
@ -216,22 +156,15 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
return course < today;
|
||||
};
|
||||
|
||||
// Filter courses (client-side filtering for status, sport, etc.)
|
||||
const filteredCourses = courses.filter((course) => {
|
||||
if (filterStatus !== "all" && course.status !== filterStatus) return false;
|
||||
if (filterSport !== "all" && !course.sport.includes(filterSport)) return false;
|
||||
if (filterLevel !== "all" && course.level !== filterLevel) return false;
|
||||
|
||||
// Trainer filter
|
||||
if (filterTrainer !== "all" && !course.trainers.includes(filterTrainer)) return false;
|
||||
|
||||
// Room filter (compare by room name, not ID)
|
||||
if (filterRoom !== "all") {
|
||||
const roomName = roomsMap[course.room] || course.room;
|
||||
if (roomName !== filterRoom) return false;
|
||||
}
|
||||
|
||||
// Date range filter (additional client-side filter on top of server-side)
|
||||
if (dateFrom) {
|
||||
const fromDate = new Date(dateFrom);
|
||||
fromDate.setHours(0, 0, 0, 0);
|
||||
|
|
@ -239,14 +172,12 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
courseDate.setHours(0, 0, 0, 0);
|
||||
if (courseDate < fromDate) return false;
|
||||
} else {
|
||||
// If no dateFrom filter is set, only show courses from today onwards
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const courseDate = new Date(course.date);
|
||||
courseDate.setHours(0, 0, 0, 0);
|
||||
if (courseDate < today) return false;
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
const toDate = new Date(dateTo);
|
||||
toDate.setHours(0, 0, 0, 0);
|
||||
|
|
@ -254,66 +185,40 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
courseDate.setHours(0, 0, 0, 0);
|
||||
if (courseDate > toDate) return false;
|
||||
}
|
||||
|
||||
// Time range filter
|
||||
if (timeFrom && course.timeFrom < timeFrom) return false;
|
||||
if (timeTo && course.timeTo > timeTo) return false;
|
||||
|
||||
// Price range filter
|
||||
if (priceMin !== null && course.price < priceMin) return false;
|
||||
if (priceMax !== null && course.price > priceMax) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Group courses by date
|
||||
const groupedCourses = filteredCourses.reduce(
|
||||
(acc, course) => {
|
||||
const dateKey = formatDateKey(course.date);
|
||||
if (!acc[dateKey]) {
|
||||
acc[dateKey] = [];
|
||||
}
|
||||
if (!acc[dateKey]) acc[dateKey] = [];
|
||||
acc[dateKey].push(course);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Course[]>
|
||||
);
|
||||
|
||||
// Sort dates
|
||||
const sortedDates = Object.keys(groupedCourses).sort((a, b) => {
|
||||
return (
|
||||
const sortedDates = Object.keys(groupedCourses).sort(
|
||||
(a, b) =>
|
||||
new Date(a.split(".").reverse().join("-")).getTime() -
|
||||
new Date(b.split(".").reverse().join("-")).getTime()
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
// Calculate price range from courses
|
||||
const priceRangeMin =
|
||||
courses.length > 0 ? Math.floor(Math.min(...courses.map((c) => c.price))) : 0;
|
||||
const priceRangeMax =
|
||||
courses.length > 0 ? Math.ceil(Math.max(...courses.map((c) => c.price))) : 0;
|
||||
|
||||
// Get unique sports for filter
|
||||
const uniqueSports = Array.from(new Set(courses.flatMap((c) => c.sport)));
|
||||
|
||||
// Get unique trainers for filter (flatten trainer UIDs, map to labels)
|
||||
const uniqueTrainers = Array.from(new Set(courses.flatMap((c) => c.trainers))).map((uid) => ({
|
||||
value: uid,
|
||||
label: trainersMap[uid]?.label ?? uid,
|
||||
}));
|
||||
|
||||
// Get unique rooms for filter (using room names from roomsMap)
|
||||
const uniqueRooms = Array.from(new Set(courses.map((c) => roomsMap[c.room] || c.room)));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Kurse werden geladen...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Count active filters
|
||||
const activeFiltersCount = [
|
||||
filterStatus !== "all",
|
||||
filterSport !== "all",
|
||||
|
|
@ -328,6 +233,84 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
priceMax !== null && priceMax !== priceRangeMax,
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mt-6">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-5 space-y-3"
|
||||
>
|
||||
<Skeleton className="h-32 w-full rounded-xl" />
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<div className="flex justify-between pt-1">
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filterProps = {
|
||||
filterStatus,
|
||||
filterSport,
|
||||
filterTrainer,
|
||||
filterRoom,
|
||||
filterLevel,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
priceMin: priceMin ?? priceRangeMin,
|
||||
priceMax: priceMax ?? priceRangeMax,
|
||||
priceRangeMin,
|
||||
priceRangeMax,
|
||||
uniqueSports,
|
||||
uniqueTrainers,
|
||||
uniqueRooms,
|
||||
onApplyFilters: (filters: {
|
||||
filterStatus: "all" | "draft" | "published";
|
||||
filterSport: string;
|
||||
filterTrainer: string;
|
||||
filterRoom: string;
|
||||
filterLevel: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
timeFrom: string;
|
||||
timeTo: string;
|
||||
priceMin: number;
|
||||
priceMax: number;
|
||||
}) => {
|
||||
setFilterStatus(filters.filterStatus);
|
||||
setFilterSport(filters.filterSport);
|
||||
setFilterTrainer(filters.filterTrainer);
|
||||
setFilterRoom(filters.filterRoom);
|
||||
setFilterLevel(filters.filterLevel);
|
||||
setDateFrom(filters.dateFrom);
|
||||
setDateTo(filters.dateTo);
|
||||
setTimeFrom(filters.timeFrom);
|
||||
setTimeTo(filters.timeTo);
|
||||
setPriceMin(filters.priceMin === priceRangeMin ? null : filters.priceMin);
|
||||
setPriceMax(filters.priceMax === priceRangeMax ? null : filters.priceMax);
|
||||
},
|
||||
onReset: () => {
|
||||
setFilterStatus("all");
|
||||
setFilterSport("all");
|
||||
setFilterTrainer("all");
|
||||
setFilterRoom("all");
|
||||
setFilterLevel("all");
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
setTimeFrom("");
|
||||
setTimeTo("");
|
||||
setPriceMin(null);
|
||||
setPriceMax(null);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
|
|
@ -348,53 +331,8 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile: Filter button below the two action buttons */}
|
||||
<div className="sm:hidden">
|
||||
<CourseFilter
|
||||
filterStatus={filterStatus}
|
||||
filterSport={filterSport}
|
||||
filterTrainer={filterTrainer}
|
||||
filterRoom={filterRoom}
|
||||
filterLevel={filterLevel}
|
||||
dateFrom={dateFrom}
|
||||
dateTo={dateTo}
|
||||
timeFrom={timeFrom}
|
||||
timeTo={timeTo}
|
||||
priceMin={priceMin ?? priceRangeMin}
|
||||
priceMax={priceMax ?? priceRangeMax}
|
||||
priceRangeMin={priceRangeMin}
|
||||
priceRangeMax={priceRangeMax}
|
||||
uniqueSports={uniqueSports}
|
||||
uniqueTrainers={uniqueTrainers}
|
||||
uniqueRooms={uniqueRooms}
|
||||
onApplyFilters={(filters) => {
|
||||
setFilterStatus(filters.filterStatus);
|
||||
setFilterSport(filters.filterSport);
|
||||
setFilterTrainer(filters.filterTrainer);
|
||||
setFilterRoom(filters.filterRoom);
|
||||
setFilterLevel(filters.filterLevel);
|
||||
setDateFrom(filters.dateFrom);
|
||||
setDateTo(filters.dateTo);
|
||||
setTimeFrom(filters.timeFrom);
|
||||
setTimeTo(filters.timeTo);
|
||||
setPriceMin(filters.priceMin === priceRangeMin ? null : filters.priceMin);
|
||||
setPriceMax(filters.priceMax === priceRangeMax ? null : filters.priceMax);
|
||||
}}
|
||||
onReset={() => {
|
||||
setFilterStatus("all");
|
||||
setFilterSport("all");
|
||||
setFilterTrainer("all");
|
||||
setFilterRoom("all");
|
||||
setFilterLevel("all");
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
setTimeFrom("");
|
||||
setTimeTo("");
|
||||
setPriceMin(null);
|
||||
setPriceMax(null);
|
||||
}}
|
||||
>
|
||||
<CourseFilter {...filterProps}>
|
||||
<Button variant="outline" className="flex w-full items-center gap-2 relative">
|
||||
<Filter size={18} />
|
||||
Filter
|
||||
|
|
@ -406,52 +344,7 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
</Button>
|
||||
</CourseFilter>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Filter + Kurs löschen nebeneinander */}
|
||||
<CourseFilter
|
||||
filterStatus={filterStatus}
|
||||
filterSport={filterSport}
|
||||
filterTrainer={filterTrainer}
|
||||
filterRoom={filterRoom}
|
||||
filterLevel={filterLevel}
|
||||
dateFrom={dateFrom}
|
||||
dateTo={dateTo}
|
||||
timeFrom={timeFrom}
|
||||
timeTo={timeTo}
|
||||
priceMin={priceMin ?? priceRangeMin}
|
||||
priceMax={priceMax ?? priceRangeMax}
|
||||
priceRangeMin={priceRangeMin}
|
||||
priceRangeMax={priceRangeMax}
|
||||
uniqueSports={uniqueSports}
|
||||
uniqueTrainers={uniqueTrainers}
|
||||
uniqueRooms={uniqueRooms}
|
||||
onApplyFilters={(filters) => {
|
||||
setFilterStatus(filters.filterStatus);
|
||||
setFilterSport(filters.filterSport);
|
||||
setFilterTrainer(filters.filterTrainer);
|
||||
setFilterRoom(filters.filterRoom);
|
||||
setFilterLevel(filters.filterLevel);
|
||||
setDateFrom(filters.dateFrom);
|
||||
setDateTo(filters.dateTo);
|
||||
setTimeFrom(filters.timeFrom);
|
||||
setTimeTo(filters.timeTo);
|
||||
setPriceMin(filters.priceMin === priceRangeMin ? null : filters.priceMin);
|
||||
setPriceMax(filters.priceMax === priceRangeMax ? null : filters.priceMax);
|
||||
}}
|
||||
onReset={() => {
|
||||
setFilterStatus("all");
|
||||
setFilterSport("all");
|
||||
setFilterTrainer("all");
|
||||
setFilterRoom("all");
|
||||
setFilterLevel("all");
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
setTimeFrom("");
|
||||
setTimeTo("");
|
||||
setPriceMin(null);
|
||||
setPriceMax(null);
|
||||
}}
|
||||
>
|
||||
<CourseFilter {...filterProps}>
|
||||
<Button variant="outline" className="hidden sm:flex items-center gap-2 relative">
|
||||
<Filter size={18} />
|
||||
Filter
|
||||
|
|
@ -495,7 +388,6 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
{sortedDates.map((dateKey) => {
|
||||
const coursesForDate = groupedCourses[dateKey];
|
||||
const firstCourse = coursesForDate[0];
|
||||
|
||||
return (
|
||||
<div key={dateKey}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
|
|
|
|||
|
|
@ -5,13 +5,51 @@ import { Button } from "@/src/components/ui/button";
|
|||
import { useRouter } from "next/navigation";
|
||||
import CourseListView from "./course-list-view";
|
||||
|
||||
type Course = {
|
||||
id: string;
|
||||
name: string;
|
||||
sport: string[];
|
||||
level: string;
|
||||
date: Date;
|
||||
timeFrom: string;
|
||||
timeTo: string;
|
||||
trainers: string[];
|
||||
room: string;
|
||||
description: string;
|
||||
maxParticipants: number;
|
||||
price: number;
|
||||
isStandingOrder: boolean;
|
||||
frequency: string | null;
|
||||
weekdays: string[];
|
||||
status: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
coverImage: string | null;
|
||||
_count?: { bookings: number };
|
||||
};
|
||||
|
||||
interface CourseViewProps {
|
||||
initialCourses: Course[];
|
||||
initialDateFrom: Date;
|
||||
initialDateTo: Date;
|
||||
roomsMap: Record<string, string>;
|
||||
trainersMap: Record<string, { label: string; pbSrc?: string }>;
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const CourseView = ({ canCreate, canEdit, canDelete }: CourseViewProps) => {
|
||||
const CourseView = ({
|
||||
initialCourses,
|
||||
initialDateFrom,
|
||||
initialDateTo,
|
||||
roomsMap,
|
||||
trainersMap,
|
||||
canCreate,
|
||||
canEdit,
|
||||
canDelete,
|
||||
}: CourseViewProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
|
|
@ -30,7 +68,16 @@ const CourseView = ({ canCreate, canEdit, canDelete }: CourseViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
<Suspense>
|
||||
<CourseListView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />
|
||||
<CourseListView
|
||||
initialCourses={initialCourses}
|
||||
initialDateFrom={initialDateFrom}
|
||||
initialDateTo={initialDateTo}
|
||||
roomsMap={roomsMap}
|
||||
trainersMap={trainersMap}
|
||||
canCreate={canCreate}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { LogoutButton } from "@/src/components/layout/LogoutButton";
|
||||
import { isManager, isEmployee, requireAuthWithData, isUser } from "@/src/lib/auth/getUser";
|
||||
import { isManager, isEmployee, isUser, type AuthUserData } from "@/src/lib/auth/getUser";
|
||||
|
||||
const DashboardView = async () => {
|
||||
const userData = await requireAuthWithData();
|
||||
interface DashboardViewProps {
|
||||
userData: AuthUserData;
|
||||
}
|
||||
|
||||
const DashboardView = ({ userData }: DashboardViewProps) => {
|
||||
const displayName = isManager(userData)
|
||||
? `${userData.firstName} ${userData.lastName}`
|
||||
: isEmployee(userData)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { deleteEmployee, getActiveCoursesCountByTrainer } from "../actions/emplo
|
|||
import { toast } from "sonner";
|
||||
|
||||
interface UseDeleteEmployeeOptions {
|
||||
onSuccess?: (deletedId: string) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function useDeleteEmployee({ onSuccess }: UseDeleteEmployeeOptions = {}) {
|
||||
|
|
@ -32,10 +32,9 @@ export function useDeleteEmployee({ onSuccess }: UseDeleteEmployeeOptions = {})
|
|||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
const deletedId = employeeToDelete.id;
|
||||
setEmployeeToDelete(null);
|
||||
setActiveCourseCount(0);
|
||||
onSuccess?.(deletedId);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
setEmployeeToDelete(null);
|
||||
setActiveCourseCount(0);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { getEmployeeById } from "../../actions/employee-actions";
|
||||
import { toast } from "sonner";
|
||||
import EmployeeCreateView from "./employee-create-view";
|
||||
|
|
@ -58,8 +59,15 @@ const EmployeeEditView = ({
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Mitarbeiter wird geladen...</p>
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-8 space-y-6 mt-4">
|
||||
<Skeleton className="h-7 w-56" />
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-10 w-full rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
<Skeleton className="h-10 w-36 rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMyEmployees, resendOnboardingEmail } from "../../actions/employee-actions";
|
||||
import { resendOnboardingEmail } from "../../actions/employee-actions";
|
||||
import EmployeeListItem from "../components/EmployeeListItem";
|
||||
import { toast } from "sonner";
|
||||
import DeleteDialog from "@/src/components/layout/DeleteDialog";
|
||||
|
|
@ -23,15 +25,14 @@ type Employee = {
|
|||
};
|
||||
|
||||
interface EmployeeListViewProps {
|
||||
employees: Employee[];
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewProps) => {
|
||||
const EmployeeListView = ({ employees, canCreate, canEdit, canDelete }: EmployeeListViewProps) => {
|
||||
const router = useRouter();
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [deleteMode, setDeleteMode] = useState(false);
|
||||
const [resendingId, setResendingId] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -45,24 +46,9 @@ const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewPro
|
|||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
} = useDeleteEmployee({
|
||||
onSuccess: (deletedId) => setEmployees((prev) => prev.filter((e) => e.id !== deletedId)),
|
||||
onSuccess: () => router.refresh(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadEmployees = async () => {
|
||||
setIsLoading(true);
|
||||
const result = await getMyEmployees();
|
||||
if (result.success) {
|
||||
setEmployees(result.employees);
|
||||
} else {
|
||||
toast.error(result.error || "Fehler beim Laden der Mitarbeiter");
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadEmployees();
|
||||
}, []);
|
||||
|
||||
const handleResendEmail = async (employeeId: string) => {
|
||||
setResendingId(employeeId);
|
||||
try {
|
||||
|
|
@ -72,8 +58,7 @@ const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewPro
|
|||
} else {
|
||||
toast.error(result.error || "Fehler beim Versenden der E-Mail");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resending onboarding email:", error);
|
||||
} catch {
|
||||
toast.error("Fehler beim Versenden der E-Mail");
|
||||
} finally {
|
||||
setResendingId(null);
|
||||
|
|
@ -87,14 +72,6 @@ const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewPro
|
|||
return employee.email;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Mitarbeiter wird geladen</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
|
|
|
|||
|
|
@ -4,13 +4,28 @@ import { Button } from "@/src/components/ui/button";
|
|||
import { useRouter } from "next/navigation";
|
||||
import EmployeeListView from "./employee-list-view";
|
||||
|
||||
type Employee = {
|
||||
id: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
email: string;
|
||||
roles: string[];
|
||||
locations: string[];
|
||||
permissions: unknown;
|
||||
status: string | null;
|
||||
isOnboarded: boolean;
|
||||
createdAt: Date | null;
|
||||
updatedAt: Date | null;
|
||||
};
|
||||
|
||||
interface EmployeeViewProps {
|
||||
employees: Employee[];
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const EmployeeView = ({ canCreate, canEdit, canDelete }: EmployeeViewProps) => {
|
||||
const EmployeeView = ({ employees, canCreate, canEdit, canDelete }: EmployeeViewProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
|
|
@ -28,7 +43,12 @@ const EmployeeView = ({ canCreate, canEdit, canDelete }: EmployeeViewProps) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EmployeeListView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />
|
||||
<EmployeeListView
|
||||
employees={employees}
|
||||
canCreate={canCreate}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { deleteRoom } from "../actions/room-actions";
|
|||
import { toast } from "sonner";
|
||||
|
||||
interface UseDeleteRoomOptions {
|
||||
onSuccess?: (deletedId: string) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function useDeleteRoom({ onSuccess }: UseDeleteRoomOptions = {}) {
|
||||
|
|
@ -28,9 +28,8 @@ export function useDeleteRoom({ onSuccess }: UseDeleteRoomOptions = {}) {
|
|||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDeleteDialogOpen(false);
|
||||
const deletedId = roomToDelete.id;
|
||||
setRoomToDelete(null);
|
||||
onSuccess?.(deletedId);
|
||||
onSuccess?.();
|
||||
} else if (result.hasActiveCourses) {
|
||||
setDeleteDialogOpen(false);
|
||||
setRoomToDelete(null);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { getRoomById } from "../../actions/room-actions";
|
||||
import { toast } from "sonner";
|
||||
import RoomsCreateView from "./rooms-create-view";
|
||||
|
|
@ -39,8 +40,13 @@ const RoomsEditView = ({ roomId }: RoomsEditViewProps) => {
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Raum wird geladen...</p>
|
||||
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-8 space-y-6 mt-4">
|
||||
<Skeleton className="h-7 w-48" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-3 w-20" />
|
||||
<Skeleton className="h-10 w-full rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMyRooms } from "../../actions/room-actions";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import RoomsListItem from "../components/RoomsListItem";
|
||||
import DeleteDialog from "@/src/components/layout/DeleteDialog";
|
||||
|
|
@ -16,15 +14,14 @@ type Room = {
|
|||
};
|
||||
|
||||
interface RoomsListViewProps {
|
||||
rooms: Room[];
|
||||
deleteMode: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const RoomsListView = ({ deleteMode, canEdit, canDelete }: RoomsListViewProps) => {
|
||||
const RoomsListView = ({ rooms, deleteMode, canEdit, canDelete }: RoomsListViewProps) => {
|
||||
const router = useRouter();
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const {
|
||||
deleteDialogOpen,
|
||||
|
|
@ -35,45 +32,15 @@ const RoomsListView = ({ deleteMode, canEdit, canDelete }: RoomsListViewProps) =
|
|||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
} = useDeleteRoom({
|
||||
onSuccess: (deletedId) => setRooms((prev) => prev.filter((room) => room.id !== deletedId)),
|
||||
onSuccess: () => router.refresh(),
|
||||
});
|
||||
|
||||
// Load rooms
|
||||
const loadRooms = async () => {
|
||||
const result = await getMyRooms();
|
||||
if (result.success) {
|
||||
setRooms(result.rooms);
|
||||
} else {
|
||||
toast.error(result.error || "Fehler beim Laden der Räume");
|
||||
}
|
||||
};
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
const fetchRooms = async () => {
|
||||
setIsLoading(true);
|
||||
await loadRooms();
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchRooms();
|
||||
}, []);
|
||||
|
||||
// Handle edit click
|
||||
const handleEditClick = (id: string) => {
|
||||
router.push(`/rooms/edit/${id}`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-xl">Räume werden geladen...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
{/* Rooms List */}
|
||||
{rooms.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg">Keine Räume gefunden</p>
|
||||
|
|
@ -92,7 +59,6 @@ const RoomsListView = ({ deleteMode, canEdit, canDelete }: RoomsListViewProps) =
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,22 @@ import { Button } from "@/src/components/ui/button";
|
|||
import { useRouter } from "next/navigation";
|
||||
import RoomsListView from "./rooms-list-view";
|
||||
|
||||
type Room = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
interface RoomsViewProps {
|
||||
rooms: Room[];
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const RoomsView = ({ canCreate, canEdit, canDelete }: RoomsViewProps) => {
|
||||
const RoomsView = ({ rooms, canCreate, canEdit, canDelete }: RoomsViewProps) => {
|
||||
const router = useRouter();
|
||||
const [deleteMode, setDeleteMode] = useState(false);
|
||||
|
||||
|
|
@ -39,7 +48,12 @@ const RoomsView = ({ canCreate, canEdit, canDelete }: RoomsViewProps) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RoomsListView deleteMode={deleteMode && canDelete} canEdit={canEdit} canDelete={canDelete} />
|
||||
<RoomsListView
|
||||
rooms={rooms}
|
||||
deleteMode={deleteMode && canDelete}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
createConnectOnboardingLink,
|
||||
createStripeExpressDashboardLink,
|
||||
getBusinessesWithStripeStatus,
|
||||
} from "@/src/modules/settings/actions/stripe-connect-actions";
|
||||
import { toast } from "sonner";
|
||||
import { CheckCircle2, AlertCircle, CreditCard, ExternalLink, Loader2 } from "lucide-react";
|
||||
|
|
@ -17,29 +16,12 @@ type BusinessWithStatus = {
|
|||
stripeAccountStatus: string | null;
|
||||
};
|
||||
|
||||
const SettingsPaymentsView = () => {
|
||||
const [businesses, setBusinesses] = useState<BusinessWithStatus[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
interface SettingsPaymentsViewProps {
|
||||
businesses: BusinessWithStatus[];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadBusinesses = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getBusinessesWithStripeStatus();
|
||||
if (result.success) {
|
||||
setBusinesses(result.businesses);
|
||||
} else {
|
||||
toast.error(result.error || "Fehler beim Laden");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Fehler beim Laden");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
void loadBusinesses();
|
||||
}, []);
|
||||
const SettingsPaymentsView = ({ businesses }: SettingsPaymentsViewProps) => {
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = async (businessId: string) => {
|
||||
setPendingId(businessId);
|
||||
|
|
@ -105,14 +87,6 @@ const SettingsPaymentsView = () => {
|
|||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue