Update training group management and enhance UI components
This commit introduces the `TrainingGroup` model and related functionality, allowing for the management of training groups within the application. The `ClubService` is updated to automatically create preset groups upon club creation. The frontend is enhanced with new views and components, including `TrainingGroupsView` and `TrainingGroupsTab`, to facilitate the display and management of training groups. Additionally, the `MembersView` is updated to allow adding and removing members from training groups, improving the overall user experience and interactivity in managing club members and their associated training groups.
This commit is contained in:
428
frontend/package-lock.json
generated
428
frontend/package-lock.json
generated
@@ -23,12 +23,12 @@
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^14.2.1",
|
||||
"vite": "^7.2.2"
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -191,9 +191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -204,13 +204,13 @@
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -221,13 +221,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -238,13 +238,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -255,13 +255,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -272,13 +272,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -289,13 +289,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -306,13 +306,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -323,13 +323,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -340,13 +340,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -357,13 +357,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -374,13 +374,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -391,13 +391,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -408,13 +408,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -425,13 +425,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -442,13 +442,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -459,13 +459,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -476,30 +476,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -510,30 +493,13 @@
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -544,30 +510,13 @@
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -578,13 +527,13 @@
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -595,13 +544,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -612,13 +561,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -629,7 +578,7 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
@@ -1335,16 +1284,16 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
@@ -1993,9 +1942,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -2003,35 +1952,32 @@
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
@@ -3651,54 +3597,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -3773,24 +3671,21 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
@@ -3799,25 +3694,19 @@
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -3838,46 +3727,9 @@
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
|
||||
|
||||
@@ -79,14 +79,14 @@
|
||||
<span class="nav-icon">📊</span>
|
||||
Trainings-Statistik
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/club-settings" class="nav-link" title="Vereinseinstellungen">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vereinseinstellungen
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Organisation</h4>
|
||||
<router-link v-if="isAdmin" to="/club-settings" class="nav-link" title="Vereinseinstellungen">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vereinseinstellungen
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('schedule', 'read')" to="/schedule" class="nav-link" title="Spielpläne">
|
||||
<span class="nav-icon">📅</span>
|
||||
Spielpläne
|
||||
|
||||
565
frontend/src/components/TrainingGroupsTab.vue
Normal file
565
frontend/src/components/TrainingGroupsTab.vue
Normal file
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="training-groups-tab">
|
||||
<div class="groups-section">
|
||||
<div class="section-header">
|
||||
<h3>Gruppen</h3>
|
||||
<button @click="showAddGroupForm = true" class="btn-primary">+ Neue Gruppe</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Form -->
|
||||
<div v-if="showAddGroupForm" class="add-group-form">
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
@keyup.enter="createGroup"
|
||||
class="input-field"
|
||||
/>
|
||||
<button @click="createGroup" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
<div class="groups-list">
|
||||
<div
|
||||
v-for="group in sortedGroups"
|
||||
:key="group.id"
|
||||
class="group-card"
|
||||
:class="{ 'preset-group': group.isPreset }"
|
||||
>
|
||||
<div class="group-header">
|
||||
<h4>{{ group.name }}</h4>
|
||||
<div class="group-actions">
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="editGroup(group)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="deleteGroup(group)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-members">
|
||||
<div class="members-count">
|
||||
{{ group.members ? group.members.length : 0 }} Mitglieder
|
||||
</div>
|
||||
<div class="members-list">
|
||||
<span
|
||||
v-for="member in (group.members || [])"
|
||||
:key="member.id"
|
||||
class="member-tag"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id, member.id)"
|
||||
class="remove-member-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Member to Group -->
|
||||
<div class="add-member-section">
|
||||
<select
|
||||
v-model="selectedMembers[group.id]"
|
||||
class="member-select"
|
||||
@change="addMemberToGroup(group.id, $event.target.value)"
|
||||
:disabled="availableMembersForGroup(group.id).length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? 'Keine Mitglieder verfügbar' : 'Mitglied hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="member in availableMembersForGroup(group.id)"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Group Dialog -->
|
||||
<div v-if="editingGroup" class="edit-group-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Gruppe bearbeiten</h3>
|
||||
<input
|
||||
v-model="editingGroup.name"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
class="input-field"
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveGroupEdit" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info/Confirm Dialogs -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="confirmDialog.resolveCallback && confirmDialog.resolveCallback(true)"
|
||||
@cancel="confirmDialog.resolveCallback && confirmDialog.resolveCallback(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
import InfoDialog from './InfoDialog.vue';
|
||||
import ConfirmDialog from './ConfirmDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingGroupsTab',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'token']),
|
||||
|
||||
sortedGroups() {
|
||||
if (!Array.isArray(this.groups)) {
|
||||
return [];
|
||||
}
|
||||
return [...this.groups].sort((a, b) => {
|
||||
// Preset-Gruppen zuerst
|
||||
if (a.isPreset && !b.isPreset) return -1;
|
||||
if (!a.isPreset && b.isPreset) return 1;
|
||||
// Dann nach sortOrder
|
||||
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
||||
// Dann alphabetisch
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
members: [],
|
||||
showAddGroupForm: false,
|
||||
newGroupName: '',
|
||||
editingGroup: null,
|
||||
selectedMembers: {},
|
||||
infoDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
},
|
||||
confirmDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
resolveCallback: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
await this.loadGroups();
|
||||
await this.loadMembers();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentClub() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
this.loadGroups();
|
||||
this.loadMembers();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
// Stelle sicher, dass es ein Array ist und dass jedes Gruppen-Objekt ein members-Array hat
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.groups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.groups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMembers() {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
|
||||
const members = Array.isArray(response.data) ? response.data : [];
|
||||
// Die API filtert bereits nach active, also müssen wir nicht nochmal filtern
|
||||
const filteredMembers = members.filter(m => m != null);
|
||||
// Sortiere alphabetisch nach Nachname, dann Vorname
|
||||
this.members = filteredMembers.sort((a, b) => {
|
||||
const lastNameA = (a.lastName || '').toLowerCase();
|
||||
const lastNameB = (b.lastName || '').toLowerCase();
|
||||
if (lastNameA !== lastNameB) {
|
||||
return lastNameA.localeCompare(lastNameB);
|
||||
}
|
||||
const firstNameA = (a.firstName || '').toLowerCase();
|
||||
const firstNameB = (b.firstName || '').toLowerCase();
|
||||
return firstNameA.localeCompare(firstNameB);
|
||||
});
|
||||
console.log('[loadMembers] Loaded', this.members.length, 'active members');
|
||||
} catch (error) {
|
||||
console.error('[loadMembers] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Mitglieder');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.members = [];
|
||||
}
|
||||
},
|
||||
|
||||
availableMembersForGroup(groupId) {
|
||||
if (!Array.isArray(this.members) || this.members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
return this.members;
|
||||
}
|
||||
|
||||
const groupMembers = Array.isArray(group.members) ? group.members : [];
|
||||
const memberIdsInGroup = new Set(groupMembers.map(m => m && m.id).filter(id => id != null));
|
||||
return this.members.filter(m => m && m.id && !memberIdsInGroup.has(m.id));
|
||||
},
|
||||
|
||||
async createGroup() {
|
||||
if (!this.newGroupName.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}`, {
|
||||
name: this.newGroupName.trim(),
|
||||
});
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelAddGroup() {
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
},
|
||||
|
||||
editGroup(group) {
|
||||
this.editingGroup = { ...group };
|
||||
},
|
||||
|
||||
async saveGroupEdit() {
|
||||
if (!this.editingGroup.name.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/training-groups/${this.currentClub}/${this.editingGroup.id}`, {
|
||||
name: this.editingGroup.name.trim(),
|
||||
sortOrder: this.editingGroup.sortOrder,
|
||||
});
|
||||
this.editingGroup = null;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelGroupEdit() {
|
||||
this.editingGroup = null;
|
||||
},
|
||||
|
||||
async deleteGroup(group) {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Gruppe löschen',
|
||||
`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`,
|
||||
'Alle Mitglieder-Zuordnungen werden entfernt.',
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${group.id}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId, memberId) {
|
||||
if (!memberId || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
this.selectedMembers[groupId] = '';
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId, memberId) {
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
showInfo(title, message, details, type) {
|
||||
this.infoDialog = { isOpen: true, title, message, details, type };
|
||||
},
|
||||
|
||||
showConfirm(title, message, details, type) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.training-groups-tab {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.add-group-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.groups-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.group-card.preset-group {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-header h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.group-members {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.members-count {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-member-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-member-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.add-member-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.member-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.edit-group-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ import TeamManagementView from './views/TeamManagementView.vue';
|
||||
import PermissionsView from './views/PermissionsView.vue';
|
||||
import LogsView from './views/LogsView.vue';
|
||||
import MemberTransferSettingsView from './views/MemberTransferSettingsView.vue';
|
||||
import TrainingGroupsView from './views/TrainingGroupsView.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -45,6 +46,7 @@ const routes = [
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/member-transfer-settings', component: MemberTransferSettingsView },
|
||||
{ path: '/training-groups', component: TrainingGroupsView },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
<div class="club-settings">
|
||||
<h1>Vereins-Einstellungen</h1>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-navigation">
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'settings' }]"
|
||||
@click="activeTab = 'settings'"
|
||||
>
|
||||
⚙️ Einstellungen
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-groups' }]"
|
||||
@click="activeTab = 'training-groups'"
|
||||
>
|
||||
👨👩👧👦 Trainingsgruppen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div v-if="activeTab === 'settings'">
|
||||
<section class="card">
|
||||
<h2>Begrüßungstext</h2>
|
||||
<div class="greeting-grid">
|
||||
@@ -30,15 +48,29 @@
|
||||
<span v-if="saved" class="saved-hint">Gespeichert</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- End Settings Tab -->
|
||||
|
||||
<!-- Training Groups Tab -->
|
||||
<div v-if="activeTab === 'training-groups'">
|
||||
<TrainingGroupsTab />
|
||||
</div>
|
||||
<!-- End Training Groups Tab -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient';
|
||||
import TrainingGroupsTab from '../components/TrainingGroupsTab.vue';
|
||||
|
||||
export default {
|
||||
name: 'ClubSettings',
|
||||
components: {
|
||||
TrainingGroupsTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'settings',
|
||||
greeting: '',
|
||||
associationMemberNumber: '',
|
||||
saved: false,
|
||||
@@ -102,6 +134,36 @@ export default {
|
||||
.btn.btn-primary:hover { background: var(--primary-hover); }
|
||||
.saved-hint { color: #28a745; font-weight: 600; }
|
||||
.hint { color: #666; font-size: 12px; margin-top: 8px; }
|
||||
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #28a745;
|
||||
border-bottom-color: #28a745;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,46 @@
|
||||
<label class="checkbox-item"><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
<label class="checkbox-item"><span>Mitgliedsformular ausgehändigt:</span> <input type="checkbox" v-model="newMemberFormHandedOver"></label>
|
||||
|
||||
<!-- Trainingsgruppen -->
|
||||
<div class="contact-section" v-if="memberToEdit">
|
||||
<label><span>Trainingsgruppen:</span></label>
|
||||
<div v-if="memberTrainingGroups.length > 0" class="member-groups-list">
|
||||
<span
|
||||
v-for="group in memberTrainingGroups"
|
||||
:key="group.id"
|
||||
class="member-group-tag"
|
||||
>
|
||||
{{ group.name }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id)"
|
||||
class="remove-group-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="no-groups-hint">Keine Gruppen zugeordnet</div>
|
||||
<select
|
||||
v-model="selectedGroupToAdd"
|
||||
class="group-select"
|
||||
@change="addMemberToGroup($event.target.value)"
|
||||
:disabled="availableGroupsForMember.length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableGroupsForMember.length === 0 ? 'Keine Gruppen verfügbar' : 'Gruppe hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="group in availableGroupsForMember"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
>
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label><span>Bild:</span>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="file" accept="image/*" @change="onFileSelected" ref="fileInput" style="display: none;" id="member-image-file">
|
||||
@@ -157,7 +197,6 @@
|
||||
<th>Name, Vorname</th>
|
||||
<th>TTR / QTTR</th>
|
||||
<th>Adresse</th>
|
||||
<th>Mitgliedsformular</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Telefon-Nr.</th>
|
||||
<th>Email-Adresse</th>
|
||||
@@ -199,7 +238,6 @@
|
||||
<span v-else class="no-rating">-</span>
|
||||
</td>
|
||||
<td>{{ member.street }}{{ member.postalCode ? ', ' + member.postalCode : '' }}, {{ member.city }}</td>
|
||||
<td>{{ member.memberFormHandedOver ? '✓' : '' }}</td>
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ getFormattedPhoneNumbers(member) }}</td>
|
||||
<td>{{ getFormattedEmails(member) }}</td>
|
||||
@@ -370,6 +408,14 @@ export default {
|
||||
|
||||
hasTestMembers() {
|
||||
return this.members.some(member => member.testMembership);
|
||||
},
|
||||
|
||||
availableGroupsForMember() {
|
||||
if (!Array.isArray(this.trainingGroups) || this.trainingGroups.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const memberGroupIds = new Set(this.memberTrainingGroups.map(g => g && g.id).filter(id => id != null));
|
||||
return this.trainingGroups.filter(g => g && g.id && !memberGroupIds.has(g.id));
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -425,12 +471,16 @@ export default {
|
||||
showMemberInfo: false,
|
||||
showActivitiesModal: false,
|
||||
selectedMemberForActivities: null,
|
||||
memberTrainingGroups: [],
|
||||
trainingGroups: [],
|
||||
selectedGroupToAdd: '',
|
||||
showTransferDialog: false,
|
||||
selectedAgeGroup: '',
|
||||
selectedGender: '',
|
||||
showTransferDialog: false
|
||||
selectedGender: ''
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadTrainingGroups();
|
||||
await this.init();
|
||||
},
|
||||
methods: {
|
||||
@@ -919,6 +969,9 @@ export default {
|
||||
this.memberContacts.phones = [{ value: member.phone || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
this.memberContacts.emails = [{ value: member.email || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
}
|
||||
|
||||
// Load training groups for this member
|
||||
await this.loadMemberTrainingGroups(member.id);
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/image/${member.id}`, {
|
||||
responseType: 'blob'
|
||||
@@ -947,8 +1000,66 @@ export default {
|
||||
},
|
||||
resetToNewMember() {
|
||||
this.memberToEdit = null;
|
||||
this.memberTrainingGroups = [];
|
||||
this.selectedGroupToAdd = '';
|
||||
this.resetNewMember();
|
||||
},
|
||||
|
||||
async loadTrainingGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.trainingGroups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[loadTrainingGroups] Error:', error);
|
||||
this.trainingGroups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMemberTrainingGroups(memberId) {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}/member/${memberId}`);
|
||||
this.memberTrainingGroups = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (error) {
|
||||
console.error('[loadMemberTrainingGroups] Error:', error);
|
||||
this.memberTrainingGroups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId) {
|
||||
if (!groupId || !this.memberToEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${this.memberToEdit.id}`);
|
||||
this.selectedGroupToAdd = '';
|
||||
await this.loadMemberTrainingGroups(this.memberToEdit.id);
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen zur Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId) {
|
||||
if (!this.memberToEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${this.memberToEdit.id}`);
|
||||
await this.loadMemberTrainingGroups(this.memberToEdit.id);
|
||||
} catch (error) {
|
||||
console.error('[removeMemberFromGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen aus der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadNotes(member) {
|
||||
this.selectedMember = member;
|
||||
const response = await apiClient.get(`/membernotes/${member.id}`, {
|
||||
@@ -1925,6 +2036,52 @@ table td {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.member-groups-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.member-group-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-group-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-group-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.group-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.no-groups-hint {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
569
frontend/src/views/TrainingGroupsView.vue
Normal file
569
frontend/src/views/TrainingGroupsView.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div class="training-groups-view">
|
||||
<h2>Trainingsgruppen</h2>
|
||||
|
||||
<div class="groups-section">
|
||||
<div class="section-header">
|
||||
<h3>Gruppen</h3>
|
||||
<button @click="showAddGroupForm = true" class="btn-primary">+ Neue Gruppe</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Form -->
|
||||
<div v-if="showAddGroupForm" class="add-group-form">
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
@keyup.enter="createGroup"
|
||||
class="input-field"
|
||||
/>
|
||||
<button @click="createGroup" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
<div class="groups-list">
|
||||
<div
|
||||
v-for="group in sortedGroups"
|
||||
:key="group.id"
|
||||
class="group-card"
|
||||
:class="{ 'preset-group': group.isPreset }"
|
||||
>
|
||||
<div class="group-header">
|
||||
<h4>{{ group.name }}</h4>
|
||||
<div class="group-actions">
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="editGroup(group)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="deleteGroup(group)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-members">
|
||||
<div class="members-count">
|
||||
{{ group.members ? group.members.length : 0 }} Mitglieder
|
||||
</div>
|
||||
<div class="members-list">
|
||||
<span
|
||||
v-for="member in (group.members || [])"
|
||||
:key="member.id"
|
||||
class="member-tag"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id, member.id)"
|
||||
class="remove-member-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Member to Group -->
|
||||
<div class="add-member-section">
|
||||
<select
|
||||
v-model="selectedMembers[group.id]"
|
||||
class="member-select"
|
||||
@change="addMemberToGroup(group.id, $event.target.value)"
|
||||
:disabled="availableMembersForGroup(group.id).length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? 'Keine Mitglieder verfügbar' : 'Mitglied hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="member in availableMembersForGroup(group.id)"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Group Dialog -->
|
||||
<div v-if="editingGroup" class="edit-group-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Gruppe bearbeiten</h3>
|
||||
<input
|
||||
v-model="editingGroup.name"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
class="input-field"
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveGroupEdit" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info/Confirm Dialogs -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="confirmDialog.resolveCallback && confirmDialog.resolveCallback(true)"
|
||||
@cancel="confirmDialog.resolveCallback && confirmDialog.resolveCallback(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingGroupsView',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'token']),
|
||||
|
||||
sortedGroups() {
|
||||
if (!Array.isArray(this.groups)) {
|
||||
return [];
|
||||
}
|
||||
return [...this.groups].sort((a, b) => {
|
||||
// Preset-Gruppen zuerst
|
||||
if (a.isPreset && !b.isPreset) return -1;
|
||||
if (!a.isPreset && b.isPreset) return 1;
|
||||
// Dann nach sortOrder
|
||||
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
||||
// Dann alphabetisch
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
members: [],
|
||||
showAddGroupForm: false,
|
||||
newGroupName: '',
|
||||
editingGroup: null,
|
||||
selectedMembers: {},
|
||||
infoDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
},
|
||||
confirmDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
resolveCallback: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
await this.loadGroups();
|
||||
await this.loadMembers();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentClub() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
this.loadGroups();
|
||||
this.loadMembers();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
// Stelle sicher, dass es ein Array ist und dass jedes Gruppen-Objekt ein members-Array hat
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.groups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.groups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMembers() {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
|
||||
const members = Array.isArray(response.data) ? response.data : [];
|
||||
// Die API filtert bereits nach active, also müssen wir nicht nochmal filtern
|
||||
const filteredMembers = members.filter(m => m != null);
|
||||
// Sortiere alphabetisch nach Nachname, dann Vorname
|
||||
this.members = filteredMembers.sort((a, b) => {
|
||||
const lastNameA = (a.lastName || '').toLowerCase();
|
||||
const lastNameB = (b.lastName || '').toLowerCase();
|
||||
if (lastNameA !== lastNameB) {
|
||||
return lastNameA.localeCompare(lastNameB);
|
||||
}
|
||||
const firstNameA = (a.firstName || '').toLowerCase();
|
||||
const firstNameB = (b.firstName || '').toLowerCase();
|
||||
return firstNameA.localeCompare(firstNameB);
|
||||
});
|
||||
console.log('[loadMembers] Loaded', this.members.length, 'active members');
|
||||
} catch (error) {
|
||||
console.error('[loadMembers] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Mitglieder');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.members = [];
|
||||
}
|
||||
},
|
||||
|
||||
availableMembersForGroup(groupId) {
|
||||
if (!Array.isArray(this.members) || this.members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
return this.members;
|
||||
}
|
||||
|
||||
const groupMembers = Array.isArray(group.members) ? group.members : [];
|
||||
const memberIdsInGroup = new Set(groupMembers.map(m => m && m.id).filter(id => id != null));
|
||||
return this.members.filter(m => m && m.id && !memberIdsInGroup.has(m.id));
|
||||
},
|
||||
|
||||
async createGroup() {
|
||||
if (!this.newGroupName.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}`, {
|
||||
name: this.newGroupName.trim(),
|
||||
});
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelAddGroup() {
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
},
|
||||
|
||||
editGroup(group) {
|
||||
this.editingGroup = { ...group };
|
||||
},
|
||||
|
||||
async saveGroupEdit() {
|
||||
if (!this.editingGroup.name.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/training-groups/${this.currentClub}/${this.editingGroup.id}`, {
|
||||
name: this.editingGroup.name.trim(),
|
||||
sortOrder: this.editingGroup.sortOrder,
|
||||
});
|
||||
this.editingGroup = null;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelGroupEdit() {
|
||||
this.editingGroup = null;
|
||||
},
|
||||
|
||||
async deleteGroup(group) {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Gruppe löschen',
|
||||
`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`,
|
||||
'Alle Mitglieder-Zuordnungen werden entfernt.',
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${group.id}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId, memberId) {
|
||||
if (!memberId || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
this.selectedMembers[groupId] = '';
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId, memberId) {
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
showInfo(title, message, details, type) {
|
||||
this.infoDialog = { isOpen: true, title, message, details, type };
|
||||
},
|
||||
|
||||
showConfirm(title, message, details, type) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.training-groups-view {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.add-group-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.groups-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.group-card.preset-group {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-header h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.group-members {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.members-count {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-member-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-member-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.add-member-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.member-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.edit-group-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user