Refactor error handling and localization in frontend components
This commit enhances the error handling and user interface of various frontend components by integrating localization support. It updates error messages and titles across multiple views and dialogs to utilize the translation function, ensuring a consistent user experience in different languages. Additionally, it refines the handling of error messages in the MyTischtennis account and member transfer settings, improving clarity and user feedback during operations.
This commit is contained in:
606
frontend/package-lock.json
generated
606
frontend/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sortablejs": "^1.15.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-multiselect": "^3.0.0",
|
||||
"vue-router": "^4.4.0",
|
||||
"vuex": "^4.1.0"
|
||||
@@ -25,10 +26,10 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^14.2.1",
|
||||
"vite": "^7.2.2"
|
||||
"vite": "^5.4.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -87,9 +88,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"
|
||||
],
|
||||
@@ -100,13 +101,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"
|
||||
],
|
||||
@@ -117,13 +118,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"
|
||||
],
|
||||
@@ -134,13 +135,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"
|
||||
],
|
||||
@@ -151,13 +152,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"
|
||||
],
|
||||
@@ -168,13 +169,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"
|
||||
],
|
||||
@@ -185,13 +186,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"
|
||||
],
|
||||
@@ -202,13 +203,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"
|
||||
],
|
||||
@@ -219,13 +220,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"
|
||||
],
|
||||
@@ -236,13 +237,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"
|
||||
],
|
||||
@@ -253,13 +254,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"
|
||||
],
|
||||
@@ -270,13 +271,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"
|
||||
],
|
||||
@@ -287,13 +288,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"
|
||||
],
|
||||
@@ -304,13 +305,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"
|
||||
],
|
||||
@@ -321,13 +322,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"
|
||||
],
|
||||
@@ -338,13 +339,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"
|
||||
],
|
||||
@@ -355,13 +356,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"
|
||||
],
|
||||
@@ -372,30 +373,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"
|
||||
],
|
||||
@@ -406,30 +390,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"
|
||||
],
|
||||
@@ -440,30 +407,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"
|
||||
],
|
||||
@@ -474,13 +424,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"
|
||||
],
|
||||
@@ -491,13 +441,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"
|
||||
],
|
||||
@@ -508,13 +458,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"
|
||||
],
|
||||
@@ -525,7 +475,7 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -547,19 +497,6 @@
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
@@ -724,6 +661,50 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
|
||||
"integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "9.14.5",
|
||||
"@intlify/shared": "9.14.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
|
||||
"integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.14.5",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
|
||||
"integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
@@ -1980,9 +1961,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",
|
||||
@@ -1990,35 +1971,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": {
|
||||
@@ -2095,43 +2073,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz",
|
||||
"integrity": "sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==",
|
||||
"version": "9.33.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
|
||||
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-utils": "^3.0.0",
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"globals": "^13.24.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"nth-check": "^2.0.1",
|
||||
"postcss-selector-parser": "^6.0.9",
|
||||
"semver": "^7.3.5",
|
||||
"vue-eslint-parser": "^8.0.1"
|
||||
"nth-check": "^2.1.1",
|
||||
"postcss-selector-parser": "^6.0.15",
|
||||
"semver": "^7.6.3",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"xml-name-validator": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
"node": "^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
|
||||
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue/node_modules/eslint-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
|
||||
"node_modules/eslint-plugin-vue/node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mysticatea"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=5"
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
@@ -2152,13 +2129,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
@@ -3237,9 +3217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3404,54 +3384,6 @@
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -3479,6 +3411,19 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -3506,24 +3451,21 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
@@ -3532,25 +3474,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
|
||||
},
|
||||
@@ -3571,46 +3507,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",
|
||||
@@ -3633,22 +3532,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz",
|
||||
"integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==",
|
||||
"version": "9.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
||||
"integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2",
|
||||
"eslint-scope": "^7.0.0",
|
||||
"eslint-visitor-keys": "^3.1.0",
|
||||
"espree": "^9.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.3.1",
|
||||
"esquery": "^1.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.5"
|
||||
"semver": "^7.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
"node": "^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mysticatea"
|
||||
@@ -3674,19 +3573,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/espree": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||
@@ -3705,6 +3591,26 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
|
||||
"integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.14.5",
|
||||
"@intlify/shared": "9.14.5",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-multiselect": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.2.0.tgz",
|
||||
@@ -3789,6 +3695,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sortablejs": "^1.15.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-multiselect": "^3.0.0",
|
||||
"vue-router": "^4.4.0",
|
||||
"vuex": "^4.1.0"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1>
|
||||
<router-link to="/" class="home-link">
|
||||
<img :src="logoUrl" alt="Logo" class="home-logo" width="24" height="24" loading="lazy" />
|
||||
<span>Trainingstagebuch</span>
|
||||
<span>{{ $t('app.name') }}</span>
|
||||
</router-link>
|
||||
</h1>
|
||||
<div v-if="isAuthenticated" class="user-menu">
|
||||
@@ -16,24 +16,29 @@
|
||||
<div v-if="userDropdownOpen" class="user-dropdown">
|
||||
<router-link to="/mytischtennis-account" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">🔗</span>
|
||||
myTischtennis-Account
|
||||
{{ $t('navigation.myTischtennisAccount') }}
|
||||
</router-link>
|
||||
<router-link v-if="canManagePermissions" to="/permissions" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">🔐</span>
|
||||
Berechtigungen
|
||||
{{ $t('navigation.permissions') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('members', 'write')" to="/member-transfer-settings" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">📤</span>
|
||||
Mitgliederübertragung
|
||||
{{ $t('navigation.memberTransfer') }}
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/logs" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">📋</span>
|
||||
System-Logs
|
||||
{{ $t('navigation.logs') }}
|
||||
</router-link>
|
||||
<div class="dropdown-divider"></div>
|
||||
<router-link to="/personal-settings" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">⚙️</span>
|
||||
{{ $t('navigation.personalSettings') }}
|
||||
</router-link>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button @click="logout" class="dropdown-item logout-item">
|
||||
<span class="dropdown-icon">🚪</span>
|
||||
Ausloggen
|
||||
{{ $t('navigation.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,65 +52,65 @@
|
||||
</button>
|
||||
<div class="sidebar-content">
|
||||
<div class="club-selector card">
|
||||
<h3 class="card-title">Verein auswählen</h3>
|
||||
<h3 class="card-title">{{ $t('club.select') }}</h3>
|
||||
<div class="select-group">
|
||||
<select v-model="selectedClub" class="club-select">
|
||||
<option value="">Verein wählen...</option>
|
||||
<option value="new">Neuer Verein</option>
|
||||
<option value="">{{ $t('club.selectPlaceholder') }}</option>
|
||||
<option value="new">{{ $t('club.new') }}</option>
|
||||
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
|
||||
</select>
|
||||
<button @click="loadClub" class="btn-primary" :disabled="!selectedClub">
|
||||
<span>Laden</span>
|
||||
<span>{{ $t('club.load') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav v-if="selectedClub" class="nav-menu">
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Tagesgeschäft</h4>
|
||||
<h4 class="nav-title">{{ $t('navigation.dailyBusiness') }}</h4>
|
||||
<router-link v-if="hasPermission('members', 'read')" to="/members" class="nav-link" title="Mitglieder">
|
||||
<span class="nav-icon">👥</span>
|
||||
Mitglieder
|
||||
{{ $t('navigation.members') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('diary', 'read')" to="/diary" class="nav-link" title="Tagebuch">
|
||||
<span class="nav-icon">📝</span>
|
||||
Tagebuch
|
||||
{{ $t('navigation.diary') }}
|
||||
</router-link>
|
||||
<router-link v-if="canManageApprovals" to="/pending-approvals" class="nav-link" title="Freigaben">
|
||||
<span class="nav-icon">⏳</span>
|
||||
Freigaben
|
||||
{{ $t('navigation.approvals') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('statistics', 'read')" to="/training-stats" class="nav-link" title="Trainings-Statistik">
|
||||
<span class="nav-icon">📊</span>
|
||||
Trainings-Statistik
|
||||
{{ $t('navigation.statistics') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Wettbewerbe</h4>
|
||||
<h4 class="nav-title">{{ $t('navigation.competitions') }}</h4>
|
||||
<router-link v-if="hasPermission('tournaments', 'read')" to="/tournaments" class="nav-link" title="Turniere">
|
||||
<span class="nav-icon">🏆</span>
|
||||
Turniere
|
||||
{{ $t('navigation.tournaments') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('schedule', 'read')" to="/schedule" class="nav-link" title="Spielpläne">
|
||||
<span class="nav-icon">📅</span>
|
||||
Spielpläne
|
||||
{{ $t('navigation.schedule') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Einstellungen</h4>
|
||||
<h4 class="nav-title">{{ $t('navigation.settings') }}</h4>
|
||||
<router-link v-if="isAdmin" to="/club-settings" class="nav-link" title="Vereinseinstellungen">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vereinseinstellungen
|
||||
{{ $t('navigation.clubSettings') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('predefined_activities', 'read')" to="/predefined-activities" class="nav-link" title="Vordefinierte Aktivitäten">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vordefinierte Aktivitäten
|
||||
{{ $t('navigation.predefinedActivities') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('teams', 'read')" to="/team-management" class="nav-link" title="Team-Verwaltung">
|
||||
<span class="nav-icon">👥</span>
|
||||
Team-Verwaltung
|
||||
{{ $t('navigation.teamManagement') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -115,8 +120,8 @@
|
||||
|
||||
<div v-else class="auth-nav">
|
||||
<div class="auth-links">
|
||||
<router-link to="/login" class="btn-primary">Einloggen</router-link>
|
||||
<router-link to="/register" class="btn-secondary">Registrieren</router-link>
|
||||
<router-link to="/login" class="btn-primary">{{ $t('navigation.login') }}</router-link>
|
||||
<router-link to="/register" class="btn-secondary">{{ $t('navigation.register') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +204,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'sidebarCollapsed', 'username', 'hasPermission', 'isClubOwner', 'userRole']),
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'sidebarCollapsed', 'username', 'hasPermission', 'isClubOwner', 'userRole', 'language']),
|
||||
canManageApprovals() {
|
||||
// Nur anzeigen, wenn Permissions geladen sind UND Berechtigung vorhanden
|
||||
if (!this.currentClub) return false;
|
||||
@@ -319,7 +324,7 @@ export default {
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
this.showInfo('Hinweis', 'Deine Sitzung ist abgelaufen. Du wirst abgemeldet.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.warning'), this.$t('auth.sessionExpired'), '', 'warning');
|
||||
this.logout();
|
||||
clearInterval(this.sessionInterval);
|
||||
this.$router.push('/login');
|
||||
@@ -329,6 +334,11 @@ export default {
|
||||
// Click-outside handler für User-Dropdown
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
|
||||
// Synchronisiere Sprache aus Store mit i18n
|
||||
if (this.language) {
|
||||
this.$i18n.locale.value = this.language;
|
||||
}
|
||||
|
||||
// Nur Daten laden, wenn der Benutzer authentifiziert ist
|
||||
if (this.isAuthenticated) {
|
||||
try {
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Unfall melden"
|
||||
:title="$t('accident.reportAccident')"
|
||||
size="medium"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit" class="accident-form">
|
||||
<div class="form-group">
|
||||
<label for="memberId">Mitglied:</label>
|
||||
<label for="memberId">{{ $t('accident.member') }}:</label>
|
||||
<select id="memberId" v-model="localAccident.memberId" class="form-select">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="">{{ $t('accident.pleaseSelect') }}</option>
|
||||
<option
|
||||
v-for="member in availableMembers"
|
||||
:key="member.id"
|
||||
@@ -21,18 +21,18 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accident">Unfall:</label>
|
||||
<label for="accident">{{ $t('accident.accident') }}:</label>
|
||||
<textarea
|
||||
id="accident"
|
||||
v-model="localAccident.accident"
|
||||
required
|
||||
class="form-textarea"
|
||||
placeholder="Beschreibung des Unfalls..."
|
||||
:placeholder="$t('accident.accidentDescription')"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="accidents.length > 0" class="accidents-list">
|
||||
<h4>Gemeldete Unfälle</h4>
|
||||
<h4>{{ $t('accident.reportedAccidents') }}</h4>
|
||||
<ul>
|
||||
<li v-for="accident in accidents" :key="accident.id" class="accident-item">
|
||||
<strong>{{ accident.firstName }} {{ accident.lastName }}:</strong> {{ accident.accident }}
|
||||
@@ -42,8 +42,8 @@
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="button" @click="handleClose" class="btn-secondary">Schließen</button>
|
||||
<button type="button" @click="handleSubmit" class="btn-primary" :disabled="!isValid">Eintragen</button>
|
||||
<button type="button" @click="handleClose" class="btn-secondary">{{ $t('accident.close') }}</button>
|
||||
<button type="button" @click="handleSubmit" class="btn-primary" :disabled="!isValid">{{ $t('accident.submit') }}</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
v-if="minimizable"
|
||||
@click="$emit('minimize')"
|
||||
class="control-btn minimize-btn"
|
||||
title="Minimieren"
|
||||
:title="$t('baseDialog.minimize')"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
@@ -30,7 +30,7 @@
|
||||
v-if="closable"
|
||||
@click="handleClose"
|
||||
class="control-btn close-btn"
|
||||
title="Schließen"
|
||||
:title="$t('baseDialog.close')"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="title"
|
||||
:title="computedTitle"
|
||||
:is-modal="true"
|
||||
size="small"
|
||||
:closable="true"
|
||||
@@ -29,13 +29,13 @@
|
||||
@click="handleCancel"
|
||||
class="btn-secondary"
|
||||
>
|
||||
{{ cancelText }}
|
||||
{{ computedCancelText }}
|
||||
</button>
|
||||
<button
|
||||
@click="handleConfirm"
|
||||
:class="confirmButtonClass"
|
||||
>
|
||||
{{ confirmText }}
|
||||
{{ computedConfirmText }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Bestätigung'
|
||||
default: ''
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
@@ -73,11 +73,11 @@ export default {
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: 'OK'
|
||||
default: ''
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: 'Abbrechen'
|
||||
default: ''
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
@@ -85,6 +85,15 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedTitle() {
|
||||
return this.title || this.$t('dialogs.confirm.title');
|
||||
},
|
||||
computedConfirmText() {
|
||||
return this.confirmText || this.$t('dialogs.confirm.ok');
|
||||
},
|
||||
computedCancelText() {
|
||||
return this.cancelText || this.$t('dialogs.confirm.cancel');
|
||||
},
|
||||
icon() {
|
||||
const icons = {
|
||||
info: 'ℹ️',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Tischtennis-Übung konfigurieren"
|
||||
:title="$t('courtDrawing.title')"
|
||||
size="large"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
@@ -16,13 +16,13 @@
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Abbrechen</button>
|
||||
<button class="btn-secondary" @click="handleClose">{{ $t('courtDrawing.cancel') }}</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleOk"
|
||||
:disabled="!isValid"
|
||||
>
|
||||
OK
|
||||
{{ $t('courtDrawing.ok') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@click="startAnimation"
|
||||
:disabled="isAnimating"
|
||||
>
|
||||
{{ isAnimating ? 'Animation läuft...' : 'Animation starten' }}
|
||||
{{ isAnimating ? $t('courtDrawingRender.animationRunning') : $t('courtDrawingRender.startAnimation') }}
|
||||
</button>
|
||||
</div>
|
||||
<canvas
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="court-drawing-tool">
|
||||
<div class="tool-header">
|
||||
<h4>Tischtennis-Übungszeichnung</h4>
|
||||
<h4>{{ $t('courtDrawingTool.title') }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
<!-- Startposition und Schlagart Auswahl -->
|
||||
<div class="exercise-selection" v-if="selectedStartPosition">
|
||||
<h5>Übung konfigurieren</h5>
|
||||
<h5>{{ $t('courtDrawingTool.configureExercise') }}</h5>
|
||||
<div class="selection-group">
|
||||
<!-- Schlagart Auswahl -->
|
||||
<div class="stroke-selection">
|
||||
<div>
|
||||
<span class="group-label">Aufschlag:</span>
|
||||
<span class="group-label">{{ $t('courtDrawingTool.service') }}</span>
|
||||
<div class="stroke-buttons">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'VH', 'btn-secondary': strokeType !== 'VH' }]"
|
||||
@click="strokeType = 'VH'"
|
||||
title="Vorhand"
|
||||
:title="$t('courtDrawingTool.forehand')"
|
||||
>
|
||||
VH
|
||||
</button>
|
||||
@@ -37,7 +37,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'RH', 'btn-secondary': strokeType !== 'RH' }]"
|
||||
@click="strokeType = 'RH'"
|
||||
title="Rückhand"
|
||||
:title="$t('courtDrawingTool.backhand')"
|
||||
>
|
||||
RH
|
||||
</button>
|
||||
@@ -46,13 +46,13 @@
|
||||
|
||||
<!-- Schnittoption Auswahl -->
|
||||
<div class="spin-selection">
|
||||
<span class="group-label">Schnitt:</span>
|
||||
<span class="group-label">{{ $t('courtDrawingTool.spin') }}</span>
|
||||
<div class="spin-buttons">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Unterschnitt', 'btn-secondary': spinType !== 'Unterschnitt' }]"
|
||||
@click="spinType = 'Unterschnitt'"
|
||||
title="Unterschnitt"
|
||||
:title="$t('courtDrawingTool.underspin')"
|
||||
>
|
||||
US
|
||||
</button>
|
||||
@@ -60,7 +60,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Überschnitt', 'btn-secondary': spinType !== 'Überschnitt' }]"
|
||||
@click="spinType = 'Überschnitt'"
|
||||
title="Überschnitt"
|
||||
:title="$t('courtDrawingTool.topspin')"
|
||||
>
|
||||
OS
|
||||
</button>
|
||||
@@ -68,7 +68,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitschnitt', 'btn-secondary': spinType !== 'Seitschnitt' }]"
|
||||
@click="spinType = 'Seitschnitt'"
|
||||
title="Seitschnitt"
|
||||
:title="$t('courtDrawingTool.sidespin')"
|
||||
>
|
||||
SR
|
||||
</button>
|
||||
@@ -76,7 +76,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitunterschnitt', 'btn-secondary': spinType !== 'Seitunterschnitt' }]"
|
||||
@click="spinType = 'Seitunterschnitt'"
|
||||
title="Seitunterschnitt"
|
||||
:title="$t('courtDrawingTool.sideUnderspin')"
|
||||
>
|
||||
SU
|
||||
</button>
|
||||
@@ -84,7 +84,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Gegenläufer', 'btn-secondary': spinType !== 'Gegenläufer' }]"
|
||||
@click="spinType = 'Gegenläufer'"
|
||||
title="Gegenläufer"
|
||||
:title="$t('courtDrawingTool.counterSpin')"
|
||||
>
|
||||
GL
|
||||
</button>
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<!-- Zielposition für Hauptschlag: explizite Auswahl 1–9 als Buttons -->
|
||||
<div class="target-selection" v-if="spinType">
|
||||
<span class="group-label">Zielposition:</span>
|
||||
<span class="group-label">{{ $t('courtDrawingTool.targetPosition') }}</span>
|
||||
<div class="target-grid">
|
||||
<button
|
||||
v-for="n in mainTargetPositions"
|
||||
@@ -119,7 +119,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'VH', 'btn-secondary': nextStrokeSide !== 'VH' }]"
|
||||
@click="nextStrokeSide = 'VH'"
|
||||
title="Vorhand"
|
||||
:title="$t('courtDrawingTool.forehand')"
|
||||
>
|
||||
VH
|
||||
</button>
|
||||
@@ -127,7 +127,7 @@
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'RH', 'btn-secondary': nextStrokeSide !== 'RH' }]"
|
||||
@click="nextStrokeSide = 'RH'"
|
||||
title="Rückhand"
|
||||
:title="$t('courtDrawingTool.backhand')"
|
||||
>
|
||||
RH
|
||||
</button>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Spielplan importieren"
|
||||
:title="$t('csvImport.title')"
|
||||
size="small"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit" class="import-form">
|
||||
<div class="form-group">
|
||||
<label for="csvFile">CSV-Datei hochladen:</label>
|
||||
<label for="csvFile">{{ $t('csvImport.uploadCsvFile') }}:</label>
|
||||
<input
|
||||
type="file"
|
||||
id="csvFile"
|
||||
@@ -27,8 +27,8 @@
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button @click="handleClose" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="handleSubmit" class="btn-primary" :disabled="!selectedFile">Importieren</button>
|
||||
<button @click="handleClose" class="btn-secondary">{{ $t('csvImport.cancel') }}</button>
|
||||
<button @click="handleSubmit" class="btn-primary" :disabled="!selectedFile">{{ $t('csvImport.import') }}</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
<template>
|
||||
<div class="dialog-examples">
|
||||
<h2>Dialog-Beispiele</h2>
|
||||
<h2>{{ $t('dialogExamples.title') }}</h2>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>Modale Dialoge</h3>
|
||||
<h3>{{ $t('dialogExamples.modalDialogs') }}</h3>
|
||||
<div class="button-group">
|
||||
<button @click="openSimpleModal" class="btn-primary">Einfacher Modal</button>
|
||||
<button @click="openLargeModal" class="btn-primary">Großer Modal</button>
|
||||
<button @click="openFullscreenModal" class="btn-primary">Fullscreen Modal</button>
|
||||
<button @click="openSimpleModal" class="btn-primary">{{ $t('dialogExamples.simpleModal') }}</button>
|
||||
<button @click="openLargeModal" class="btn-primary">{{ $t('dialogExamples.largeModal') }}</button>
|
||||
<button @click="openFullscreenModal" class="btn-primary">{{ $t('dialogExamples.fullscreenModal') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>Nicht-modale Dialoge</h3>
|
||||
<h3>{{ $t('dialogExamples.nonModalDialogs') }}</h3>
|
||||
<div class="button-group">
|
||||
<button @click="openNonModalDialog" class="btn-primary">Nicht-modaler Dialog</button>
|
||||
<button @click="openMultipleDialogs" class="btn-primary">Mehrere Dialoge</button>
|
||||
<button @click="openNonModalDialog" class="btn-primary">{{ $t('dialogExamples.nonModalDialog') }}</button>
|
||||
<button @click="openMultipleDialogs" class="btn-primary">{{ $t('dialogExamples.multipleDialogs') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>Informations-Dialoge</h3>
|
||||
<h3>{{ $t('dialogExamples.infoDialogs') }}</h3>
|
||||
<div class="button-group">
|
||||
<button @click="showInfoDialog" class="btn-primary">Info</button>
|
||||
<button @click="showSuccessDialog" class="btn-success">Erfolg</button>
|
||||
<button @click="showWarningDialog" class="btn-warning">Warnung</button>
|
||||
<button @click="showErrorDialog" class="btn-danger">Fehler</button>
|
||||
<button @click="showInfoDialog" class="btn-primary">{{ $t('dialogExamples.info') }}</button>
|
||||
<button @click="showSuccessDialog" class="btn-success">{{ $t('dialogExamples.success') }}</button>
|
||||
<button @click="showWarningDialog" class="btn-warning">{{ $t('dialogExamples.warning') }}</button>
|
||||
<button @click="showErrorDialog" class="btn-danger">{{ $t('dialogExamples.error') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>Bestätigungs-Dialoge</h3>
|
||||
<h3>{{ $t('dialogExamples.confirmDialogs') }}</h3>
|
||||
<div class="button-group">
|
||||
<button @click="showInfoConfirm" class="btn-primary">Info</button>
|
||||
<button @click="showWarningConfirm" class="btn-warning">Warnung</button>
|
||||
<button @click="showDangerConfirm" class="btn-danger">Löschen</button>
|
||||
<button @click="showInfoConfirm" class="btn-primary">{{ $t('dialogExamples.info') }}</button>
|
||||
<button @click="showWarningConfirm" class="btn-warning">{{ $t('dialogExamples.warning') }}</button>
|
||||
<button @click="showDangerConfirm" class="btn-danger">{{ $t('common.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>Composable-Verwendung</h3>
|
||||
<h3>{{ $t('dialogExamples.composableUsage') }}</h3>
|
||||
<div class="button-group">
|
||||
<button @click="composableDialog.open()" class="btn-primary">useDialog Beispiel</button>
|
||||
<button @click="showComposableConfirm" class="btn-primary">useConfirm Beispiel</button>
|
||||
<button @click="composableDialog.open()" class="btn-primary">{{ $t('dialogExamples.useDialogExample') }}</button>
|
||||
<button @click="showComposableConfirm" class="btn-primary">{{ $t('dialogExamples.useConfirmExample') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Einfacher Modal Dialog -->
|
||||
<BaseDialog
|
||||
v-model="simpleModal.isOpen"
|
||||
title="Einfacher Modal Dialog"
|
||||
:title="$t('dialogExamples.simpleModalTitle')"
|
||||
size="medium"
|
||||
>
|
||||
<p>Dies ist ein einfacher modaler Dialog mit mittlerer Größe.</p>
|
||||
<p>Klicken Sie außerhalb oder auf das X, um zu schließen.</p>
|
||||
<p>{{ $t('dialogExamples.simpleModalText') }}</p>
|
||||
<p>{{ $t('dialogExamples.simpleModalText2') }}</p>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Großer Modal Dialog -->
|
||||
<BaseDialog
|
||||
v-model="largeModal.isOpen"
|
||||
title="Großer Modal Dialog"
|
||||
:title="$t('dialogExamples.largeModalTitle')"
|
||||
size="large"
|
||||
>
|
||||
<p>Dies ist ein großer modaler Dialog.</p>
|
||||
<p>Er bietet mehr Platz für Inhalte.</p>
|
||||
<p>{{ $t('dialogExamples.largeModalText') }}</p>
|
||||
<p>{{ $t('dialogExamples.largeModalText2') }}</p>
|
||||
<div style="height: 400px; background: #f5f5f5; margin-top: 1rem; padding: 1rem;">
|
||||
Scroll-Bereich für viel Inhalt...
|
||||
{{ $t('dialogExamples.scrollArea') }}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Fullscreen Modal Dialog -->
|
||||
<BaseDialog
|
||||
v-model="fullscreenModal.isOpen"
|
||||
title="Fullscreen Modal Dialog"
|
||||
:title="$t('dialogExamples.fullscreenModalTitle')"
|
||||
size="fullscreen"
|
||||
>
|
||||
<p>Dies ist ein Fullscreen-Dialog.</p>
|
||||
<p>Er nimmt fast den gesamten Bildschirm ein (90vw x 90vh).</p>
|
||||
<p>{{ $t('dialogExamples.fullscreenModalText') }}</p>
|
||||
<p>{{ $t('dialogExamples.fullscreenModalText2') }}</p>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Nicht-modaler Dialog -->
|
||||
<BaseDialog
|
||||
v-model="nonModal.isOpen"
|
||||
title="Nicht-modaler Dialog"
|
||||
:title="$t('dialogExamples.nonModalTitle')"
|
||||
:is-modal="false"
|
||||
:position="nonModal.position"
|
||||
@update:position="nonModal.position = $event"
|
||||
@@ -91,17 +91,17 @@
|
||||
:minimizable="true"
|
||||
@minimize="handleMinimize('nonModal')"
|
||||
>
|
||||
<p>Dies ist ein nicht-modaler Dialog.</p>
|
||||
<p>Sie können ihn verschieben und mehrere gleichzeitig öffnen!</p>
|
||||
<p>{{ $t('dialogExamples.nonModalText') }}</p>
|
||||
<p>{{ $t('dialogExamples.nonModalText2') }}</p>
|
||||
<template #footer>
|
||||
<button @click="nonModal.isOpen = false" class="btn-secondary">Schließen</button>
|
||||
<button @click="nonModal.isOpen = false" class="btn-secondary">{{ $t('common.close') }}</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Zweiter nicht-modaler Dialog -->
|
||||
<BaseDialog
|
||||
v-model="nonModal2.isOpen"
|
||||
title="Zweiter nicht-modaler Dialog"
|
||||
:title="$t('dialogExamples.secondNonModalTitle')"
|
||||
:is-modal="false"
|
||||
:position="nonModal2.position"
|
||||
@update:position="nonModal2.position = $event"
|
||||
@@ -109,41 +109,41 @@
|
||||
:draggable="true"
|
||||
:z-index="1001"
|
||||
>
|
||||
<p>Noch ein nicht-modaler Dialog!</p>
|
||||
<p>{{ $t('dialogExamples.secondNonModalText') }}</p>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Informations-Dialoge -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
title="Information"
|
||||
message="Dies ist eine Informationsmeldung."
|
||||
:title="$t('dialogExamples.information')"
|
||||
:message="$t('dialogExamples.infoMessage')"
|
||||
type="info"
|
||||
@ok="infoDialog.isOpen = false"
|
||||
/>
|
||||
|
||||
<InfoDialog
|
||||
v-model="successDialog.isOpen"
|
||||
title="Erfolg"
|
||||
message="Der Vorgang wurde erfolgreich abgeschlossen!"
|
||||
details="Alle Änderungen wurden gespeichert."
|
||||
:title="$t('dialogExamples.success')"
|
||||
:message="$t('dialogExamples.successMessage')"
|
||||
:details="$t('dialogExamples.successDetails')"
|
||||
type="success"
|
||||
@ok="successDialog.isOpen = false"
|
||||
/>
|
||||
|
||||
<InfoDialog
|
||||
v-model="warningDialog.isOpen"
|
||||
title="Warnung"
|
||||
message="Bitte beachten Sie folgende Hinweise."
|
||||
details="Einige Felder sind möglicherweise nicht vollständig ausgefüllt."
|
||||
:title="$t('dialogExamples.warning')"
|
||||
:message="$t('dialogExamples.warningMessage')"
|
||||
:details="$t('dialogExamples.warningDetails')"
|
||||
type="warning"
|
||||
@ok="warningDialog.isOpen = false"
|
||||
/>
|
||||
|
||||
<InfoDialog
|
||||
v-model="errorDialog.isOpen"
|
||||
title="Fehler"
|
||||
message="Ein Fehler ist aufgetreten."
|
||||
details="Bitte versuchen Sie es später erneut."
|
||||
:title="$t('dialogExamples.error')"
|
||||
:message="$t('dialogExamples.errorMessage')"
|
||||
:details="$t('dialogExamples.errorDetails')"
|
||||
type="error"
|
||||
@ok="errorDialog.isOpen = false"
|
||||
/>
|
||||
@@ -151,8 +151,8 @@
|
||||
<!-- Bestätigungs-Dialoge -->
|
||||
<ConfirmDialog
|
||||
v-model="infoConfirm.isOpen"
|
||||
title="Information"
|
||||
message="Dies ist eine Informationsmeldung."
|
||||
:title="$t('dialogExamples.information')"
|
||||
:message="$t('dialogExamples.infoMessage')"
|
||||
type="info"
|
||||
:show-cancel="false"
|
||||
@confirm="infoConfirm.isOpen = false"
|
||||
@@ -160,9 +160,9 @@
|
||||
|
||||
<ConfirmDialog
|
||||
v-model="warningConfirm.isOpen"
|
||||
title="Warnung"
|
||||
message="Sind Sie sicher, dass Sie fortfahren möchten?"
|
||||
details="Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
:title="$t('dialogExamples.warning')"
|
||||
:message="$t('dialogExamples.confirmWarningMessage')"
|
||||
:details="$t('dialogExamples.confirmWarningDetails')"
|
||||
type="warning"
|
||||
@confirm="handleWarningConfirm"
|
||||
@cancel="warningConfirm.isOpen = false"
|
||||
@@ -170,10 +170,10 @@
|
||||
|
||||
<ConfirmDialog
|
||||
v-model="dangerConfirm.isOpen"
|
||||
title="Löschen bestätigen"
|
||||
message="Möchten Sie diesen Eintrag wirklich löschen?"
|
||||
:title="$t('dialogExamples.confirmDeleteTitle')"
|
||||
:message="$t('dialogExamples.confirmDeleteMessage')"
|
||||
type="danger"
|
||||
confirm-text="Löschen"
|
||||
:confirm-text="$t('common.delete')"
|
||||
@confirm="handleDelete"
|
||||
@cancel="dangerConfirm.isOpen = false"
|
||||
/>
|
||||
@@ -181,13 +181,13 @@
|
||||
<!-- Composable Dialog -->
|
||||
<BaseDialog
|
||||
v-model="composableDialog.isOpen"
|
||||
title="Dialog mit useDialog Composable"
|
||||
:title="$t('dialogExamples.composableDialogTitle')"
|
||||
size="medium"
|
||||
>
|
||||
<p>Dieser Dialog verwendet das useDialog Composable.</p>
|
||||
<p>Das macht die Verwaltung einfacher!</p>
|
||||
<p>{{ $t('dialogExamples.composableDialogText') }}</p>
|
||||
<p>{{ $t('dialogExamples.composableDialogText2') }}</p>
|
||||
<template #footer>
|
||||
<button @click="composableDialog.close()" class="btn-primary">Schließen</button>
|
||||
<button @click="composableDialog.close()" class="btn-primary">{{ $t('common.close') }}</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
|
||||
<!-- Minimierte Dialoge Anzeige -->
|
||||
<div v-if="minimizedDialogs.length > 0" class="minimized-section">
|
||||
<h4>Minimierte Dialoge:</h4>
|
||||
<h4>{{ $t('dialogExamples.minimizedDialogs') }}</h4>
|
||||
<button
|
||||
v-for="(dialog, index) in minimizedDialogs"
|
||||
:key="index"
|
||||
@@ -319,9 +319,9 @@ export default {
|
||||
|
||||
async showComposableConfirm() {
|
||||
const result = await this.confirmComposable.confirm({
|
||||
title: 'useConfirm Beispiel',
|
||||
message: 'Möchten Sie fortfahren?',
|
||||
details: 'Dies ist ein Beispiel für das useConfirm Composable.',
|
||||
title: this.$t('dialogExamples.composableConfirmTitle'),
|
||||
message: this.$t('dialogExamples.composableConfirmMessage'),
|
||||
details: this.$t('dialogExamples.composableConfirmDetails'),
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{{ dialog.title }}
|
||||
</button>
|
||||
<div v-if="minimizedDialogs.length === 0" class="no-minimized-dialogs">
|
||||
Keine minimierten Dialoge
|
||||
{{ $t('dialogManager.noMinimizedDialogs') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-controls">
|
||||
<button @click="minimizeDialog(dialog.id)" class="control-btn minimize-btn" title="Minimieren">−</button>
|
||||
<button @click="closeDialog(dialog.id)" class="control-btn close-btn" title="Schließen">×</button>
|
||||
<button @click="minimizeDialog(dialog.id)" class="control-btn minimize-btn" :title="$t('dialogManager.minimize')">−</button>
|
||||
<button @click="closeDialog(dialog.id)" class="control-btn close-btn" :title="$t('dialogManager.close')">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="title"
|
||||
:title="computedTitle"
|
||||
:is-modal="true"
|
||||
size="large"
|
||||
:closable="true"
|
||||
@@ -18,7 +18,7 @@
|
||||
class="dialog-image"
|
||||
/>
|
||||
<div v-else class="no-image">
|
||||
Kein Bild verfügbar
|
||||
{{ $t('imageDialog.noImageAvailable') }}
|
||||
</div>
|
||||
|
||||
<!-- Optionale zusätzliche Inhalte -->
|
||||
@@ -31,7 +31,7 @@
|
||||
<template #footer>
|
||||
<slot name="actions">
|
||||
<button @click="handleClose" class="btn-secondary">
|
||||
Schließen
|
||||
{{ $t('imageDialog.close') }}
|
||||
</button>
|
||||
</slot>
|
||||
</template>
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Bild'
|
||||
default: ''
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
@@ -61,6 +61,11 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close'],
|
||||
computed: {
|
||||
computedTitle() {
|
||||
return this.title || this.$t('imageDialog.title');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
v-if="hasImages && images.length > 1"
|
||||
class="nav-button nav-button--prev"
|
||||
@click="showPreviousImage"
|
||||
title="Vorheriges Bild"
|
||||
:title="$t('imageViewer.previousImage')"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@@ -29,7 +29,7 @@
|
||||
:style="imageStyle"
|
||||
/>
|
||||
<div v-else class="no-image">
|
||||
Kein Bild verfügbar
|
||||
{{ $t('imageViewer.noImageAvailable') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
v-if="hasImages && images.length > 1"
|
||||
class="nav-button nav-button--next"
|
||||
@click="showNextImage"
|
||||
title="Nächstes Bild"
|
||||
:title="$t('imageViewer.nextImage')"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
@@ -48,33 +48,33 @@
|
||||
v-if="allowRotate && currentImageId"
|
||||
@click="rotate('left')"
|
||||
class="action-btn"
|
||||
title="90° links drehen"
|
||||
:title="$t('imageViewer.rotateLeft')"
|
||||
>
|
||||
↺ Links drehen
|
||||
↺ {{ $t('imageViewer.rotateLeft') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="allowRotate && currentImageId"
|
||||
@click="rotate('right')"
|
||||
class="action-btn"
|
||||
title="90° rechts drehen"
|
||||
:title="$t('imageViewer.rotateRight')"
|
||||
>
|
||||
↻ Rechts drehen
|
||||
↻ {{ $t('imageViewer.rotateRight') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showSetPrimary && currentImage && !currentImage.isPrimary"
|
||||
@click="setPrimary"
|
||||
class="action-btn"
|
||||
title="Als Hauptbild festlegen"
|
||||
:title="$t('imageViewer.setAsPrimary')"
|
||||
>
|
||||
⭐ Als Hauptbild setzen
|
||||
⭐ {{ $t('imageViewer.setAsPrimaryButton') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showDelete && currentImageId"
|
||||
@click="deleteImage"
|
||||
class="action-btn action-btn--danger"
|
||||
title="Bild löschen"
|
||||
:title="$t('imageViewer.deleteImage')"
|
||||
>
|
||||
🗑️ Löschen
|
||||
🗑️ {{ $t('imageViewer.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -82,11 +82,11 @@
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="file" multiple accept="image/*" @change="handleFileSelect" ref="fileInput" style="display: none;" id="image-upload-file">
|
||||
<label for="image-upload-file" class="upload-label" style="cursor: pointer; padding: 8px 15px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5; display: inline-block;">
|
||||
📁 Dateien auswählen
|
||||
📁 {{ $t('imageViewer.selectFiles') }}
|
||||
</label>
|
||||
<input type="file" multiple accept="image/*" capture="environment" @change="handleFileSelect" ref="cameraInput" style="display: none;" id="image-upload-camera">
|
||||
<label for="image-upload-camera" class="upload-label" style="cursor: pointer; padding: 8px 15px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5; display: inline-block;">
|
||||
📷 Kamera
|
||||
📷 {{ $t('imageViewer.camera') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="title"
|
||||
:title="computedTitle"
|
||||
:is-modal="true"
|
||||
:size="size"
|
||||
:closable="true"
|
||||
@@ -31,7 +31,7 @@
|
||||
@click="handleOk"
|
||||
:class="buttonClass"
|
||||
>
|
||||
{{ okText }}
|
||||
{{ computedOkText }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Information'
|
||||
default: ''
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
},
|
||||
okText: {
|
||||
type: String,
|
||||
default: 'OK'
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
@@ -85,6 +85,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedTitle() {
|
||||
return this.title || this.$t('dialogs.info.title');
|
||||
},
|
||||
computedOkText() {
|
||||
return this.okText || this.$t('dialogs.info.ok');
|
||||
},
|
||||
computedIcon() {
|
||||
// Wenn ein eigenes Icon übergeben wurde
|
||||
if (typeof this.icon === 'string') {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="report-content">
|
||||
<div v-if="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Lade Spielberichtsdaten...</p>
|
||||
<p>{{ $t('matchReportApi.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error">
|
||||
<h3>❌ Fehler beim Laden der Daten</h3>
|
||||
<h3>❌ {{ $t('matchReportApi.errorLoading') }}</h3>
|
||||
<p>{{ error }}</p>
|
||||
<button @click="loadData" class="retry-btn">🔄 Erneut versuchen</button>
|
||||
<button @click="loadData" class="retry-btn">🔄 {{ $t('matchReportApi.retry') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="meetingData" class="meeting-info">
|
||||
@@ -24,41 +24,41 @@
|
||||
<div class="section-grid">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'general' }" @click="setActiveSection('general')">
|
||||
<div class="section-icon">ℹ️</div>
|
||||
<span>Allgemein</span>
|
||||
<span>{{ $t('matchReportApi.general') }}</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'homeLineup', certified: isHomeLineupCertified }"
|
||||
@click="setActiveSection('homeLineup')">
|
||||
<div class="section-icon">👥</div>
|
||||
<span>Aufstellung Heim</span>
|
||||
<span>{{ $t('matchReportApi.homeLineup') }}</span>
|
||||
<span v-if="isHomeLineupCertified" class="certified-badge">✓</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'guestLineup', certified: isGuestLineupCertified }"
|
||||
@click="setActiveSection('guestLineup')">
|
||||
<div class="section-icon">👥</div>
|
||||
<span>Aufstellung Gast</span>
|
||||
<span>{{ $t('matchReportApi.guestLineup') }}</span>
|
||||
<span v-if="isGuestLineupCertified" class="certified-badge">✓</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'greeting', completed: isGreetingCompleted, disabled: !canOpenNextStages }"
|
||||
@click="setActiveSection('greeting')" :disabled="!canOpenNextStages">
|
||||
<div class="section-icon">👋</div>
|
||||
<span>Begrüßung</span>
|
||||
<span>{{ $t('matchReportApi.greeting') }}</span>
|
||||
<span v-if="isGreetingCompleted" class="completed-badge">✅</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'result', disabled: !canOpenNextStages }"
|
||||
@click="setActiveSection('result')" :disabled="!canOpenNextStages">
|
||||
<div class="section-icon">⚽</div>
|
||||
<span>Ergebniserfassung</span>
|
||||
<span>{{ $t('matchReportApi.result') }}</span>
|
||||
<span v-if="isMatchCompleted" class="locked-indicator">🔒</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'completion', disabled: !canOpenNextStages }"
|
||||
@click="setActiveSection('completion')" :disabled="!canOpenNextStages">
|
||||
<div class="section-icon">✅</div>
|
||||
<span>Abschluss</span>
|
||||
<span>{{ $t('matchReportApi.completion') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +71,7 @@
|
||||
<h3>{{ meetingData.homeClub }}</h3>
|
||||
<div class="points">{{ getOverallMatchScore().split(':')[0] }}</div>
|
||||
</div>
|
||||
<div class="vs">vs</div>
|
||||
<div class="vs">{{ $t('matchReportApi.vs') }}</div>
|
||||
<div class="guest-team">
|
||||
<h3>{{ meetingData.guestClub }}</h3>
|
||||
<div class="points">{{ getOverallMatchScore().split(':')[1] }}</div>
|
||||
@@ -89,12 +89,12 @@
|
||||
@change="setStartTime($event.target.value)"
|
||||
class="time-input"
|
||||
/>
|
||||
<button @click="setCurrentStartTime" class="time-btn" title="Aktuelle Zeit setzen">🕐</button>
|
||||
<button @click="setCurrentStartTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')">🕐</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<label>Endzeit:</label>
|
||||
<label>{{ $t('matchReportApi.endTime') }}:</label>
|
||||
<div class="time-input-group">
|
||||
<input
|
||||
type="time"
|
||||
@@ -102,51 +102,51 @@
|
||||
@change="setEndTime($event.target.value)"
|
||||
class="time-input"
|
||||
/>
|
||||
<button @click="setCurrentEndTime" class="time-btn" title="Aktuelle Zeit setzen">🕐</button>
|
||||
<button @click="setCurrentEndTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')">🕐</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<label>Status:</label>
|
||||
<label>{{ $t('matchReportApi.status') }}:</label>
|
||||
<span :class="meetingData.isCompleted ? 'completed' : 'pending'">
|
||||
{{ meetingData.isCompleted ? 'Abgeschlossen' : 'Ausstehend' }}
|
||||
{{ meetingData.isCompleted ? $t('matchReportApi.completed') : $t('matchReportApi.pending') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<label>Nicht angetreten:</label>
|
||||
<label>{{ $t('matchReportApi.notAppeared') }}:</label>
|
||||
<select
|
||||
v-model="teamNotAppeared"
|
||||
class="not-appeared-select"
|
||||
:class="{ 'not-appeared': teamNotAppeared !== null }"
|
||||
>
|
||||
<option :value="null">Beide Mannschaften angetreten</option>
|
||||
<option value="home">Heimmannschaft ({{ meetingData.homeTeamname || meetingData.homeClub }}) nicht angetreten</option>
|
||||
<option value="guest">Gastmannschaft ({{ meetingData.guestTeamname || meetingData.guestClub }}) nicht angetreten</option>
|
||||
<option :value="null">{{ $t('matchReportApi.bothTeamsAppeared') }}</option>
|
||||
<option value="home">{{ $t('matchReportApi.homeTeamNotAppeared', { team: meetingData.homeTeamname || meetingData.homeClub }) }}</option>
|
||||
<option value="guest">{{ $t('matchReportApi.guestTeamNotAppeared', { team: meetingData.guestTeamname || meetingData.guestClub }) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="info-item" v-if="(meetingDetails && meetingDetails.playMode) || (meetingData && (meetingData.playMode || meetingData.matchSystem || meetingData.system))">
|
||||
<label>Spielsystem:</label>
|
||||
<label>{{ $t('matchReportApi.playSystem') }}:</label>
|
||||
<span>{{ (meetingDetails && meetingDetails.playMode) || meetingData.playMode || meetingData.matchSystem || meetingData.system }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'homeLineup'" class="lineup-content">
|
||||
<h3>Aufstellung Heim ({{ meetingData.homeTeamname }})</h3>
|
||||
<h3>{{ $t('matchReportApi.homeLineup') }} ({{ meetingData.homeTeamname }})</h3>
|
||||
|
||||
<!-- PIN-Eingabe für Heimverein -->
|
||||
<div class="pin-section">
|
||||
<h4>PIN-Eingabe</h4>
|
||||
<h4>{{ $t('matchReportApi.pinInput') }}</h4>
|
||||
<div class="pin-input-group">
|
||||
<label for="homePin">PIN Heimverein:</label>
|
||||
<label for="homePin">{{ $t('matchReportApi.homePin') }}:</label>
|
||||
<div class="pin-input-wrapper">
|
||||
<input id="homePin" v-model="homePin" type="password" class="pin-input"
|
||||
@input="onPinChange('home', $event)" />
|
||||
<button @click="signLineup('home')" class="sign-btn"
|
||||
:disabled="!canSignLineup('home')">
|
||||
✍️ Aufstellung signieren
|
||||
✍️ {{ $t('matchReportApi.signLineup') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Fehlermeldung für Mindestspielerzahlen -->
|
||||
@@ -157,21 +157,21 @@
|
||||
</div>
|
||||
|
||||
<div v-if="meetingDetails && meetingDetails.teamLineupHomePlayers" class="lineup-section">
|
||||
<h4>Ausgewählte Spieler</h4>
|
||||
<h4>{{ $t('matchReportApi.selectedPlayers') }}</h4>
|
||||
<div class="players-grid">
|
||||
<div v-for="player in meetingDetails.teamLineupHomePlayers.filter(p => p.isSelected)"
|
||||
:key="player.nuLigaPersonId" class="player-card selected">
|
||||
<div class="player-main" @click="togglePlayerSelection(player, 'home')">
|
||||
<div class="player-name">{{ player.firstname }} {{ player.lastname }}</div>
|
||||
<div class="player-details">
|
||||
<span class="player-rank">Rang: {{ player.rank }}</span>
|
||||
<span class="player-rank">{{ $t('matchReportApi.rank') }}: {{ player.rank }}</span>
|
||||
<span class="player-position">
|
||||
{{ player.positions.join(', ') }}
|
||||
<span v-if="player.positionDouble" class="double-position"> | D{{
|
||||
player.positionDouble }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="player-action">👆 Klicken zum Entfernen</div>
|
||||
<div class="player-action">👆 {{ $t('matchReportApi.clickToRemove') }}</div>
|
||||
</div>
|
||||
<div class="double-selection">
|
||||
<button class="double-btn d1-btn"
|
||||
@@ -190,14 +190,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Verfügbare Spieler</h4>
|
||||
<h4>{{ $t('matchReportApi.availablePlayers') }}</h4>
|
||||
<div class="players-grid">
|
||||
<div v-for="player in meetingDetails.teamLineupHomePlayers.filter(p => !p.isSelected && p.nuLigaPersonId > 0)"
|
||||
:key="player.nuLigaPersonId" class="player-card available clickable">
|
||||
<div @click="togglePlayerSelection(player, 'home')">
|
||||
<div class="player-name">{{ player.firstname }} {{ player.lastname }}</div>
|
||||
<div class="player-details">
|
||||
<span class="player-rank">Rang: {{ player.rank }}</span>
|
||||
<span class="player-rank">{{ $t('matchReportApi.rank') }}: {{ player.rank }}</span>
|
||||
<span class="player-position">Verfügbar</span>
|
||||
</div>
|
||||
<div class="player-action">👆 Klicken zum Hinzufügen</div>
|
||||
@@ -221,18 +221,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<p>Keine Aufstellungsdaten verfügbar</p>
|
||||
<p>{{ $t('matchReportApi.noLineupData') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'guestLineup'" class="lineup-content">
|
||||
<h3>Aufstellung Gast ({{ meetingData.guestTeamname }})</h3>
|
||||
<h3>{{ $t('matchReportApi.guestLineup') }} ({{ meetingData.guestTeamname }})</h3>
|
||||
|
||||
<!-- PIN-Eingabe für Gastverein -->
|
||||
<div class="pin-section">
|
||||
<h4>PIN-Eingabe</h4>
|
||||
<h4>{{ $t('matchReportApi.pinInput') }}</h4>
|
||||
<div class="pin-input-group">
|
||||
<label for="guestPin">PIN Gastverein:</label>
|
||||
<label for="guestPin">{{ $t('matchReportApi.guestPin') }}:</label>
|
||||
<div class="pin-input-wrapper">
|
||||
<input id="guestPin" v-model="guestPin" type="password" class="pin-input"
|
||||
autocomplete="new-password" autocapitalize="off" spellcheck="false"
|
||||
@@ -241,7 +241,7 @@
|
||||
@input="onPinChange('guest', $event)" />
|
||||
<button @click="signLineup('guest')" class="sign-btn"
|
||||
:disabled="!canSignLineup('guest')">
|
||||
✍️ Aufstellung signieren
|
||||
✍️ {{ $t('matchReportApi.signLineup') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Fehlermeldung für Mindestspielerzahlen -->
|
||||
@@ -252,21 +252,21 @@
|
||||
</div>
|
||||
|
||||
<div v-if="meetingDetails && meetingDetails.teamLineupGuestPlayers" class="lineup-section">
|
||||
<h4>Ausgewählte Spieler</h4>
|
||||
<h4>{{ $t('matchReportApi.selectedPlayers') }}</h4>
|
||||
<div class="players-grid">
|
||||
<div v-for="player in meetingDetails.teamLineupGuestPlayers.filter(p => p.isSelected)"
|
||||
:key="player.nuLigaPersonId" class="player-card selected">
|
||||
<div class="player-main" @click="togglePlayerSelection(player, 'guest')">
|
||||
<div class="player-name">{{ player.firstname }} {{ player.lastname }}</div>
|
||||
<div class="player-details">
|
||||
<span class="player-rank">Rang: {{ player.rank }}</span>
|
||||
<span class="player-rank">{{ $t('matchReportApi.rank') }}: {{ player.rank }}</span>
|
||||
<span class="player-position">
|
||||
{{ player.positions.join(', ') }}
|
||||
<span v-if="player.positionDouble" class="double-position"> | D{{
|
||||
player.positionDouble }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="player-action">👆 Klicken zum Entfernen</div>
|
||||
<div class="player-action">👆 {{ $t('matchReportApi.clickToRemove') }}</div>
|
||||
</div>
|
||||
<div class="double-selection">
|
||||
<button class="double-btn d1-btn"
|
||||
@@ -285,14 +285,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Verfügbare Spieler</h4>
|
||||
<h4>{{ $t('matchReportApi.availablePlayers') }}</h4>
|
||||
<div class="players-grid">
|
||||
<div v-for="player in meetingDetails.teamLineupGuestPlayers.filter(p => !p.isSelected && p.nuLigaPersonId > 0)"
|
||||
:key="player.nuLigaPersonId" class="player-card available clickable">
|
||||
<div @click="togglePlayerSelection(player, 'guest')">
|
||||
<div class="player-name">{{ player.firstname }} {{ player.lastname }}</div>
|
||||
<div class="player-details">
|
||||
<span class="player-rank">Rang: {{ player.rank }}</span>
|
||||
<span class="player-rank">{{ $t('matchReportApi.rank') }}: {{ player.rank }}</span>
|
||||
<span class="player-position">Verfügbar</span>
|
||||
</div>
|
||||
<div class="player-action">👆 Klicken zum Hinzufügen</div>
|
||||
@@ -315,7 +315,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<p>Keine Aufstellungsdaten verfügbar</p>
|
||||
<p>{{ $t('matchReportApi.noLineupData') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<!-- Toggle für Analyzer -->
|
||||
<div class="analyzer-toggle">
|
||||
<button @click="showAnalyzer = !showAnalyzer" class="toggle-btn">
|
||||
{{ showAnalyzer ? '🔼 Analyzer verstecken' : '🔍 nuscore analysieren' }}
|
||||
{{ showAnalyzer ? '🔼 ' + $t('matchReport.hideAnalyzer') : '🔍 ' + $t('matchReport.analyzeNuscore') }}
|
||||
</button>
|
||||
<button @click="createLocalCopy" class="toggle-btn" style="margin-left: 10px;">
|
||||
💾 Lokale Kopie erstellen
|
||||
💾 {{ $t('matchReport.createLocalCopy') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="match-report-header-actions">
|
||||
<button @click="insertPin" class="header-action-btn" title="PIN automatisch einfügen">
|
||||
📌 PIN einfügen
|
||||
<button @click="insertPin" class="header-action-btn" :title="$t('matchReportHeaderActions.insertPinTitle')">
|
||||
📌 {{ $t('matchReportHeaderActions.insertPin') }}
|
||||
</button>
|
||||
<button @click="copyPin($event)" class="header-action-btn copy-button" title="PIN in Zwischenablage kopieren">
|
||||
📋 PIN kopieren
|
||||
<button @click="copyPin($event)" class="header-action-btn copy-button" :title="$t('matchReportHeaderActions.copyPinTitle')">
|
||||
📋 {{ $t('matchReportHeaderActions.copyPin') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
const pin = this.match.homePin || this.match.guestPin;
|
||||
if (!pin) {
|
||||
if (button) {
|
||||
this.showCopyFeedback(button, 'Keine PIN verfügbar', '#dc3545');
|
||||
this.showCopyFeedback(button, this.$t('matchReportHeaderActions.noPinAvailable'), '#dc3545');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -45,10 +45,10 @@ export default {
|
||||
await navigator.clipboard.writeText(pin);
|
||||
console.log('✅ PIN erfolgreich kopiert:', pin);
|
||||
if (button) {
|
||||
this.showCopyFeedback(button, '✅ Kopiert!', '#28a745');
|
||||
this.showCopyFeedback(button, '✅ ' + this.$t('matchReportHeaderActions.copied'), '#28a745');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Kopieren der PIN:', error);
|
||||
console.error('❌ ' + this.$t('matchReportHeaderActions.copyError') + ':', error);
|
||||
|
||||
// Fallback: Text-Auswahl
|
||||
const textArea = document.createElement('textarea');
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (button) {
|
||||
this.showCopyFeedback(button, '✅ Kopiert!', '#28a745');
|
||||
this.showCopyFeedback(button, '✅ ' + this.$t('matchReportHeaderActions.copied'), '#28a745');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-model="isOpen"
|
||||
:title="`Übungen von ${memberName}`"
|
||||
:title="`${$t('memberActivities.title')} ${memberName}`"
|
||||
@close="closeDialog"
|
||||
:width="800"
|
||||
>
|
||||
<div class="activities-content">
|
||||
<!-- Period Selection -->
|
||||
<div class="period-selector">
|
||||
<label>Zeitraum:</label>
|
||||
<label>{{ $t('memberActivities.period') }}:</label>
|
||||
<select v-model="selectedPeriod" @change="loadActivities">
|
||||
<option value="month">Letzte 4 Wochen</option>
|
||||
<option value="3months">Letzte 3 Monate</option>
|
||||
<option value="6months">Letztes halbes Jahr</option>
|
||||
<option value="year">Letztes Jahr</option>
|
||||
<option value="all">Alle</option>
|
||||
<option value="month">{{ $t('memberActivities.last4Weeks') }}</option>
|
||||
<option value="3months">{{ $t('memberActivities.last3Months') }}</option>
|
||||
<option value="6months">{{ $t('memberActivities.last6Months') }}</option>
|
||||
<option value="year">{{ $t('memberActivities.lastYear') }}</option>
|
||||
<option value="all">{{ $t('memberActivities.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="loading">
|
||||
Lade Übungen...
|
||||
{{ $t('memberActivities.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- No Activities -->
|
||||
<div v-else-if="activities.length === 0" class="no-activities">
|
||||
Keine Übungen im gewählten Zeitraum gefunden.
|
||||
{{ $t('memberActivities.noActivities') }}
|
||||
</div>
|
||||
|
||||
<!-- Activities List -->
|
||||
@@ -33,9 +33,9 @@
|
||||
<table class="activities-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Übung</th>
|
||||
<th>Häufigkeit</th>
|
||||
<th>Daten</th>
|
||||
<th>{{ $t('memberActivities.activity') }}</th>
|
||||
<th>{{ $t('memberActivities.frequency') }}</th>
|
||||
<th>{{ $t('memberActivities.dates') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -56,14 +56,14 @@
|
||||
@click="toggleShowAllDates(activity.name)"
|
||||
class="show-more-btn"
|
||||
>
|
||||
+{{ activity.dates.length - 5 }} weitere
|
||||
+{{ activity.dates.length - 5 }} {{ $t('memberActivities.more') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="activity.dates.length > 5 && showAllDates[activity.name]"
|
||||
@click="toggleShowAllDates(activity.name)"
|
||||
class="show-less-btn"
|
||||
>
|
||||
weniger anzeigen
|
||||
{{ $t('memberActivities.showLess') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button @click="closeDialog" class="btn-close">Schließen</button>
|
||||
<button @click="closeDialog" class="btn-close">{{ $t('memberActivities.close') }}</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Übungs-Statistiken ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
:title="`${$t('activityStats.title')} ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="medium"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="activity-stats-content">
|
||||
<!-- Letzte 3 Teilnahmen -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">Letzte 3 Teilnahmen</h3>
|
||||
<h3 class="section-title">{{ $t('activityStats.last3Participations') }}</h3>
|
||||
<div v-if="groupedParticipations && groupedParticipations.length" class="participations-list">
|
||||
<div v-for="dateGroup in groupedParticipations" :key="dateGroup.date" class="participation-date-group">
|
||||
<div class="participation-date-header">{{ formatDate(dateGroup.date) }}</div>
|
||||
@@ -21,13 +21,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<em>Keine Teilnahmen vorhanden</em>
|
||||
<em>{{ $t('activityStats.noParticipations') }}</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistik der Übungen -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">Statistik der Übungen</h3>
|
||||
<h3 class="section-title">{{ $t('activityStats.activityStatistics') }}</h3>
|
||||
<div v-if="activityStats && activityStats.length" class="stats-list">
|
||||
<div v-for="stat in activityStats" :key="stat.code || stat.name" class="stat-item">
|
||||
<div class="stat-name" :title="stat.name">{{ stat.code || stat.name }}</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<em>Keine Statistiken vorhanden</em>
|
||||
<em>{{ $t('activityStats.noStatistics') }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="date && date !== 'new' ? 'Mitglieder-Galerie - Klicken Sie auf ein Bild, um als Teilnehmer hinzuzufügen' : 'Mitglieder-Galerie'"
|
||||
:title="date && date !== 'new' ? $t('memberGallery.titleWithDate') : $t('memberGallery.title')"
|
||||
size="large"
|
||||
:close-on-overlay="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="gallery-dialog-content">
|
||||
<div class="gallery-controls">
|
||||
<label for="gallery-size">Bildgröße:</label>
|
||||
<label for="gallery-size">{{ $t('memberGallery.imageSize') }}:</label>
|
||||
<select id="gallery-size" v-model="gallerySize" @change="loadGalleryMembers" :disabled="galleryLoading">
|
||||
<option :value="100">100x100 px</option>
|
||||
<option :value="150">150x150 px</option>
|
||||
<option :value="200">200x200 px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="galleryLoading" class="gallery-loading">Galerie wird geladen…</div>
|
||||
<div v-if="galleryLoading" class="gallery-loading">{{ $t('memberGallery.loading') }}</div>
|
||||
<div v-else-if="galleryMembers.length > 0" class="gallery-members-grid" :style="{ gridTemplateColumns: 'repeat(auto-fill, ' + gallerySize + 'px)' }">
|
||||
<div
|
||||
v-for="member in galleryMembers"
|
||||
@@ -34,12 +34,12 @@
|
||||
@error="handleImageError(member)"
|
||||
@load="handleImageLoad(member)"
|
||||
/>
|
||||
<div v-else class="gallery-member-placeholder">Kein Bild</div>
|
||||
<div v-else class="gallery-member-placeholder">{{ $t('memberGallery.noImage') }}</div>
|
||||
<div class="gallery-member-name">{{ member.fullName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gallery-error">
|
||||
{{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
|
||||
{{ galleryError || $t('memberGallery.noMembersWithImages') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Notizen für ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
:title="`${$t('memberNotes.title')} ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="large"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="member" class="notes-modal-content">
|
||||
<div class="notes-header-info">
|
||||
Telefon-Nr.: {{ member.phone }}
|
||||
{{ $t('memberNotes.phone') }}: {{ member.phone }}
|
||||
</div>
|
||||
<div class="notes-body">
|
||||
<div class="notes-left">
|
||||
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
|
||||
<img v-if="member.imageUrl" :src="member.imageUrl" :alt="$t('memberNotes.memberImage')"
|
||||
class="member-image" />
|
||||
</div>
|
||||
<div class="notes-right">
|
||||
<div class="form-group">
|
||||
<label>Tags</label>
|
||||
<label>{{ $t('memberNotes.tags') }}</label>
|
||||
<multiselect
|
||||
v-model="localSelectedTags"
|
||||
:options="availableTags"
|
||||
placeholder="Tags auswählen"
|
||||
:placeholder="$t('memberNotes.selectTags')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
multiple
|
||||
@@ -32,12 +32,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Neue Notiz</label>
|
||||
<textarea v-model="localNoteContent" placeholder="Neue Notiz" rows="4" class="note-textarea"></textarea>
|
||||
<button @click="handleAddNote" class="btn-primary">Hinzufügen</button>
|
||||
<label>{{ $t('memberNotes.newNote') }}</label>
|
||||
<textarea v-model="localNoteContent" :placeholder="$t('memberNotes.newNote')" rows="4" class="note-textarea"></textarea>
|
||||
<button @click="handleAddNote" class="btn-primary">{{ $t('memberNotes.add') }}</button>
|
||||
</div>
|
||||
<div class="notes-list">
|
||||
<h4>Notizen</h4>
|
||||
<h4>{{ $t('memberNotes.notes') }}</h4>
|
||||
<ul>
|
||||
<li v-for="note in notes" :key="note.id" class="note-item">
|
||||
<button @click="$emit('delete-note', note.id)" class="trash-btn">🗑️</button>
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Mitglieder auswählen"
|
||||
:title="$t('memberSelection.title')"
|
||||
size="large"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="member-selection-content">
|
||||
<div class="controls-bar">
|
||||
<button class="btn-secondary" @click="$emit('select-all')">Alle auswählen</button>
|
||||
<button class="btn-secondary" @click="$emit('deselect-all')">Alle abwählen</button>
|
||||
<button class="btn-secondary" @click="$emit('select-all')">{{ $t('memberSelection.selectAll') }}</button>
|
||||
<button class="btn-secondary" @click="$emit('deselect-all')">{{ $t('memberSelection.deselectAll') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="selection-layout">
|
||||
<div class="members-column">
|
||||
<h4>Mitglieder</h4>
|
||||
<h4>{{ $t('memberSelection.members') }}</h4>
|
||||
<div class="checkbox-list">
|
||||
<label v-for="m in members" :key="m.id" class="checkbox-item">
|
||||
<input
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="recommendations-column" v-if="activeMember && showRecommendations">
|
||||
<h4>Empfehlungen</h4>
|
||||
<h4>{{ $t('memberSelection.recommendations') }}</h4>
|
||||
<div v-if="recommendations && recommendations.length" class="checkbox-list">
|
||||
<label v-for="rec in recommendations" :key="rec.key" class="checkbox-item">
|
||||
<input
|
||||
@@ -44,20 +44,20 @@
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<em>Keine passenden Empfehlungen gefunden.</em>
|
||||
<em>{{ $t('memberSelection.noRecommendations') }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Schließen</button>
|
||||
<button class="btn-secondary" @click="handleClose">{{ $t('memberSelection.close') }}</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="selectedIds.length === 0"
|
||||
@click="$emit('generate-pdf')"
|
||||
>
|
||||
PDF erzeugen
|
||||
{{ $t('memberSelection.generatePdf') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -2,60 +2,60 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Mitglieder übertragen"
|
||||
:title="$t('memberTransferDialog.title')"
|
||||
size="large"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="loadingConfig" class="loading-config">
|
||||
Gespeicherte Konfiguration wird geladen...
|
||||
{{ $t('memberTransferDialog.loadingConfig') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="transfer-form">
|
||||
<div v-if="!hasConfig" class="config-missing">
|
||||
<p><strong>Keine Konfiguration gefunden</strong></p>
|
||||
<p>Bitte konfigurieren Sie zuerst die Mitgliederübertragung in den Einstellungen.</p>
|
||||
<p><strong>{{ $t('memberTransferDialog.noConfigFound') }}</strong></p>
|
||||
<p>{{ $t('memberTransferDialog.noConfigMessage') }}</p>
|
||||
<router-link to="/member-transfer-settings" class="btn-link" @click="handleClose">
|
||||
Zu den Einstellungen
|
||||
{{ $t('memberTransferDialog.goToSettings') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="config-summary">
|
||||
<h4>Übertragungskonfiguration</h4>
|
||||
<h4>{{ $t('memberTransferDialog.transferConfiguration') }}</h4>
|
||||
<div class="summary-info">
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Server:</span>
|
||||
<span class="summary-label">{{ $t('memberTransferDialog.server') }}:</span>
|
||||
<span class="summary-value">{{ config.server }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Endpoint:</span>
|
||||
<span class="summary-label">{{ $t('memberTransferDialog.endpoint') }}:</span>
|
||||
<span class="summary-value">{{ config.transferEndpoint }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Methode:</span>
|
||||
<span class="summary-label">{{ $t('memberTransferDialog.method') }}:</span>
|
||||
<span class="summary-value">{{ config.transferMethod }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Format:</span>
|
||||
<span class="summary-label">{{ $t('memberTransferDialog.format') }}:</span>
|
||||
<span class="summary-value">{{ config.transferFormat }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Modus:</span>
|
||||
<span class="summary-value">{{ config.useBulkMode ? 'Bulk-Import' : 'Einzeln' }}</span>
|
||||
<span class="summary-label">{{ $t('memberTransferDialog.mode') }}:</span>
|
||||
<span class="summary-value">{{ config.useBulkMode ? $t('memberTransferDialog.bulkImport') : $t('memberTransferDialog.single') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<router-link to="/member-transfer-settings" class="btn-link-small" @click="handleClose">
|
||||
Konfiguration bearbeiten
|
||||
{{ $t('memberTransferDialog.editConfig') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="config.loginEndpoint" class="form-section">
|
||||
<h4>Login-Daten (optional überschreiben)</h4>
|
||||
<p class="section-hint">Die Login-Daten werden aus den Einstellungen verwendet. Sie können sie hier überschreiben, falls nötig.</p>
|
||||
<h4>{{ $t('memberTransferDialog.loginDataOverride') }}</h4>
|
||||
<p class="section-hint">{{ $t('memberTransferDialog.loginDataHint') }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Login-Daten:</label>
|
||||
<label>{{ $t('memberTransferDialog.loginData') }}:</label>
|
||||
<div class="credentials-group">
|
||||
<div class="credential-row">
|
||||
<!-- Dummy-Felder, um Browser-Autofill zu verhindern (ausgeblendet) -->
|
||||
@@ -65,7 +65,7 @@
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.username"
|
||||
placeholder="Benutzername / Email"
|
||||
:placeholder="$t('memberTransferDialog.usernameEmail')"
|
||||
class="form-input"
|
||||
autocomplete="off"
|
||||
name="transfer-username"
|
||||
@@ -74,7 +74,7 @@
|
||||
<input
|
||||
type="password"
|
||||
v-model="loginCredentials.password"
|
||||
placeholder="Passwort (leer lassen für gespeichertes)"
|
||||
:placeholder="$t('memberTransferDialog.passwordPlaceholder')"
|
||||
class="form-input"
|
||||
autocomplete="new-password"
|
||||
name="transfer-password"
|
||||
@@ -100,20 +100,20 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hint">Nur ausfüllen, wenn Sie die gespeicherten Login-Daten überschreiben möchten. Leere Felder verwenden die gespeicherten Werte.</span>
|
||||
<span class="hint">{{ $t('memberTransferDialog.loginDataOverrideHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Abbrechen</button>
|
||||
<button class="btn-secondary" @click="handleClose">{{ $t('memberTransferDialog.cancel') }}</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleTransfer"
|
||||
:disabled="!isValid || isTransferring"
|
||||
>
|
||||
{{ isTransferring ? 'Übertrage...' : 'Übertragen' }}
|
||||
{{ isTransferring ? $t('memberTransferDialog.transferring') : $t('memberTransferDialog.transfer') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
@@ -149,16 +149,16 @@ export default {
|
||||
|
||||
additionalField1Placeholder() {
|
||||
if (this.config.loginFormat === 'form-data' || this.config.loginFormat === 'x-www-form-urlencoded') {
|
||||
return 'Feldname: Wert (z.B. client_id: abc123)';
|
||||
return this.$t('memberTransferDialog.additionalFieldPlaceholderForm');
|
||||
}
|
||||
return 'Zusätzliches Feld (z.B. client_id)';
|
||||
return this.$t('memberTransferDialog.additionalFieldPlaceholder');
|
||||
},
|
||||
|
||||
additionalField2Placeholder() {
|
||||
if (this.config.loginFormat === 'form-data' || this.config.loginFormat === 'x-www-form-urlencoded') {
|
||||
return 'Feldname: Wert (z.B. client_secret: xyz789)';
|
||||
return this.$t('memberTransferDialog.additionalFieldPlaceholderForm');
|
||||
}
|
||||
return 'Zusätzliches Feld (z.B. client_secret)';
|
||||
return this.$t('memberTransferDialog.additionalFieldPlaceholder');
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -368,14 +368,14 @@ export default {
|
||||
if (response.data.success) {
|
||||
const message = getSafeMessage(
|
||||
response.data.message,
|
||||
`${response.data.transferred} von ${response.data.total} Mitgliedern erfolgreich übertragen.`
|
||||
this.$t('memberTransferDialog.transferSuccess', { transferred: response.data.transferred, total: response.data.total })
|
||||
);
|
||||
|
||||
let details = '';
|
||||
|
||||
// Zeige ausgeschlossene Mitglieder an
|
||||
if (response.data.invalidMembers && response.data.invalidMembers.length > 0) {
|
||||
details += 'Ausgeschlossene Mitglieder (fehlende Pflichtfelder):\n';
|
||||
details += this.$t('memberTransferDialog.excludedMembers') + ':\n';
|
||||
details += response.data.invalidMembers.map(inv => {
|
||||
const name = sanitizeText(`${inv.member.firstName || ''} ${inv.member.lastName || ''}`.trim(), `ID: ${inv.member.id}`);
|
||||
const errorText = sanitizeText(inv.errors.join(', '), 'Unbekannter Fehler');
|
||||
@@ -386,7 +386,7 @@ export default {
|
||||
|
||||
// Zeige weitere Fehler an
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
details += 'Weitere Fehler:\n';
|
||||
details += this.$t('memberTransferDialog.additionalErrors') + ':\n';
|
||||
details += response.data.errors.map((e) => {
|
||||
const memberName = sanitizeText(e.member, 'Unbekanntes Mitglied');
|
||||
const err = sanitizeText(e.error, 'Unbekannter Fehler');
|
||||
@@ -403,10 +403,10 @@ export default {
|
||||
this.handleClose();
|
||||
} else {
|
||||
// Bei Fehlern auch ausgeschlossene Mitglieder anzeigen
|
||||
let errorDetails = getSafeMessage(response.data.error, 'Übertragung fehlgeschlagen');
|
||||
let errorDetails = getSafeMessage(response.data.error, this.$t('memberTransferDialog.transferFailed'));
|
||||
|
||||
if (response.data.invalidMembers && response.data.invalidMembers.length > 0) {
|
||||
errorDetails += '\n\nAusgeschlossene Mitglieder (fehlende Pflichtfelder):\n';
|
||||
errorDetails += '\n\n' + this.$t('memberTransferDialog.excludedMembers') + ':\n';
|
||||
errorDetails += response.data.invalidMembers.map(inv => {
|
||||
const name = sanitizeText(`${inv.member.firstName || ''} ${inv.member.lastName || ''}`.trim(), `ID: ${inv.member.id}`);
|
||||
const err = sanitizeText(inv.errors.join(', '), 'Unbekannter Fehler');
|
||||
@@ -415,7 +415,7 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('error', {
|
||||
message: getSafeMessage(response.data.message, 'Übertragung fehlgeschlagen'),
|
||||
message: getSafeMessage(response.data.message, this.$t('memberTransferDialog.transferFailed')),
|
||||
error: errorDetails
|
||||
});
|
||||
|
||||
@@ -423,10 +423,10 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transfer error:', error);
|
||||
const errorMessage = getSafeErrorMessage(error, 'Fehler bei der Übertragung');
|
||||
const errorMessage = getSafeErrorMessage(error, this.$t('memberTransferDialog.transferError'));
|
||||
|
||||
this.$emit('error', {
|
||||
message: 'Fehler bei der Übertragung',
|
||||
message: this.$t('memberTransferDialog.transferError'),
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="modal-overlay" @click.self="$emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ account ? 'myTischtennis-Account bearbeiten' : 'myTischtennis-Account verknüpfen' }}</h3>
|
||||
<h3>{{ account ? $t('myTischtennisDialog.editAccount') : $t('myTischtennisDialog.linkAccount') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="mtt-email">myTischtennis-E-Mail:</label>
|
||||
<label for="mtt-email">{{ $t('myTischtennisDialog.email') }}:</label>
|
||||
<input
|
||||
type="email"
|
||||
id="mtt-email"
|
||||
v-model="formData.email"
|
||||
placeholder="Ihre myTischtennis-E-Mail-Adresse"
|
||||
:placeholder="$t('myTischtennisDialog.emailPlaceholder')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mtt-password">myTischtennis-Passwort:</label>
|
||||
<label for="mtt-password">{{ $t('myTischtennisDialog.password') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="mtt-password"
|
||||
v-model="formData.password"
|
||||
:placeholder="account && account.savePassword ? 'Leer lassen um beizubehalten' : 'Ihr myTischtennis-Passwort'"
|
||||
:placeholder="account && account.savePassword ? $t('myTischtennisDialog.passwordPlaceholderKeep') : $t('myTischtennisDialog.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,11 +33,10 @@
|
||||
type="checkbox"
|
||||
v-model="formData.savePassword"
|
||||
/>
|
||||
<span>myTischtennis-Passwort speichern</span>
|
||||
<span>{{ $t('myTischtennisDialog.savePassword') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
Wenn aktiviert, wird Ihr myTischtennis-Passwort verschlüsselt gespeichert,
|
||||
sodass automatische Synchronisationen möglich sind.
|
||||
{{ $t('myTischtennisDialog.savePasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -48,29 +47,27 @@
|
||||
v-model="formData.autoUpdateRatings"
|
||||
:disabled="!formData.savePassword"
|
||||
/>
|
||||
<span>Automatische Update-Ratings aktivieren</span>
|
||||
<span>{{ $t('myTischtennisDialog.autoUpdateRatings') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
Täglich um 6:00 Uhr werden automatisch die neuesten Ratings von myTischtennis abgerufen.
|
||||
<strong>Erfordert gespeichertes Passwort.</strong>
|
||||
{{ $t('myTischtennisDialog.autoUpdateRatingsHint') }}
|
||||
</p>
|
||||
<p v-if="!formData.savePassword" class="warning">
|
||||
⚠️ Für automatische Updates muss das myTischtennis-Passwort gespeichert werden.
|
||||
⚠️ {{ $t('myTischtennisDialog.autoUpdateWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="formData.password">
|
||||
<label for="app-password">Ihr App-Passwort zur Bestätigung:</label>
|
||||
<label for="app-password">{{ $t('myTischtennisDialog.appPassword') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="app-password"
|
||||
v-model="formData.userPassword"
|
||||
placeholder="Ihr Passwort für diese App"
|
||||
:placeholder="$t('myTischtennisDialog.appPasswordPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<p class="hint">
|
||||
Aus Sicherheitsgründen benötigen wir Ihr App-Passwort,
|
||||
um das myTischtennis-Passwort zu speichern.
|
||||
{{ $t('myTischtennisDialog.appPasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -81,10 +78,10 @@
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
|
||||
Abbrechen
|
||||
{{ $t('myTischtennisDialog.cancel') }}
|
||||
</button>
|
||||
<button class="btn-primary" @click="saveAccount" :disabled="!canSave || saving">
|
||||
{{ saving ? 'Speichere...' : 'Speichern' }}
|
||||
{{ saving ? $t('myTischtennisDialog.saving') : $t('myTischtennisDialog.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +156,7 @@ export default {
|
||||
this.$emit('saved');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
this.error = error.response?.data?.message || 'Fehler beim Speichern des Accounts';
|
||||
this.error = error.response?.data?.message || this.$t('myTischtennisDialog.errorSaving');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
<div class="modal-overlay" @click.self="$emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>Update-Ratings History</h3>
|
||||
<h3>{{ $t('myTischtennisHistory.title') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div v-if="loading" class="loading">
|
||||
Lade History...
|
||||
{{ $t('myTischtennisHistory.loading') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="history.length === 0" class="no-history">
|
||||
<p>Noch keine automatischen Updates durchgeführt.</p>
|
||||
<p>{{ $t('myTischtennisHistory.noHistory') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="history-header">
|
||||
<span class="history-date">{{ formatDate(entry.createdAt) }}</span>
|
||||
<span class="history-status" :class="entry.success ? 'success' : 'error'">
|
||||
{{ entry.success ? 'Erfolgreich' : 'Fehlgeschlagen' }}
|
||||
{{ entry.success ? $t('myTischtennisHistory.successful') : $t('myTischtennisHistory.failed') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="entry.message" class="history-message">
|
||||
@@ -29,7 +29,7 @@
|
||||
{{ entry.errorDetails }}
|
||||
</div>
|
||||
<div v-if="entry.updatedCount !== undefined" class="history-stats">
|
||||
{{ entry.updatedCount }} Ratings aktualisiert
|
||||
{{ entry.updatedCount }} {{ $t('myTischtennisHistory.ratingsUpdated') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @click="$emit('close')">
|
||||
Schließen
|
||||
{{ $t('myTischtennisHistory.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="nuscore-analyzer">
|
||||
<div class="analyzer-header">
|
||||
<h3>🔍 nuscore Analyzer</h3>
|
||||
<p>Analysiert und lädt nuscore-Ressourcen für lokale Nutzung herunter</p>
|
||||
<h3>{{ $t('nuscoreAnalyzer.title') }}</h3>
|
||||
<p>{{ $t('nuscoreAnalyzer.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="analyzer-controls">
|
||||
<button @click="analyzeNuscore" :disabled="isAnalyzing" class="analyze-btn">
|
||||
{{ isAnalyzing ? '🔄 Analysiere...' : '🔍 nuscore analysieren' }}
|
||||
{{ isAnalyzing ? $t('nuscoreAnalyzer.analyzing') : $t('nuscoreAnalyzer.analyze') }}
|
||||
</button>
|
||||
|
||||
<button @click="downloadResources" :disabled="!resources.length || isDownloading" class="download-btn">
|
||||
{{ isDownloading ? '⬇️ Lade herunter...' : `⬇️ ${resources.length} Ressourcen herunterladen` }}
|
||||
{{ isDownloading ? $t('nuscoreAnalyzer.downloading') : `⬇️ ${resources.length} ${$t('nuscoreAnalyzer.downloadResources')}` }}
|
||||
</button>
|
||||
|
||||
<button @click="createLocalCopy" :disabled="!hasDownloadedResources || isCreating" class="create-btn">
|
||||
{{ isCreating ? '🏗️ Erstelle...' : '🏗️ Lokale Kopie erstellen' }}
|
||||
{{ isCreating ? $t('nuscoreAnalyzer.creating') : $t('nuscoreAnalyzer.createLocalCopy') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="analyzer-results" v-if="resources.length > 0">
|
||||
<h4>📋 Gefundene Ressourcen:</h4>
|
||||
<h4>{{ $t('nuscoreAnalyzer.foundResources') }}</h4>
|
||||
<div class="resource-list">
|
||||
<div
|
||||
v-for="resource in resources"
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<div class="analyzer-log" v-if="logs.length > 0">
|
||||
<h4>📝 Log:</h4>
|
||||
<h4>{{ $t('nuscoreAnalyzer.log') }}</h4>
|
||||
<div class="log-content">
|
||||
<div v-for="(log, index) in logs" :key="index" class="log-entry">
|
||||
{{ log }}
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
this.logs = [];
|
||||
this.resources = [];
|
||||
|
||||
this.addLog('🔍 Starte nuscore-Analyse...');
|
||||
this.addLog('🔍 ' + this.$t('nuscoreAnalyzer.startAnalysis'));
|
||||
|
||||
try {
|
||||
// Öffne nuscore in einem versteckten iframe
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
setTimeout(resolve, 5000); // Timeout nach 5 Sekunden
|
||||
});
|
||||
|
||||
this.addLog('✅ nuscore-Seite geladen');
|
||||
this.addLog('✅ ' + this.$t('nuscoreAnalyzer.pageLoaded'));
|
||||
|
||||
// Analysiere die Seite
|
||||
await this.analyzePage(iframe);
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
// Entferne das iframe
|
||||
document.body.removeChild(iframe);
|
||||
|
||||
this.addLog(`✅ Analyse abgeschlossen: ${this.resources.length} Ressourcen gefunden`);
|
||||
this.addLog('✅ ' + this.$t('nuscoreAnalyzer.analysisComplete', { count: this.resources.length }));
|
||||
|
||||
} catch (error) {
|
||||
this.addLog(`❌ Fehler bei der Analyse: ${error.message}`);
|
||||
|
||||
@@ -3,7 +3,7 @@ import autoTable from 'jspdf-autotable';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
class PDFGenerator {
|
||||
constructor(margin = 20, columnGap = 10) {
|
||||
constructor(margin = 20, columnGap = 10, t = null) {
|
||||
this.pdf = new jsPDF('p', 'mm', 'a4');
|
||||
this.margin = margin;
|
||||
this.columnGap = columnGap;
|
||||
@@ -19,6 +19,59 @@ class PDFGenerator {
|
||||
this.COLUMN_DURATION = margin + 150;
|
||||
this.LINE_HEIGHT = 7;
|
||||
this.cursorY = margin;
|
||||
// Übersetzungsfunktion (optional)
|
||||
this.t = t || ((key) => {
|
||||
// Fallback zu deutschen Texten wenn keine Übersetzung vorhanden
|
||||
const fallbacks = {
|
||||
'pdfGenerator.trainingPlan': 'Trainingsplan',
|
||||
'pdfGenerator.date': 'Datum:',
|
||||
'pdfGenerator.time': 'Uhrzeit:',
|
||||
'pdfGenerator.startTime': 'Startzeit',
|
||||
'pdfGenerator.activityTimeBlock': 'Aktivität / Zeitblock',
|
||||
'pdfGenerator.group': 'Gruppe',
|
||||
'pdfGenerator.duration': 'Dauer (Min)',
|
||||
'pdfGenerator.timeBlock': 'Zeitblock',
|
||||
'pdfGenerator.phoneList': 'Telefonliste - Aktive Mitglieder',
|
||||
'pdfGenerator.nameFirstName': 'Name, Vorname',
|
||||
'pdfGenerator.birthDate': 'Geburtsdatum',
|
||||
'pdfGenerator.phoneNumber': 'Telefon-Nr.',
|
||||
'pdfGenerator.officialTournament': 'Offizielles Turnier',
|
||||
'pdfGenerator.member': 'Mitglied:',
|
||||
'pdfGenerator.hints': 'Hinweise:',
|
||||
'pdfGenerator.tournament': 'Turnier',
|
||||
'pdfGenerator.place': 'Platz',
|
||||
'pdfGenerator.player': 'Spieler',
|
||||
'pdfGenerator.points': 'Punkte',
|
||||
'pdfGenerator.sets': 'Sätze',
|
||||
'pdfGenerator.diff': 'Diff',
|
||||
'pdfGenerator.groupMatrices': 'Gruppen-Matrizen',
|
||||
'pdfGenerator.allGames': 'Alle Spiele',
|
||||
'pdfGenerator.noActivities': 'Keine Aktivitäten für diesen Trainingstag erfasst.',
|
||||
'pdfGenerator.groupLabel': 'Gruppe',
|
||||
'pdfGenerator.oneHourBefore': 'Eine Stunde vor Beginn der Konkurrenz in der Halle sein',
|
||||
'pdfGenerator.hallShoes': 'Hallenschuhe (dürfen auf Boden nicht abfärben)',
|
||||
'pdfGenerator.informTrainer': 'Da der Verein die Meldung übernehmen möchte, die Trainer mind. eine Woche vor dem Turnier über die Teilnahme informieren',
|
||||
'pdfGenerator.trainersPresent': 'Die Trainer probieren bei allen Turnieren anwesend zu sein.',
|
||||
'pdfGenerator.recommendations': 'Empfehlungen',
|
||||
'pdfGenerator.competition': 'Wettbewerb',
|
||||
'pdfGenerator.fee': 'Gebühr',
|
||||
'pdfGenerator.alsoPlayable': 'Ebenfalls spielbar',
|
||||
'pdfGenerator.venue': 'Austragungsort',
|
||||
'pdfGenerator.venues': 'Austragungsorte',
|
||||
'pdfGenerator.noWhiteJersey': 'Kein weißes Trikot',
|
||||
'pdfGenerator.sportShorts': 'Sportshorts (oder Sportröckchen), am besten auch nicht weiß',
|
||||
'pdfGenerator.waterBottle': 'Eine Flasche Wasser dabei haben',
|
||||
'pdfGenerator.overallRanking': 'Gesamt-Ranking (K.O.-Runden)',
|
||||
'pdfGenerator.round': 'Runde',
|
||||
'pdfGenerator.player1': 'Spieler 1',
|
||||
'pdfGenerator.player2': 'Spieler 2',
|
||||
'pdfGenerator.result': 'Ergebnis',
|
||||
'pdfGenerator.status': 'Status',
|
||||
'pdfGenerator.placement': 'Platzierung',
|
||||
'pdfGenerator.competitionName': 'Konkurrenz'
|
||||
};
|
||||
return fallbacks[key] || key;
|
||||
});
|
||||
}
|
||||
|
||||
async addSchedule(element) {
|
||||
@@ -82,22 +135,22 @@ class PDFGenerator {
|
||||
addHeader(clubName, formattedDate, formattedStartTime, formattedEndTime) {
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.text(`${clubName} - Trainingsplan`, this.margin, this.yPos);
|
||||
this.pdf.text(`${clubName} - ${this.t('pdfGenerator.trainingPlan')}`, this.margin, this.yPos);
|
||||
this.yPos += 10;
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.text(`Datum: ${formattedDate}`, this.margin, this.yPos);
|
||||
this.pdf.text(`${this.t('pdfGenerator.date')} ${formattedDate}`, this.margin, this.yPos);
|
||||
this.yPos += 7;
|
||||
this.pdf.text(`Uhrzeit: ${formattedStartTime} - ${formattedEndTime}`, this.margin, this.yPos);
|
||||
this.pdf.text(`${this.t('pdfGenerator.time')} ${formattedStartTime} - ${formattedEndTime}`, this.margin, this.yPos);
|
||||
this.yPos += 10;
|
||||
}
|
||||
|
||||
addTableHeaders() {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.text('Startzeit', this.COLUMN_START_TIME, this.yPos);
|
||||
this.pdf.text('Aktivität / Zeitblock', this.COLUMN_ACTIVITY, this.yPos);
|
||||
this.pdf.text('Gruppe', this.COLUMN_GROUP, this.yPos);
|
||||
this.pdf.text('Dauer (Min)', this.COLUMN_DURATION, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.startTime'), this.COLUMN_START_TIME, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.activityTimeBlock'), this.COLUMN_ACTIVITY, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.group'), this.COLUMN_GROUP, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.duration'), this.COLUMN_DURATION, this.yPos);
|
||||
this.yPos += this.LINE_HEIGHT;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
}
|
||||
@@ -121,7 +174,7 @@ class PDFGenerator {
|
||||
addTimeBlock(item) {
|
||||
this.pdf.setFont('helvetica');
|
||||
this.pdf.text(item.startTime, this.COLUMN_START_TIME, this.yPos);
|
||||
this.pdf.text('Zeitblock', this.COLUMN_ACTIVITY, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.timeBlock'), this.COLUMN_ACTIVITY, this.yPos);
|
||||
this.yPos += this.LINE_HEIGHT;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
if (item.groupActivities && item.groupActivities.length > 0) {
|
||||
@@ -181,7 +234,7 @@ class PDFGenerator {
|
||||
addPhoneList(members) {
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.text('Telefonliste - Aktive Mitglieder', this.margin, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.phoneList'), this.margin, this.yPos);
|
||||
this.yPos += 10;
|
||||
this.addPhoneListHeaders();
|
||||
|
||||
@@ -195,9 +248,9 @@ class PDFGenerator {
|
||||
addPhoneListHeaders() {
|
||||
this.pdf.setFontSize(10);
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.text('Name, Vorname', this.margin, this.yPos);
|
||||
this.pdf.text('Geburtsdatum', this.margin + 60, this.yPos);
|
||||
this.pdf.text('Telefon-Nr.', this.margin + 120, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.nameFirstName'), this.margin, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.birthDate'), this.margin + 60, this.yPos);
|
||||
this.pdf.text(this.t('pdfGenerator.phoneNumber'), this.margin + 120, this.yPos);
|
||||
this.yPos += this.LINE_HEIGHT;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(10); // Sicherstellen, dass die Schriftgröße für die Einträge korrekt ist
|
||||
@@ -389,7 +442,7 @@ class PDFGenerator {
|
||||
|
||||
addParticipantsSummary(tournamentTitle, tournamentDateText, groups) {
|
||||
// Header
|
||||
const title = tournamentTitle || 'Offizielles Turnier';
|
||||
const title = tournamentTitle || this.t('pdfGenerator.officialTournament');
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
@@ -402,7 +455,7 @@ class PDFGenerator {
|
||||
}
|
||||
|
||||
// Tabelle mit Gruppierung
|
||||
const head = [['Mitglied', 'Konkurrenz', 'Startzeit', 'Status', 'Platzierung']];
|
||||
const head = [[this.t('pdfGenerator.member').replace(':', ''), this.t('pdfGenerator.competitionName'), this.t('pdfGenerator.startTime'), this.t('pdfGenerator.status'), this.t('pdfGenerator.placement')]];
|
||||
const body = [];
|
||||
const rowStyles = [];
|
||||
|
||||
@@ -462,24 +515,24 @@ class PDFGenerator {
|
||||
let y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(tournamentTitle || 'Offizielles Turnier', this.margin, y);
|
||||
this.pdf.text(tournamentTitle || this.t('pdfGenerator.officialTournament'), this.margin, y);
|
||||
y += 9;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.text(`Mitglied: ${memberName}`, this.margin, y);
|
||||
this.pdf.text(`${this.t('pdfGenerator.member')} ${memberName}`, this.margin, y);
|
||||
y += 8;
|
||||
// Empfehlungen (fett)
|
||||
if (recommendedRows && recommendedRows.length) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Empfehlungen', this.margin, y);
|
||||
this.pdf.text(this.t('pdfGenerator.recommendations'), this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.text('Wettbewerb', this.margin, y);
|
||||
this.pdf.text('Datum', this.margin + 80, y);
|
||||
this.pdf.text('Startzeit', this.margin + 120, y);
|
||||
this.pdf.text('Gebühr', this.margin + 160, y);
|
||||
this.pdf.text(this.t('pdfGenerator.competition'), this.margin, y);
|
||||
this.pdf.text(this.t('pdfGenerator.date').replace(':', ''), this.margin + 80, y);
|
||||
this.pdf.text(this.t('pdfGenerator.startTime'), this.margin + 120, y);
|
||||
this.pdf.text(this.t('pdfGenerator.fee'), this.margin + 160, y);
|
||||
y += 7;
|
||||
for (const r of recommendedRows) {
|
||||
this.pdf.text(r.name || '', this.margin, y);
|
||||
@@ -500,7 +553,7 @@ class PDFGenerator {
|
||||
if (y > this.pageHeight) { this.addNewPage(); y = this.margin; }
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Ebenfalls spielbar', this.margin, y);
|
||||
this.pdf.text(this.t('pdfGenerator.alsoPlayable'), this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
@@ -519,7 +572,7 @@ class PDFGenerator {
|
||||
// Austragungsort(e) direkt vor den Hinweisen
|
||||
const venueLines = Array.isArray(venues) ? venues.filter(Boolean) : [];
|
||||
if (venueLines.length) {
|
||||
const heading = venueLines.length === 1 ? 'Austragungsort' : 'Austragungsorte';
|
||||
const heading = venueLines.length === 1 ? this.t('pdfGenerator.venue') : this.t('pdfGenerator.venues');
|
||||
const maxWidth = 210 - this.margin * 2;
|
||||
if (y + 20 + venueLines.length * 6 > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
@@ -557,18 +610,18 @@ class PDFGenerator {
|
||||
}
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Hinweise:', this.margin, y);
|
||||
this.pdf.text(this.t('pdfGenerator.hints'), this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
const maxWidth = 210 - this.margin * 2;
|
||||
const bullets = [
|
||||
'Eine Stunde vor Beginn der Konkurrenz in der Halle sein',
|
||||
'Kein weißes Trikot',
|
||||
'Sportshorts (oder Sportröckchen), am besten auch nicht weiß',
|
||||
'Hallenschuhe (dürfen auf Boden nicht abfärben)',
|
||||
'Eine Flasche Wasser dabei haben',
|
||||
'Da der Verein die Meldung übernehmen möchte, die Trainer mind. eine Woche vor dem Turnier über die Teilnahme informieren',
|
||||
this.t('pdfGenerator.oneHourBefore'),
|
||||
this.t('pdfGenerator.noWhiteJersey'),
|
||||
this.t('pdfGenerator.sportShorts'),
|
||||
this.t('pdfGenerator.hallShoes'),
|
||||
this.t('pdfGenerator.waterBottle'),
|
||||
this.t('pdfGenerator.informTrainer'),
|
||||
];
|
||||
for (const b of bullets) {
|
||||
const lines = this.pdf.splitTextToSize(`- ${b}`, maxWidth);
|
||||
@@ -590,7 +643,7 @@ class PDFGenerator {
|
||||
} else {
|
||||
y += 6;
|
||||
}
|
||||
const finalLine = 'Die Trainer probieren bei allen Turnieren anwesend zu sein.';
|
||||
const finalLine = this.t('pdfGenerator.trainersPresent');
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
const finalLines = this.pdf.splitTextToSize(finalLine, maxWidth);
|
||||
@@ -611,7 +664,7 @@ class PDFGenerator {
|
||||
// Header
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(16);
|
||||
this.pdf.text(tournamentName || 'Turnier', this.margin, this.cursorY);
|
||||
this.pdf.text(tournamentName || this.t('pdfGenerator.tournament'), this.margin, this.cursorY);
|
||||
this.cursorY += 8;
|
||||
|
||||
if (tournamentDate) {
|
||||
@@ -622,7 +675,7 @@ class PDFGenerator {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
this.pdf.text(`Datum: ${formattedDate}`, this.margin, this.cursorY);
|
||||
this.pdf.text(`${this.t('pdfGenerator.date')} ${formattedDate}`, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
|
||||
@@ -834,7 +887,7 @@ class PDFGenerator {
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
let title = `Gruppe ${group.groupNumber}`;
|
||||
let title = `${this.t('pdfGenerator.groupLabel')} ${group.groupNumber}`;
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
@@ -844,7 +897,7 @@ class PDFGenerator {
|
||||
// Tabelle mit Rankings
|
||||
const rankings = groupRankings[group.groupId] || [];
|
||||
if (rankings.length > 0) {
|
||||
const head = [['Platz', 'Spieler', 'Punkte', 'Sätze', 'Diff']];
|
||||
const head = [[this.t('pdfGenerator.place'), this.t('pdfGenerator.player'), this.t('pdfGenerator.points'), this.t('pdfGenerator.sets'), this.t('pdfGenerator.diff')]];
|
||||
const body = rankings.map(p => [
|
||||
`${p.position}.`,
|
||||
(p.seeded ? '★ ' : '') + p.name,
|
||||
@@ -881,11 +934,11 @@ class PDFGenerator {
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Gesamt-Ranking (K.O.-Runden)', this.margin, this.cursorY);
|
||||
this.pdf.text(this.t('pdfGenerator.overallRanking'), this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Ranking-Tabelle
|
||||
const head = [['Platz', 'Spieler']];
|
||||
const head = [[this.t('pdfGenerator.place'), this.t('pdfGenerator.player')]];
|
||||
const body = knockoutRanking.map(entry => {
|
||||
const playerName = entry.member
|
||||
? `${entry.member.firstName || ''} ${entry.member.lastName || ''}`.trim()
|
||||
@@ -917,7 +970,7 @@ class PDFGenerator {
|
||||
this.cursorY = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Gruppen-Matrizen', this.margin, this.cursorY);
|
||||
this.pdf.text(this.t('pdfGenerator.groupMatrices'), this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Für jede Klasse
|
||||
@@ -937,7 +990,7 @@ class PDFGenerator {
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(11);
|
||||
let title = `Gruppe ${group.groupNumber}`;
|
||||
let title = `${this.t('pdfGenerator.groupLabel')} ${group.groupNumber}`;
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
@@ -986,7 +1039,7 @@ class PDFGenerator {
|
||||
this.cursorY = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Alle Spiele', this.margin, this.cursorY);
|
||||
this.pdf.text(this.t('pdfGenerator.allGames'), this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Für jede Klasse
|
||||
@@ -1013,7 +1066,7 @@ class PDFGenerator {
|
||||
this.cursorY += 7;
|
||||
|
||||
// Spiele-Tabelle
|
||||
const head = [['Runde', 'Spieler 1', 'Spieler 2', 'Ergebnis', 'Sätze']];
|
||||
const head = [[this.t('pdfGenerator.round'), this.t('pdfGenerator.player1'), this.t('pdfGenerator.player2'), this.t('pdfGenerator.result'), this.t('pdfGenerator.sets')]];
|
||||
const body = matches.map(m => [
|
||||
m.groupRound?.toString() || '-',
|
||||
getPlayerName(m.player1),
|
||||
@@ -1105,7 +1158,7 @@ class PDFGenerator {
|
||||
this.cursorY += 7;
|
||||
|
||||
// Spiele-Tabelle
|
||||
const head = [['Runde', 'Spieler 1', 'Spieler 2', 'Ergebnis', 'Sätze']];
|
||||
const head = [[this.t('pdfGenerator.round'), this.t('pdfGenerator.player1'), this.t('pdfGenerator.player2'), this.t('pdfGenerator.result'), this.t('pdfGenerator.sets')]];
|
||||
const body = classMatches.map(m => [
|
||||
m.round || '-',
|
||||
getPlayerName(m.player1),
|
||||
@@ -1452,7 +1505,7 @@ class PDFGenerator {
|
||||
if (sortedMembers.length === 0) {
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(11);
|
||||
this.pdf.text('Keine Aktivitäten für diesen Trainingstag erfasst.', this.margin, this.cursorY);
|
||||
this.pdf.text(this.t('pdfGenerator.noActivities'), this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Neues Mitglied hinzufügen"
|
||||
:title="$t('quickAddMember.title')"
|
||||
size="medium"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="quick-add-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="firstName">Vorname:</label>
|
||||
<label for="firstName">{{ $t('quickAddMember.firstName') }}:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
@@ -18,24 +18,24 @@
|
||||
@input="updateMember('firstName', $event.target.value)"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Vorname"
|
||||
:placeholder="$t('quickAddMember.firstName')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lastName">Nachname (optional):</label>
|
||||
<label for="lastName">{{ $t('quickAddMember.lastName') }}:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
:value="localMember.lastName"
|
||||
@input="updateMember('lastName', $event.target.value)"
|
||||
class="form-input"
|
||||
placeholder="Nachname"
|
||||
:placeholder="$t('members.lastName')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="birthDate">Geburtsdatum (optional):</label>
|
||||
<label for="birthDate">{{ $t('quickAddMember.birthDate') }}:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="birthDate"
|
||||
@@ -45,25 +45,25 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gender">Geschlecht:</label>
|
||||
<label for="gender">{{ $t('quickAddMember.gender') }}:</label>
|
||||
<select id="gender" :value="localMember.gender" @change="updateMember('gender', $event.target.value)" class="form-select">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
<option value="">{{ $t('quickAddMember.pleaseSelect') }}</option>
|
||||
<option value="male">{{ $t('members.genderMale') }}</option>
|
||||
<option value="female">{{ $t('members.genderFemale') }}</option>
|
||||
<option value="diverse">{{ $t('members.genderDiverse') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Abbrechen</button>
|
||||
<button class="btn-secondary" @click="handleClose">{{ $t('quickAddMember.cancel') }}</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="!isValid"
|
||||
>
|
||||
Erstellen & Hinzufügen
|
||||
{{ $t('quickAddMember.createAndAdd') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="season-selector">
|
||||
<label>
|
||||
<span>Saison:</span>
|
||||
<span>{{ t('seasonSelector.label') }}</span>
|
||||
<div class="season-input-group">
|
||||
<select v-model="selectedSeasonId" @change="onSeasonChange" class="season-select" :disabled="loading">
|
||||
<option value="">{{ loading ? 'Lade...' : 'Saison wählen...' }}</option>
|
||||
<option value="">{{ loading ? t('seasonSelector.loading') : t('seasonSelector.selectSeason') }}</option>
|
||||
<option v-for="season in seasons" :key="season.id" :value="season.id">
|
||||
{{ season.season }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="showNewSeasonForm = !showNewSeasonForm" class="btn-add-season" title="Neue Saison hinzufügen">
|
||||
<button @click="showNewSeasonForm = !showNewSeasonForm" class="btn-add-season" :title="t('seasonSelector.addSeason')">
|
||||
{{ showNewSeasonForm ? '✕' : '+' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -21,21 +21,21 @@
|
||||
|
||||
<div v-if="showNewSeasonForm" class="new-season-form">
|
||||
<label>
|
||||
<span>Neue Saison:</span>
|
||||
<span>{{ t('seasonSelector.newSeason') }}</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newSeasonString"
|
||||
placeholder="z.B. 2023/2024"
|
||||
:placeholder="t('seasonSelector.placeholder')"
|
||||
@keyup.enter="createSeason"
|
||||
class="season-input"
|
||||
>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button @click="createSeason" :disabled="!isValidSeasonFormat" class="btn-create">
|
||||
Erstellen
|
||||
{{ t('seasonSelector.create') }}
|
||||
</button>
|
||||
<button @click="cancelNewSeason" class="btn-cancel">
|
||||
Abbrechen
|
||||
{{ t('seasonSelector.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,6 +66,7 @@
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import apiClient from '../apiClient.js';
|
||||
|
||||
import InfoDialog from './InfoDialog.vue';
|
||||
@@ -89,6 +90,7 @@ export default {
|
||||
emits: ['update:modelValue', 'season-change'],
|
||||
setup(props, { emit }) {
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Dialog States
|
||||
const infoDialog = ref({
|
||||
@@ -139,7 +141,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Saisons:', err);
|
||||
error.value = 'Fehler beim Laden der Saisons';
|
||||
error.value = t('seasonSelector.errorLoading');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -171,9 +173,9 @@ export default {
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Erstellen der Saison:', err);
|
||||
if (err.response?.data?.error === 'alreadyexists') {
|
||||
showInfo('Hinweis', 'Diese Saison existiert bereits!', '', 'warning');
|
||||
showInfo(t('seasonSelector.hint'), t('seasonSelector.alreadyExists'), '', 'warning');
|
||||
} else {
|
||||
showInfo('Fehler', 'Fehler beim Erstellen der Saison', '', 'error');
|
||||
showInfo(t('common.error'), t('seasonSelector.errorCreating'), '', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -226,6 +228,7 @@ export default {
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
infoDialog,
|
||||
confirmDialog,
|
||||
showInfo,
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Tag-Historie ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
:title="`${$t('tagHistory.title')} ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="medium"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="tag-history-content">
|
||||
<div class="form-group">
|
||||
<label>Tags auswählen</label>
|
||||
<label>{{ $t('tagHistory.selectTags') }}</label>
|
||||
<multiselect
|
||||
v-model="localSelectedTags"
|
||||
:options="activityTags"
|
||||
placeholder="Tags auswählen"
|
||||
:placeholder="$t('tagHistory.selectTags')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
multiple
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-history">
|
||||
<em>Keine Tag-Historie vorhanden</em>
|
||||
<em>{{ $t('tagHistory.noHistory') }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Trainings-Details: ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
:title="`${$t('trainingDetails.title')}: ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="large"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="member" class="details-modal-content">
|
||||
<div class="member-info">
|
||||
<div class="info-item">
|
||||
<strong>Geburtsdatum:</strong> {{ formatBirthdate(member.birthDate) }}
|
||||
<strong>{{ $t('trainingDetails.birthdate') }}:</strong> {{ formatBirthdate(member.birthDate) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Geburtsjahr:</strong> {{ getBirthYear(member.birthDate) }}
|
||||
<strong>{{ $t('trainingDetails.birthYear') }}:</strong> {{ getBirthYear(member.birthDate) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participation-summary">
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 12 Monate:</span>
|
||||
<span class="label">{{ $t('trainingDetails.last12Months') }}:</span>
|
||||
<span class="value">{{ member.participation12Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 3 Monate:</span>
|
||||
<span class="label">{{ $t('trainingDetails.last3Months') }}:</span>
|
||||
<span class="value">{{ member.participation3Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Gesamt:</span>
|
||||
<span class="label">{{ $t('trainingDetails.total') }}:</span>
|
||||
<span class="value">{{ member.participationTotal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="training-details">
|
||||
<h4>Trainingsteilnahmen (absteigend sortiert)</h4>
|
||||
<h4>{{ $t('trainingDetails.trainingParticipations') }}</h4>
|
||||
<div class="training-list">
|
||||
<div
|
||||
v-for="training in member.trainingDetails"
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!member.trainingDetails || member.trainingDetails.length === 0" class="no-trainings">
|
||||
<em>Keine Trainingsteilnahmen vorhanden</em>
|
||||
<em>{{ $t('trainingDetails.noTrainings') }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<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>
|
||||
<h3>{{ $t('trainingGroupsTab.groups') }}</h3>
|
||||
<button @click="showAddGroupForm = true" class="btn-primary">{{ $t('trainingGroupsTab.newGroup') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Form -->
|
||||
@@ -11,12 +11,12 @@
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
:placeholder="$t('trainingGroupsTab.groupName')"
|
||||
@keyup.enter="createGroup"
|
||||
class="input-field"
|
||||
/>
|
||||
<button @click="createGroup" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="createGroup" class="btn-primary">{{ $t('trainingGroupsTab.create') }}</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">{{ $t('trainingGroupsTab.cancel') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
@@ -34,7 +34,7 @@
|
||||
v-if="!group.isPreset"
|
||||
@click="editGroup(group)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
:title="$t('trainingGroupsTab.edit')"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
@@ -42,7 +42,7 @@
|
||||
v-if="!group.isPreset"
|
||||
@click="deleteGroup(group)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
:title="$t('trainingGroupsTab.delete')"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<div class="group-members">
|
||||
<div class="members-count">
|
||||
{{ group.members ? group.members.length : 0 }} Mitglieder
|
||||
{{ group.members ? group.members.length : 0 }} {{ $t('trainingGroupsTab.members') }}
|
||||
</div>
|
||||
<div class="members-list">
|
||||
<span
|
||||
@@ -63,7 +63,7 @@
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id, member.id)"
|
||||
class="remove-member-btn"
|
||||
title="Entfernen"
|
||||
:title="$t('trainingGroupsTab.remove')"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -80,7 +80,7 @@
|
||||
:disabled="availableMembersForGroup(group.id).length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? 'Keine Mitglieder verfügbar' : 'Mitglied hinzufügen...' }}
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? $t('trainingGroupsTab.noMembersAvailable') : $t('trainingGroupsTab.addMember') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="member in availableMembersForGroup(group.id)"
|
||||
@@ -98,16 +98,16 @@
|
||||
<!-- Edit Group Dialog -->
|
||||
<div v-if="editingGroup" class="edit-group-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Gruppe bearbeiten</h3>
|
||||
<h3>{{ $t('trainingGroupsTab.editGroup') }}</h3>
|
||||
<input
|
||||
v-model="editingGroup.name"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
:placeholder="$t('trainingGroupsTab.groupName')"
|
||||
class="input-field"
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveGroupEdit" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="saveGroupEdit" class="btn-primary">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">{{ $t('trainingGroupsTab.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="training-times-tab">
|
||||
<div v-if="loading" class="loading">Lade Trainingszeiten...</div>
|
||||
<div v-if="loading" class="loading">{{ $t('trainingTimesTab.loading') }}</div>
|
||||
|
||||
<div v-else class="groups-section">
|
||||
<div
|
||||
@@ -14,7 +14,7 @@
|
||||
@click="showAddTimeForm(group.id)"
|
||||
class="btn-primary btn-small"
|
||||
>
|
||||
+ Zeit hinzufügen
|
||||
{{ $t('trainingTimesTab.addTime') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,19 +22,19 @@
|
||||
<div v-if="addTimeFormGroupId === group.id" class="add-time-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<span>Wochentag:</span>
|
||||
<span>{{ $t('trainingTimesTab.weekday') }}</span>
|
||||
<select v-model="newTime.weekday" class="input-field">
|
||||
<option :value="0">Sonntag</option>
|
||||
<option :value="1">Montag</option>
|
||||
<option :value="2">Dienstag</option>
|
||||
<option :value="3">Mittwoch</option>
|
||||
<option :value="4">Donnerstag</option>
|
||||
<option :value="5">Freitag</option>
|
||||
<option :value="6">Samstag</option>
|
||||
<option :value="0">{{ $t('trainingTimesTab.sunday') }}</option>
|
||||
<option :value="1">{{ $t('trainingTimesTab.monday') }}</option>
|
||||
<option :value="2">{{ $t('trainingTimesTab.tuesday') }}</option>
|
||||
<option :value="3">{{ $t('trainingTimesTab.wednesday') }}</option>
|
||||
<option :value="4">{{ $t('trainingTimesTab.thursday') }}</option>
|
||||
<option :value="5">{{ $t('trainingTimesTab.friday') }}</option>
|
||||
<option :value="6">{{ $t('trainingTimesTab.saturday') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Von:</span>
|
||||
<span>{{ $t('trainingTimesTab.from') }}</span>
|
||||
<input
|
||||
v-model="newTime.startTime"
|
||||
type="time"
|
||||
@@ -42,7 +42,7 @@
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis:</span>
|
||||
<span>{{ $t('trainingTimesTab.to') }}</span>
|
||||
<input
|
||||
v-model="newTime.endTime"
|
||||
type="time"
|
||||
@@ -51,8 +51,8 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="createTime(group.id)" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddTime" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="createTime(group.id)" class="btn-primary">{{ $t('trainingTimesTab.create') }}</button>
|
||||
<button @click="cancelAddTime" class="btn-secondary">{{ $t('trainingTimesTab.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,14 +71,14 @@
|
||||
<button
|
||||
@click="editTime(time)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
:title="$t('trainingTimesTab.edit')"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
@click="deleteTime(time.id)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
:title="$t('trainingTimesTab.delete')"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-times">
|
||||
Keine Trainingszeiten definiert
|
||||
{{ $t('trainingTimesTab.noTimes') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,22 +94,22 @@
|
||||
<!-- Edit Time Dialog -->
|
||||
<div v-if="editingTime" class="edit-time-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Trainingszeit bearbeiten</h3>
|
||||
<h3>{{ $t('trainingTimesTab.editTime') }}</h3>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<span>Wochentag:</span>
|
||||
<span>{{ $t('trainingTimesTab.weekday') }}</span>
|
||||
<select v-model="editingTime.weekday" class="input-field">
|
||||
<option :value="0">Sonntag</option>
|
||||
<option :value="1">Montag</option>
|
||||
<option :value="2">Dienstag</option>
|
||||
<option :value="3">Mittwoch</option>
|
||||
<option :value="4">Donnerstag</option>
|
||||
<option :value="5">Freitag</option>
|
||||
<option :value="6">Samstag</option>
|
||||
<option :value="0">{{ $t('trainingTimesTab.sunday') }}</option>
|
||||
<option :value="1">{{ $t('trainingTimesTab.monday') }}</option>
|
||||
<option :value="2">{{ $t('trainingTimesTab.tuesday') }}</option>
|
||||
<option :value="3">{{ $t('trainingTimesTab.wednesday') }}</option>
|
||||
<option :value="4">{{ $t('trainingTimesTab.thursday') }}</option>
|
||||
<option :value="5">{{ $t('trainingTimesTab.friday') }}</option>
|
||||
<option :value="6">{{ $t('trainingTimesTab.saturday') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Von:</span>
|
||||
<span>{{ $t('trainingTimesTab.from') }}</span>
|
||||
<input
|
||||
v-model="editingTime.startTime"
|
||||
type="time"
|
||||
@@ -126,8 +126,8 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="saveTime" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelEdit" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="saveTime" class="btn-primary">{{ $t('trainingTimesTab.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-secondary">{{ $t('trainingTimesTab.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +245,7 @@ export default {
|
||||
await this.loadTrainingTimes();
|
||||
} catch (error) {
|
||||
console.error('[saveTime] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Speichern der Trainingszeit');
|
||||
const msg = getSafeErrorMessage(error, this.$t('trainingTimesTab.saveError'));
|
||||
alert(msg);
|
||||
}
|
||||
},
|
||||
|
||||
167
frontend/src/i18n/index.js
Normal file
167
frontend/src/i18n/index.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { safeLocalStorage } from '../utils/storage.js';
|
||||
|
||||
// Import all locale files
|
||||
import de from './locales/de.json';
|
||||
import enGB from './locales/en-GB.json';
|
||||
import enUS from './locales/en-US.json';
|
||||
import enAU from './locales/en-AU.json';
|
||||
import deCH from './locales/de-CH.json';
|
||||
import fr from './locales/fr.json';
|
||||
import es from './locales/es.json';
|
||||
import it from './locales/it.json';
|
||||
import pl from './locales/pl.json';
|
||||
import ja from './locales/ja.json';
|
||||
import zh from './locales/zh.json';
|
||||
import tl from './locales/tl.json';
|
||||
import th from './locales/th.json';
|
||||
import fil from './locales/fil.json';
|
||||
|
||||
// Language mapping for browser language detection
|
||||
const languageMap = {
|
||||
'de': 'de',
|
||||
'de-DE': 'de',
|
||||
'de-AT': 'de',
|
||||
'en': 'en-GB', // Default to British English
|
||||
'en-GB': 'en-GB',
|
||||
'en-US': 'en-US',
|
||||
'en-AU': 'en-AU',
|
||||
'de-CH': 'de-CH',
|
||||
'fr': 'fr',
|
||||
'fr-FR': 'fr',
|
||||
'es': 'es',
|
||||
'es-ES': 'es',
|
||||
'it': 'it',
|
||||
'it-IT': 'it',
|
||||
'pl': 'pl',
|
||||
'pl-PL': 'pl',
|
||||
'ja': 'ja',
|
||||
'ja-JP': 'ja',
|
||||
'zh': 'zh',
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'zh',
|
||||
'tl': 'tl',
|
||||
'th': 'th',
|
||||
'th-TH': 'th',
|
||||
'fil': 'fil',
|
||||
'fil-PH': 'fil',
|
||||
};
|
||||
|
||||
// Detect browser language
|
||||
function detectBrowserLanguage() {
|
||||
const browserLang = navigator.language || navigator.userLanguage;
|
||||
const langCode = browserLang.split('-')[0];
|
||||
const fullLang = browserLang;
|
||||
|
||||
// Check for exact match first
|
||||
if (languageMap[fullLang]) {
|
||||
return languageMap[fullLang];
|
||||
}
|
||||
|
||||
// Check for language code match
|
||||
if (languageMap[langCode]) {
|
||||
return languageMap[langCode];
|
||||
}
|
||||
|
||||
// Default to German
|
||||
return 'de';
|
||||
}
|
||||
|
||||
// Get saved language or detect from browser
|
||||
function getInitialLocale() {
|
||||
// Prüfe zuerst URL-Parameter für manuelles Setzen (für Tests)
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const langParam = urlParams.get('lang');
|
||||
if (langParam && languageMap[langParam]) {
|
||||
// Speichere die Sprache aus URL-Parameter
|
||||
safeLocalStorage.setItem('userLanguage', languageMap[langParam]);
|
||||
return languageMap[langParam];
|
||||
}
|
||||
}
|
||||
|
||||
const saved = safeLocalStorage.getItem('userLanguage');
|
||||
if (saved && languageMap[saved]) {
|
||||
return saved;
|
||||
}
|
||||
return detectBrowserLanguage();
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: getInitialLocale(),
|
||||
fallbackLocale: 'de',
|
||||
messages: {
|
||||
'de': de,
|
||||
'en-GB': enGB,
|
||||
'en-US': enUS,
|
||||
'en-AU': enAU,
|
||||
'de-CH': deCH,
|
||||
'fr': fr,
|
||||
'es': es,
|
||||
'it': it,
|
||||
'pl': pl,
|
||||
'ja': ja,
|
||||
'zh': zh,
|
||||
'tl': tl,
|
||||
'th': th,
|
||||
'fil': fil,
|
||||
},
|
||||
legacy: true, // Use Options API mode (required for $t in templates with Options API)
|
||||
// globalInjection is not needed with legacy: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Setzt die Sprache manuell (für Tests)
|
||||
* @param {string} locale - Sprachcode (z.B. 'de', 'en-GB', 'fr')
|
||||
* @returns {boolean} - true wenn erfolgreich, false wenn ungültiger Code
|
||||
*/
|
||||
export function setLanguage(locale) {
|
||||
if (!languageMap[locale]) {
|
||||
console.warn(`Ungültiger Sprachcode: ${locale}. Verfügbare Sprachen:`, Object.keys(languageMap));
|
||||
return false;
|
||||
}
|
||||
|
||||
const mappedLocale = languageMap[locale];
|
||||
// Bei legacy: true ist locale direkt ein String
|
||||
i18n.global.locale = mappedLocale;
|
||||
safeLocalStorage.setItem('userLanguage', mappedLocale);
|
||||
console.log(`✅ Sprache geändert zu: ${mappedLocale}`);
|
||||
|
||||
// Seite neu laden, damit alle Komponenten die neue Sprache verwenden
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.reload();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Sprache zurück
|
||||
* @returns {string} - Aktueller Sprachcode
|
||||
*/
|
||||
export function getCurrentLanguage() {
|
||||
return i18n.global.locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle verfügbaren Sprachen zurück
|
||||
* @returns {string[]} - Array von Sprachcodes
|
||||
*/
|
||||
export function getAvailableLanguages() {
|
||||
return Object.keys(languageMap);
|
||||
}
|
||||
|
||||
// Exponiere setLanguage global für Browser-Konsole (nur im Development)
|
||||
if (typeof window !== 'undefined' && (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev')) {
|
||||
window.setLanguage = setLanguage;
|
||||
window.getCurrentLanguage = getCurrentLanguage;
|
||||
window.getAvailableLanguages = getAvailableLanguages;
|
||||
console.log('🌐 Sprache-Test-Funktionen verfügbar:');
|
||||
console.log(' - setLanguage("de") - Sprache ändern');
|
||||
console.log(' - getCurrentLanguage() - Aktuelle Sprache abrufen');
|
||||
console.log(' - getAvailableLanguages() - Verfügbare Sprachen anzeigen');
|
||||
console.log(' - Oder URL-Parameter verwenden: ?lang=de');
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
102
frontend/src/i18n/locales/de-CH.json
Normal file
102
frontend/src/i18n/locales/de-CH.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Trainingstagebuch",
|
||||
"title": "Trainingstagebuch"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Lade...",
|
||||
"save": "Speichere",
|
||||
"cancel": "Abbreche",
|
||||
"delete": "Lösche",
|
||||
"edit": "Bearbeite",
|
||||
"add": "Hinzufüge",
|
||||
"close": "Schliesse",
|
||||
"confirm": "Bestätige",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
"search": "Suche",
|
||||
"filter": "Filter",
|
||||
"actions": "Aktione",
|
||||
"back": "Zrugg",
|
||||
"next": "Wiiter",
|
||||
"previous": "Zrugg",
|
||||
"submit": "Abschicke",
|
||||
"reset": "Zruggsetze"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
"members": "Mitglider",
|
||||
"diary": "Tagebuech",
|
||||
"approvals": "Freigabe",
|
||||
"statistics": "Trainings-Statistik",
|
||||
"tournaments": "Turnier",
|
||||
"schedule": "Spielplän",
|
||||
"clubSettings": "Vereinsiistellige",
|
||||
"predefinedActivities": "Vordefinierte Aktivitäte",
|
||||
"teamManagement": "Team-Verwaltig",
|
||||
"permissions": "Berechtigunge",
|
||||
"logs": "System-Logs",
|
||||
"memberTransfer": "Mitgliderübertragig",
|
||||
"myTischtennisAccount": "myTischtennis-Account",
|
||||
"personalSettings": "Persönlichi Iistellige",
|
||||
"logout": "Abmelde",
|
||||
"login": "Aamelde",
|
||||
"register": "Registriere",
|
||||
"dailyBusiness": "Tagesgschäft",
|
||||
"competitions": "Wettbewerb",
|
||||
"settings": "Iistellige"
|
||||
},
|
||||
"club": {
|
||||
"select": "Verein uswähle",
|
||||
"selectPlaceholder": "Verein wähle...",
|
||||
"new": "Neue Verein",
|
||||
"load": "Lade",
|
||||
"name": "Vereinsname",
|
||||
"create": "Verein erstelle"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Aamelde",
|
||||
"logout": "Abmelde",
|
||||
"register": "Registriere",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"forgotPassword": "Passwort vergesse?",
|
||||
"rememberMe": "Aagmeldet bliibe",
|
||||
"loginSuccess": "Erfolgriich aagmeldet",
|
||||
"logoutSuccess": "Erfolgriich abgmeldet",
|
||||
"sessionExpired": "Dini Sitzig isch abglaufe. Du wirsch abgmeldet."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Iistellige",
|
||||
"personalSettings": "Persönlichi Iistellige",
|
||||
"language": "Sproch",
|
||||
"languageDescription": "Wähl dini bevorzugti Sproch für d Aawendig",
|
||||
"languageChanged": "Sproch erfolgriich gänderet",
|
||||
"selectLanguage": "Sproch uswähle"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Dütsch)",
|
||||
"en-GB": "English (Englisch (GB))",
|
||||
"en-US": "English (Englisch (US))",
|
||||
"en-AU": "English (Englisch (AU))",
|
||||
"de-CH": "Schwiizerdütsch",
|
||||
"fr": "Français (Französisch)",
|
||||
"es": "Español (Spanisch)",
|
||||
"it": "Italiano (Italienisch)",
|
||||
"pl": "Polski (Polnisch)",
|
||||
"ja": "日本語 (Japanisch)",
|
||||
"zh": "中文 (Chinesisch)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Erfolg",
|
||||
"error": "Fehler",
|
||||
"warning": "Warnig",
|
||||
"info": "Information",
|
||||
"confirm": "Bestätige",
|
||||
"cancel": "Abbreche"
|
||||
}
|
||||
}
|
||||
|
||||
361
frontend/src/i18n/locales/de-extended.json
Normal file
361
frontend/src/i18n/locales/de-extended.json
Normal file
@@ -0,0 +1,361 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Trainingstagebuch",
|
||||
"title": "Trainingstagebuch"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Lade...",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"add": "Hinzufügen",
|
||||
"close": "Schließen",
|
||||
"confirm": "Bestätigen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"search": "Suchen",
|
||||
"filter": "Filter",
|
||||
"actions": "Aktionen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"previous": "Zurück",
|
||||
"submit": "Absenden",
|
||||
"reset": "Zurücksetzen",
|
||||
"all": "Alle",
|
||||
"today": "Heute",
|
||||
"new": "Neu",
|
||||
"update": "Aktualisieren",
|
||||
"create": "Erstellen",
|
||||
"remove": "Entfernen",
|
||||
"select": "Auswählen",
|
||||
"choose": "Wählen",
|
||||
"apply": "Anwenden",
|
||||
"clear": "Löschen",
|
||||
"details": "Details",
|
||||
"view": "Anzeigen",
|
||||
"name": "Name",
|
||||
"date": "Datum",
|
||||
"time": "Zeit",
|
||||
"status": "Status",
|
||||
"type": "Typ",
|
||||
"description": "Beschreibung",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"required": "Erforderlich",
|
||||
"optional": "Optional",
|
||||
"min": "Min",
|
||||
"minutes": "Minuten",
|
||||
"hours": "Stunden",
|
||||
"days": "Tage",
|
||||
"weeks": "Wochen",
|
||||
"months": "Monate",
|
||||
"years": "Jahre"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
"members": "Mitglieder",
|
||||
"diary": "Tagebuch",
|
||||
"approvals": "Freigaben",
|
||||
"statistics": "Trainings-Statistik",
|
||||
"tournaments": "Turniere",
|
||||
"schedule": "Spielpläne",
|
||||
"clubSettings": "Vereinseinstellungen",
|
||||
"predefinedActivities": "Vordefinierte Aktivitäten",
|
||||
"teamManagement": "Team-Verwaltung",
|
||||
"permissions": "Berechtigungen",
|
||||
"logs": "System-Logs",
|
||||
"memberTransfer": "Mitgliederübertragung",
|
||||
"myTischtennisAccount": "myTischtennis-Account",
|
||||
"personalSettings": "Persönliche Einstellungen",
|
||||
"logout": "Ausloggen",
|
||||
"login": "Einloggen",
|
||||
"register": "Registrieren",
|
||||
"dailyBusiness": "Tagesgeschäft",
|
||||
"competitions": "Wettbewerbe",
|
||||
"settings": "Einstellungen",
|
||||
"backToHome": "Zur Startseite"
|
||||
},
|
||||
"club": {
|
||||
"select": "Verein auswählen",
|
||||
"selectPlaceholder": "Verein wählen...",
|
||||
"new": "Neuer Verein",
|
||||
"load": "Laden",
|
||||
"name": "Vereinsname",
|
||||
"create": "Verein erstellen",
|
||||
"createTitle": "Verein anlegen",
|
||||
"members": "Mitglieder",
|
||||
"trainingDiary": "Trainingstagebuch",
|
||||
"noAccess": "Für diesen Verein wurde Dir noch kein Zugriff gestattet.",
|
||||
"requestAccess": "Zugriff beantragen",
|
||||
"openRequests": "Offene Anfragen auf Zugriff"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Einloggen",
|
||||
"logout": "Ausloggen",
|
||||
"register": "Registrieren",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"rememberMe": "Angemeldet bleiben",
|
||||
"loginSuccess": "Erfolgreich eingeloggt",
|
||||
"logoutSuccess": "Erfolgreich ausgeloggt",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Du wirst abgemeldet.",
|
||||
"noAccount": "Noch kein Konto?",
|
||||
"hasAccount": "Bereits ein Konto?",
|
||||
"toLogin": "Zum Login",
|
||||
"loginFailed": "Login fehlgeschlagen. Bitte Zugangsdaten prüfen und erneut versuchen.",
|
||||
"registerSuccess": "Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mails, um den Account zu aktivieren.",
|
||||
"registerFailed": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"activate": "Aktivieren"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"personalSettings": "Persönliche Einstellungen",
|
||||
"language": "Sprache",
|
||||
"languageDescription": "Wähle deine bevorzugte Sprache für die Anwendung",
|
||||
"languageChanged": "Sprache erfolgreich geändert",
|
||||
"selectLanguage": "Sprache auswählen"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch",
|
||||
"en-GB": "Englisch (GB)",
|
||||
"en-US": "Englisch (US)",
|
||||
"en-AU": "Englisch (AU)",
|
||||
"de-CH": "Schweizer Deutsch",
|
||||
"fr": "Französisch",
|
||||
"es": "Spanisch",
|
||||
"it": "Italienisch",
|
||||
"pl": "Polnisch",
|
||||
"ja": "Japanisch",
|
||||
"zh": "Chinesisch",
|
||||
"tl": "Tagalog",
|
||||
"th": "Thai",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Erfolg",
|
||||
"error": "Fehler",
|
||||
"warning": "Warnung",
|
||||
"info": "Information",
|
||||
"confirm": "Bestätigen",
|
||||
"cancel": "Abbrechen",
|
||||
"successfullyLoaded": "Erfolgreich geladen",
|
||||
"recordsFound": "Datensätze gefunden",
|
||||
"recordsDisplayed": "angezeigt"
|
||||
},
|
||||
"members": {
|
||||
"title": "Mitglieder",
|
||||
"memberInfo": "Mitglieder-Info",
|
||||
"activeMembers": "Aktive Mitglieder",
|
||||
"testMembers": "Testmitglieder",
|
||||
"inactiveMembers": "Inaktive Mitglieder",
|
||||
"generatePhoneList": "Telefonliste generieren",
|
||||
"onlyActiveMembers": "Es werden nur aktive Mitglieder ausgegeben",
|
||||
"updateRatings": "TTR/QTTR von myTischtennis aktualisieren",
|
||||
"updating": "Aktualisiere...",
|
||||
"transferMembers": "Mitglieder übertragen",
|
||||
"newMember": "Neues Mitglied",
|
||||
"editMember": "Mitglied bearbeiten",
|
||||
"createNewMember": "Neues Mitglied anlegen",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"street": "Straße",
|
||||
"postalCode": "PLZ",
|
||||
"city": "Ort",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"phones": "Telefonnummern",
|
||||
"emails": "E-Mail-Adressen",
|
||||
"addPhone": "Telefonnummer hinzufügen",
|
||||
"addEmail": "E-Mail-Adresse hinzufügen",
|
||||
"phoneNumber": "Telefonnummer",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"parent": "Elternteil",
|
||||
"parentName": "Name (z.B. Mutter, Vater)",
|
||||
"primary": "Primär",
|
||||
"gender": "Geschlecht",
|
||||
"genderUnknown": "Unbekannt",
|
||||
"genderMale": "Männlich",
|
||||
"genderFemale": "Weiblich",
|
||||
"genderDiverse": "Divers",
|
||||
"picsInInternetAllowed": "Pics in Internet erlaubt",
|
||||
"testMembership": "Testmitgliedschaft",
|
||||
"memberFormHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"trainingGroups": "Trainingsgruppen"
|
||||
},
|
||||
"diary": {
|
||||
"title": "Trainingstagebuch",
|
||||
"date": "Datum",
|
||||
"noEntries": "Keine Einträge",
|
||||
"deleteDate": "Datum löschen",
|
||||
"createNew": "Neu anlegen",
|
||||
"gallery": "Mitglieder-Galerie",
|
||||
"galleryCreating": "Galerie wird erstellt…",
|
||||
"selectTrainingGroup": "Trainingsgruppe auswählen",
|
||||
"selectTrainingGroupPlaceholder": "Bitte wählen...",
|
||||
"suggestion": "Vorschlag",
|
||||
"nextAppointment": "Nächster Termin",
|
||||
"applySuggestion": "Vorschlag übernehmen",
|
||||
"skipSuggestion": "Ohne Vorschlag fortfahren",
|
||||
"createNewDate": "Neues Datum anlegen",
|
||||
"trainingStart": "Trainingsbeginn",
|
||||
"trainingEnd": "Trainingsende",
|
||||
"createDate": "Datum anlegen",
|
||||
"editTrainingTimes": "Trainingszeiten bearbeiten",
|
||||
"updateTimes": "Zeiten aktualisieren",
|
||||
"groupManagement": "Gruppenverwaltung",
|
||||
"createGroups": "Gruppen erstellen",
|
||||
"trainingPlan": "Trainingsplan",
|
||||
"startTime": "Startzeit",
|
||||
"group": "Gruppe",
|
||||
"timeblock": "Zeitblock",
|
||||
"assignParticipants": "Teilnehmer zuordnen",
|
||||
"addTimeblock": "Zeitblock",
|
||||
"activities": "Aktivitäten",
|
||||
"addActivity": "Aktivität hinzufügen",
|
||||
"bookAccident": "Unfall buchen",
|
||||
"activity": "Aktivität",
|
||||
"duration": "Dauer",
|
||||
"activityImage": "Aktivitätsbild",
|
||||
"activityDrawing": "Aktivitätszeichnung"
|
||||
},
|
||||
"home": {
|
||||
"welcome": "Willkommen im TrainingsTagebuch",
|
||||
"heroTitle": "Vereinsverwaltung, Trainingsplanung und Turniere – alles an einem Ort",
|
||||
"heroSubtitle": "Das TrainingsTagebuch ist die umfassende Lösung für Vereine: Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten, Trainingstagebuch, Turnierorganisation, Team-Management, MyTischtennis-Integration, Statistiken und mehr – DSGVO‑konform und einfach zu bedienen.",
|
||||
"startFree": "Kostenlos starten",
|
||||
"features": {
|
||||
"memberManagement": "Mitglieder- und Gruppenverwaltung",
|
||||
"trainingGroups": "Trainingsgruppen & Trainingszeiten",
|
||||
"trainingDiary": "Trainingstagebuch & Dokumentation",
|
||||
"tournaments": "Turniere (intern, offen, offiziell)",
|
||||
"teamManagement": "Team-Management & Ligen",
|
||||
"myTischtennis": "MyTischtennis-Integration",
|
||||
"statistics": "Statistiken & Auswertungen"
|
||||
},
|
||||
"howItWorks": "So funktioniert es",
|
||||
"step1": {
|
||||
"title": "Registrieren",
|
||||
"description": "Lege kostenlos einen Account an und aktiviere ihn per E‑Mail."
|
||||
},
|
||||
"step2": {
|
||||
"title": "Verein anlegen",
|
||||
"description": "Erstelle deinen Verein, lade Mitglieder ein und richte Gruppen ein."
|
||||
},
|
||||
"step3": {
|
||||
"title": "Planen & dokumentieren",
|
||||
"description": "Plane Termine, dokumentiere Trainings und verfolge Fortschritte."
|
||||
},
|
||||
"forWhom": "Für wen ist das TrainingsTagebuch?",
|
||||
"forWhomText": "Das TrainingsTagebuch ist die umfassende Plattform für Vereine, Abteilungen und Trainerteams. Es vereint Mitgliederverwaltung mit Trainingsgruppen und -zeiten, detailliertes Trainingstagebuch, umfassende Turnierorganisation (interne, offene und offizielle Turniere), Team-Management mit Liga-Integration, MyTischtennis-Synchronisation, aussagekräftige Statistiken und Auswertungen sowie ein flexibles Berechtigungssystem in einer modernen Web‑Anwendung. Durch klare Rollen (Admin, Trainer, Mannschaftsführer, Mitglied) und individuelle Berechtigungen behalten Verantwortliche die Kontrolle, während Mitglieder selbstbestimmt mitwirken können. Ideal für Mannschafts‑, Racket‑ und Individualsportarten – vom Nachwuchs bis zum Leistungsbereich. DSGVO‑konform mit transparenten Freigaben und vollständigem Aktivitätsprotokoll.",
|
||||
"faq": "Häufige Fragen",
|
||||
"faqFree": {
|
||||
"question": "Ist die Nutzung kostenlos?",
|
||||
"answer": "Ja, du kannst kostenlos starten. Erweiterungen können später folgen."
|
||||
},
|
||||
"faqPrivacy": {
|
||||
"question": "Wie steht es um den Datenschutz?",
|
||||
"answer": "Wir setzen auf Datensparsamkeit, transparente Freigaben, rollenbasierte Zugriffe und vollständiges Aktivitätsprotokoll. Die Anwendung ist DSGVO‑konform."
|
||||
}
|
||||
},
|
||||
"tournaments": {
|
||||
"title": "Turniere",
|
||||
"tournamentName": "Turniername",
|
||||
"events": "Veranstaltungen",
|
||||
"participations": "Turnierbeteiligungen",
|
||||
"showEvents": "Gespeicherte Veranstaltungen anzeigen",
|
||||
"showParticipations": "Turnierbeteiligungen anzeigen"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "Berechtigungsverwaltung",
|
||||
"clubMembers": "Clubmitglieder",
|
||||
"email": "Email",
|
||||
"role": "Rolle",
|
||||
"status": "Status",
|
||||
"resetResource": "Zurücksetzen",
|
||||
"resetAll": "Alle zurücksetzen",
|
||||
"close": "Abbrechen",
|
||||
"save": "Speichern"
|
||||
},
|
||||
"logs": {
|
||||
"title": "System-Logs",
|
||||
"applyFilters": "Filter anwenden",
|
||||
"clearFilters": "Zurücksetzen",
|
||||
"time": "Zeit",
|
||||
"type": "Typ",
|
||||
"method": "Methode",
|
||||
"path": "Pfad",
|
||||
"status": "Status",
|
||||
"executionTime": "Ausführungszeit",
|
||||
"error": "Fehler",
|
||||
"viewDetails": "Details"
|
||||
},
|
||||
"myTischtennis": {
|
||||
"title": "myTischtennis-Account",
|
||||
"linkedAccount": "Verknüpfter Account",
|
||||
"passwordSaved": "Passwort gespeichert",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"club": "Verein (myTischtennis)",
|
||||
"lastSuccessfulLogin": "Letzter erfolgreicher Login",
|
||||
"lastLoginAttempt": "Letzter Login-Versuch",
|
||||
"lastFetch": "Letzter Abruf",
|
||||
"autoUpdates": "Automatische Updates",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"editAccount": "Account bearbeiten",
|
||||
"testLogin": "Erneut einloggen",
|
||||
"updateHistory": "Update-History",
|
||||
"deleteAccount": "Account trennen",
|
||||
"fetchStatistics": "Datenabruf-Statistiken",
|
||||
"loadingStats": "Lade Statistiken...",
|
||||
"playerRatings": "Spielerwertungen",
|
||||
"matchResults": "Spielergebnisse",
|
||||
"leagueTables": "Ligatabellen",
|
||||
"neverFetched": "Noch nie abgerufen",
|
||||
"refreshStats": "Statistiken aktualisieren",
|
||||
"noAccount": "Kein myTischtennis-Account verknüpft.",
|
||||
"linkAccount": "Account verknüpfen",
|
||||
"about": "Über myTischtennis",
|
||||
"aboutText": "Durch die Verknüpfung Ihres myTischtennis-Accounts können Sie:",
|
||||
"aboutFeatures": {
|
||||
"import": "Automatisch Turnierdaten importieren",
|
||||
"sync": "Spielerergebnisse synchronisieren",
|
||||
"fetch": "Wettkampfdaten direkt abrufen"
|
||||
},
|
||||
"passwordNote": "Hinweis: Das Speichern des Passworts ist optional. Wenn Sie es nicht speichern, werden Sie bei jeder Synchronisation nach dem Passwort gefragt.",
|
||||
"passwordsEncrypted": "Passwörter werden verschlüsselt gespeichert"
|
||||
},
|
||||
"memberTransfer": {
|
||||
"title": "Mitgliederübertragung",
|
||||
"transferConfig": "Übertragungskonfiguration"
|
||||
},
|
||||
"imprint": {
|
||||
"title": "Impressum",
|
||||
"serviceProvider": "Diensteanbieter",
|
||||
"contact": "Kontakt"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Datenschutzerklärung",
|
||||
"responsible": "Verantwortlicher",
|
||||
"purposes": "Zwecke und Rechtsgrundlagen der Verarbeitung",
|
||||
"website": "Bereitstellung der Website",
|
||||
"usage": "Nutzung des TrainingsTagebuchs",
|
||||
"consent": "Einwilligungsbasierte Vorgänge",
|
||||
"dataCategories": "Kategorien personenbezogener Daten",
|
||||
"usageData": "Nutzungsdaten",
|
||||
"registrationData": "Registrierungs-/Profildaten",
|
||||
"clubData": "Vereins-/Aktivitätsdaten",
|
||||
"memberData": "Mitgliederdaten",
|
||||
"trainingData": "Trainingsdaten",
|
||||
"tournamentData": "Turnierdaten",
|
||||
"myTischtennisData": "MyTischtennis-Daten",
|
||||
"cookies": "Cookies/Local Storage",
|
||||
"logData": "Logdaten",
|
||||
"recipients": "Empfänger"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1663
frontend/src/i18n/locales/de.json
Normal file
1663
frontend/src/i18n/locales/de.json
Normal file
File diff suppressed because it is too large
Load Diff
102
frontend/src/i18n/locales/de.json.backup
Normal file
102
frontend/src/i18n/locales/de.json.backup
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Trainingstagebuch",
|
||||
"title": "Trainingstagebuch"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Lade...",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"add": "Hinzufügen",
|
||||
"close": "Schließen",
|
||||
"confirm": "Bestätigen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"search": "Suchen",
|
||||
"filter": "Filter",
|
||||
"actions": "Aktionen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"previous": "Zurück",
|
||||
"submit": "Absenden",
|
||||
"reset": "Zurücksetzen"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
"members": "Mitglieder",
|
||||
"diary": "Tagebuch",
|
||||
"approvals": "Freigaben",
|
||||
"statistics": "Trainings-Statistik",
|
||||
"tournaments": "Turniere",
|
||||
"schedule": "Spielpläne",
|
||||
"clubSettings": "Vereinseinstellungen",
|
||||
"predefinedActivities": "Vordefinierte Aktivitäten",
|
||||
"teamManagement": "Team-Verwaltung",
|
||||
"permissions": "Berechtigungen",
|
||||
"logs": "System-Logs",
|
||||
"memberTransfer": "Mitgliederübertragung",
|
||||
"myTischtennisAccount": "myTischtennis-Account",
|
||||
"personalSettings": "Persönliche Einstellungen",
|
||||
"logout": "Ausloggen",
|
||||
"login": "Einloggen",
|
||||
"register": "Registrieren",
|
||||
"dailyBusiness": "Tagesgeschäft",
|
||||
"competitions": "Wettbewerbe",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"club": {
|
||||
"select": "Verein auswählen",
|
||||
"selectPlaceholder": "Verein wählen...",
|
||||
"new": "Neuer Verein",
|
||||
"load": "Laden",
|
||||
"name": "Vereinsname",
|
||||
"create": "Verein erstellen"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Einloggen",
|
||||
"logout": "Ausloggen",
|
||||
"register": "Registrieren",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"rememberMe": "Angemeldet bleiben",
|
||||
"loginSuccess": "Erfolgreich eingeloggt",
|
||||
"logoutSuccess": "Erfolgreich ausgeloggt",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Du wirst abgemeldet."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"personalSettings": "Persönliche Einstellungen",
|
||||
"language": "Sprache",
|
||||
"languageDescription": "Wähle deine bevorzugte Sprache für die Anwendung",
|
||||
"languageChanged": "Sprache erfolgreich geändert",
|
||||
"selectLanguage": "Sprache auswählen"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch",
|
||||
"en-GB": "Englisch (GB)",
|
||||
"en-US": "Englisch (US)",
|
||||
"en-AU": "Englisch (AU)",
|
||||
"de-CH": "Schweizer Deutsch",
|
||||
"fr": "Französisch",
|
||||
"es": "Spanisch",
|
||||
"it": "Italienisch",
|
||||
"pl": "Polnisch",
|
||||
"ja": "Japanisch",
|
||||
"zh": "Chinesisch",
|
||||
"tl": "Tagalog",
|
||||
"th": "Thai",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Erfolg",
|
||||
"error": "Fehler",
|
||||
"warning": "Warnung",
|
||||
"info": "Information",
|
||||
"confirm": "Bestätigen",
|
||||
"cancel": "Abbrechen"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/en-AU.json
Normal file
102
frontend/src/i18n/locales/en-AU.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Training Diary",
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"actions": "Actions",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
"diary": "Diary",
|
||||
"approvals": "Approvals",
|
||||
"statistics": "Training Statistics",
|
||||
"tournaments": "Tournaments",
|
||||
"schedule": "Schedules",
|
||||
"clubSettings": "Club Settings",
|
||||
"predefinedActivities": "Predefined Activities",
|
||||
"teamManagement": "Team Management",
|
||||
"permissions": "Permissions",
|
||||
"logs": "System Logs",
|
||||
"memberTransfer": "Member Transfer",
|
||||
"myTischtennisAccount": "myTischtennis Account",
|
||||
"personalSettings": "Personal Settings",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"dailyBusiness": "Daily Business",
|
||||
"competitions": "Competitions",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"club": {
|
||||
"select": "Select Club",
|
||||
"selectPlaceholder": "Choose club...",
|
||||
"new": "New Club",
|
||||
"load": "Load",
|
||||
"name": "Club Name",
|
||||
"create": "Create Club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Register",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"rememberMe": "Remember me",
|
||||
"loginSuccess": "Successfully logged in",
|
||||
"logoutSuccess": "Successfully logged out",
|
||||
"sessionExpired": "Your session has expired. You will be logged out."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"personalSettings": "Personal Settings",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your preferred language for the application",
|
||||
"languageChanged": "Language successfully changed",
|
||||
"selectLanguage": "Select Language"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (German)",
|
||||
"en-GB": "English (British English)",
|
||||
"en-US": "English (US English)",
|
||||
"en-AU": "English",
|
||||
"de-CH": "Schwiizerdütsch (Swiss German)",
|
||||
"fr": "Français (French)",
|
||||
"es": "Español (Spanish)",
|
||||
"it": "Italiano (Italian)",
|
||||
"pl": "Polski (Polish)",
|
||||
"ja": "日本語 (Japanese)",
|
||||
"zh": "中文 (Chinese)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Information",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/en-GB.json
Normal file
102
frontend/src/i18n/locales/en-GB.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Training Diary",
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"actions": "Actions",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
"diary": "Diary",
|
||||
"approvals": "Approvals",
|
||||
"statistics": "Training Statistics",
|
||||
"tournaments": "Tournaments",
|
||||
"schedule": "Schedules",
|
||||
"clubSettings": "Club Settings",
|
||||
"predefinedActivities": "Predefined Activities",
|
||||
"teamManagement": "Team Management",
|
||||
"permissions": "Permissions",
|
||||
"logs": "System Logs",
|
||||
"memberTransfer": "Member Transfer",
|
||||
"myTischtennisAccount": "myTischtennis Account",
|
||||
"personalSettings": "Personal Settings",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"dailyBusiness": "Daily Business",
|
||||
"competitions": "Competitions",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"club": {
|
||||
"select": "Select Club",
|
||||
"selectPlaceholder": "Choose club...",
|
||||
"new": "New Club",
|
||||
"load": "Load",
|
||||
"name": "Club Name",
|
||||
"create": "Create Club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Register",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"rememberMe": "Remember me",
|
||||
"loginSuccess": "Successfully logged in",
|
||||
"logoutSuccess": "Successfully logged out",
|
||||
"sessionExpired": "Your session has expired. You will be logged out."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"personalSettings": "Personal Settings",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your preferred language for the application",
|
||||
"languageChanged": "Language successfully changed",
|
||||
"selectLanguage": "Select Language"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (German)",
|
||||
"en-GB": "English",
|
||||
"en-US": "English (US English)",
|
||||
"en-AU": "English (Australian English)",
|
||||
"de-CH": "Schwiizerdütsch (Swiss German)",
|
||||
"fr": "Français (French)",
|
||||
"es": "Español (Spanish)",
|
||||
"it": "Italiano (Italian)",
|
||||
"pl": "Polski (Polish)",
|
||||
"ja": "日本語 (Japanese)",
|
||||
"zh": "中文 (Chinese)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Information",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/en-US.json
Normal file
102
frontend/src/i18n/locales/en-US.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Training Diary",
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"actions": "Actions",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
"diary": "Diary",
|
||||
"approvals": "Approvals",
|
||||
"statistics": "Training Statistics",
|
||||
"tournaments": "Tournaments",
|
||||
"schedule": "Schedules",
|
||||
"clubSettings": "Club Settings",
|
||||
"predefinedActivities": "Predefined Activities",
|
||||
"teamManagement": "Team Management",
|
||||
"permissions": "Permissions",
|
||||
"logs": "System Logs",
|
||||
"memberTransfer": "Member Transfer",
|
||||
"myTischtennisAccount": "myTischtennis Account",
|
||||
"personalSettings": "Personal Settings",
|
||||
"logout": "Log Out",
|
||||
"login": "Log In",
|
||||
"register": "Sign Up",
|
||||
"dailyBusiness": "Daily Business",
|
||||
"competitions": "Competitions",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"club": {
|
||||
"select": "Select Club",
|
||||
"selectPlaceholder": "Choose club...",
|
||||
"new": "New Club",
|
||||
"load": "Load",
|
||||
"name": "Club Name",
|
||||
"create": "Create Club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Log In",
|
||||
"logout": "Log Out",
|
||||
"register": "Sign Up",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"rememberMe": "Remember me",
|
||||
"loginSuccess": "Successfully logged in",
|
||||
"logoutSuccess": "Successfully logged out",
|
||||
"sessionExpired": "Your session has expired. You will be logged out."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"personalSettings": "Personal Settings",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your preferred language for the application",
|
||||
"languageChanged": "Language successfully changed",
|
||||
"selectLanguage": "Select Language"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (German)",
|
||||
"en-GB": "English (British English)",
|
||||
"en-US": "English",
|
||||
"en-AU": "English (Australian English)",
|
||||
"de-CH": "Schwiizerdütsch (Swiss German)",
|
||||
"fr": "Français (French)",
|
||||
"es": "Español (Spanish)",
|
||||
"it": "Italiano (Italian)",
|
||||
"pl": "Polski (Polish)",
|
||||
"ja": "日本語 (Japanese)",
|
||||
"zh": "中文 (Chinese)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Information",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/es.json
Normal file
102
frontend/src/i18n/locales/es.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Diario de entrenamiento",
|
||||
"title": "Diario de entrenamiento"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Cargando...",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"add": "Añadir",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"search": "Buscar",
|
||||
"filter": "Filtrar",
|
||||
"actions": "Acciones",
|
||||
"back": "Atrás",
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior",
|
||||
"submit": "Enviar",
|
||||
"reset": "Restablecer"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Inicio",
|
||||
"members": "Miembros",
|
||||
"diary": "Diario",
|
||||
"approvals": "Aprobaciones",
|
||||
"statistics": "Estadísticas de entrenamiento",
|
||||
"tournaments": "Torneos",
|
||||
"schedule": "Calendarios",
|
||||
"clubSettings": "Configuración del club",
|
||||
"predefinedActivities": "Actividades predefinidas",
|
||||
"teamManagement": "Gestión de equipos",
|
||||
"permissions": "Permisos",
|
||||
"logs": "Registros del sistema",
|
||||
"memberTransfer": "Transferencia de miembros",
|
||||
"myTischtennisAccount": "Cuenta myTischtennis",
|
||||
"personalSettings": "Configuración personal",
|
||||
"logout": "Cerrar sesión",
|
||||
"login": "Iniciar sesión",
|
||||
"register": "Registrarse",
|
||||
"dailyBusiness": "Negocios diarios",
|
||||
"competitions": "Competiciones",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"club": {
|
||||
"select": "Seleccionar club",
|
||||
"selectPlaceholder": "Elegir club...",
|
||||
"new": "Nuevo club",
|
||||
"load": "Cargar",
|
||||
"name": "Nombre del club",
|
||||
"create": "Crear club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Iniciar sesión",
|
||||
"logout": "Cerrar sesión",
|
||||
"register": "Registrarse",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"rememberMe": "Recordarme",
|
||||
"loginSuccess": "Sesión iniciada correctamente",
|
||||
"logoutSuccess": "Sesión cerrada correctamente",
|
||||
"sessionExpired": "Tu sesión ha expirado. Serás desconectado."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"personalSettings": "Configuración personal",
|
||||
"language": "Idioma",
|
||||
"languageDescription": "Elige tu idioma preferido para la aplicación",
|
||||
"languageChanged": "Idioma cambiado correctamente",
|
||||
"selectLanguage": "Seleccionar idioma"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Alemán)",
|
||||
"en-GB": "English (Inglés (GB))",
|
||||
"en-US": "English (Inglés (US))",
|
||||
"en-AU": "English (Inglés (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Alemán suizo)",
|
||||
"fr": "Français (Francés)",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pl": "Polski (Polaco)",
|
||||
"ja": "日本語 (Japonés)",
|
||||
"zh": "中文 (Chino)",
|
||||
"tl": "Tagalog (Tagalo)",
|
||||
"th": "ไทย (Tailandés)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Éxito",
|
||||
"error": "Error",
|
||||
"warning": "Advertencia",
|
||||
"info": "Información",
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/fil.json
Normal file
102
frontend/src/i18n/locales/fil.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Training Diary",
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Naglo-load...",
|
||||
"save": "I-save",
|
||||
"cancel": "Kanselahin",
|
||||
"delete": "Tanggalin",
|
||||
"edit": "I-edit",
|
||||
"add": "Magdagdag",
|
||||
"close": "Isara",
|
||||
"confirm": "Kumpirmahin",
|
||||
"yes": "Oo",
|
||||
"no": "Hindi",
|
||||
"search": "Maghanap",
|
||||
"filter": "I-filter",
|
||||
"actions": "Mga aksyon",
|
||||
"back": "Bumalik",
|
||||
"next": "Susunod",
|
||||
"previous": "Nakaraan",
|
||||
"submit": "Ipasa",
|
||||
"reset": "I-reset"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Mga miyembro",
|
||||
"diary": "Talaarawan",
|
||||
"approvals": "Mga pag-apruba",
|
||||
"statistics": "Mga istatistika ng pagsasanay",
|
||||
"tournaments": "Mga paligsahan",
|
||||
"schedule": "Mga iskedyul",
|
||||
"clubSettings": "Mga setting ng club",
|
||||
"predefinedActivities": "Mga paunang natukoy na aktibidad",
|
||||
"teamManagement": "Pamamahala ng koponan",
|
||||
"permissions": "Mga pahintulot",
|
||||
"logs": "Mga system log",
|
||||
"memberTransfer": "Paglipat ng miyembro",
|
||||
"myTischtennisAccount": "myTischtennis Account",
|
||||
"personalSettings": "Mga personal na setting",
|
||||
"logout": "Mag-logout",
|
||||
"login": "Mag-login",
|
||||
"register": "Magrehistro",
|
||||
"dailyBusiness": "Araw-araw na negosyo",
|
||||
"competitions": "Mga kompetisyon",
|
||||
"settings": "Mga setting"
|
||||
},
|
||||
"club": {
|
||||
"select": "Pumili ng club",
|
||||
"selectPlaceholder": "Pumili ng club...",
|
||||
"new": "Bagong club",
|
||||
"load": "I-load",
|
||||
"name": "Pangalan ng club",
|
||||
"create": "Gumawa ng club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Mag-login",
|
||||
"logout": "Mag-logout",
|
||||
"register": "Magrehistro",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Nakalimutan ang password?",
|
||||
"rememberMe": "Tandaan ako",
|
||||
"loginSuccess": "Matagumpay na nag-login",
|
||||
"logoutSuccess": "Matagumpay na nag-logout",
|
||||
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Mga setting",
|
||||
"personalSettings": "Mga personal na setting",
|
||||
"language": "Wika",
|
||||
"languageDescription": "Pumili ng iyong gustong wika para sa aplikasyon",
|
||||
"languageChanged": "Matagumpay na nagbago ang wika",
|
||||
"selectLanguage": "Pumili ng wika"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Aleman)",
|
||||
"en-GB": "English (Ingles (GB))",
|
||||
"en-US": "English (Ingles (US))",
|
||||
"en-AU": "English (Ingles (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Swiss Aleman)",
|
||||
"fr": "Français (Pranses)",
|
||||
"es": "Español (Espanyol)",
|
||||
"it": "Italiano (Italyano)",
|
||||
"pl": "Polski (Polako)",
|
||||
"ja": "日本語 (Hapones)",
|
||||
"zh": "中文 (Intsik)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Tagumpay",
|
||||
"error": "Error",
|
||||
"warning": "Babala",
|
||||
"info": "Impormasyon",
|
||||
"confirm": "Kumpirmahin",
|
||||
"cancel": "Kanselahin"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/fr.json
Normal file
102
frontend/src/i18n/locales/fr.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Journal d'entraînement",
|
||||
"title": "Journal d'entraînement"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Chargement...",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"add": "Ajouter",
|
||||
"close": "Fermer",
|
||||
"confirm": "Confirmer",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"search": "Rechercher",
|
||||
"filter": "Filtrer",
|
||||
"actions": "Actions",
|
||||
"back": "Retour",
|
||||
"next": "Suivant",
|
||||
"previous": "Précédent",
|
||||
"submit": "Soumettre",
|
||||
"reset": "Réinitialiser"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Accueil",
|
||||
"members": "Membres",
|
||||
"diary": "Journal",
|
||||
"approvals": "Approbations",
|
||||
"statistics": "Statistiques d'entraînement",
|
||||
"tournaments": "Tournois",
|
||||
"schedule": "Calendriers",
|
||||
"clubSettings": "Paramètres du club",
|
||||
"predefinedActivities": "Activités prédéfinies",
|
||||
"teamManagement": "Gestion d'équipe",
|
||||
"permissions": "Autorisations",
|
||||
"logs": "Journaux système",
|
||||
"memberTransfer": "Transfert de membre",
|
||||
"myTischtennisAccount": "Compte myTischtennis",
|
||||
"personalSettings": "Paramètres personnels",
|
||||
"logout": "Déconnexion",
|
||||
"login": "Connexion",
|
||||
"register": "S'inscrire",
|
||||
"dailyBusiness": "Affaires quotidiennes",
|
||||
"competitions": "Compétitions",
|
||||
"settings": "Paramètres"
|
||||
},
|
||||
"club": {
|
||||
"select": "Sélectionner un club",
|
||||
"selectPlaceholder": "Choisir un club...",
|
||||
"new": "Nouveau club",
|
||||
"load": "Charger",
|
||||
"name": "Nom du club",
|
||||
"create": "Créer un club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Connexion",
|
||||
"logout": "Déconnexion",
|
||||
"register": "S'inscrire",
|
||||
"email": "E-mail",
|
||||
"password": "Mot de passe",
|
||||
"forgotPassword": "Mot de passe oublié ?",
|
||||
"rememberMe": "Se souvenir de moi",
|
||||
"loginSuccess": "Connexion réussie",
|
||||
"logoutSuccess": "Déconnexion réussie",
|
||||
"sessionExpired": "Votre session a expiré. Vous allez être déconnecté."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"personalSettings": "Paramètres personnels",
|
||||
"language": "Langue",
|
||||
"languageDescription": "Choisissez votre langue préférée pour l'application",
|
||||
"languageChanged": "Langue modifiée avec succès",
|
||||
"selectLanguage": "Sélectionner la langue"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Allemand)",
|
||||
"en-GB": "English (Anglais (GB))",
|
||||
"en-US": "English (Anglais (US))",
|
||||
"en-AU": "English (Anglais (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Allemand suisse)",
|
||||
"fr": "Français",
|
||||
"es": "Español (Espagnol)",
|
||||
"it": "Italiano (Italien)",
|
||||
"pl": "Polski (Polonais)",
|
||||
"ja": "日本語 (Japonais)",
|
||||
"zh": "中文 (Chinois)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thaï)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Succès",
|
||||
"error": "Erreur",
|
||||
"warning": "Avertissement",
|
||||
"info": "Information",
|
||||
"confirm": "Confirmer",
|
||||
"cancel": "Annuler"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/it.json
Normal file
102
frontend/src/i18n/locales/it.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Diario di allenamento",
|
||||
"title": "Diario di allenamento"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Caricamento...",
|
||||
"save": "Salva",
|
||||
"cancel": "Annulla",
|
||||
"delete": "Elimina",
|
||||
"edit": "Modifica",
|
||||
"add": "Aggiungi",
|
||||
"close": "Chiudi",
|
||||
"confirm": "Conferma",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"search": "Cerca",
|
||||
"filter": "Filtra",
|
||||
"actions": "Azioni",
|
||||
"back": "Indietro",
|
||||
"next": "Avanti",
|
||||
"previous": "Precedente",
|
||||
"submit": "Invia",
|
||||
"reset": "Reimposta"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Membri",
|
||||
"diary": "Diario",
|
||||
"approvals": "Approvazioni",
|
||||
"statistics": "Statistiche di allenamento",
|
||||
"tournaments": "Tornei",
|
||||
"schedule": "Calendari",
|
||||
"clubSettings": "Impostazioni club",
|
||||
"predefinedActivities": "Attività predefinite",
|
||||
"teamManagement": "Gestione squadra",
|
||||
"permissions": "Permessi",
|
||||
"logs": "Log di sistema",
|
||||
"memberTransfer": "Trasferimento membri",
|
||||
"myTischtennisAccount": "Account myTischtennis",
|
||||
"personalSettings": "Impostazioni personali",
|
||||
"logout": "Esci",
|
||||
"login": "Accedi",
|
||||
"register": "Registrati",
|
||||
"dailyBusiness": "Attività quotidiane",
|
||||
"competitions": "Competizioni",
|
||||
"settings": "Impostazioni"
|
||||
},
|
||||
"club": {
|
||||
"select": "Seleziona club",
|
||||
"selectPlaceholder": "Scegli club...",
|
||||
"new": "Nuovo club",
|
||||
"load": "Carica",
|
||||
"name": "Nome club",
|
||||
"create": "Crea club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Accedi",
|
||||
"logout": "Esci",
|
||||
"register": "Registrati",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Password dimenticata?",
|
||||
"rememberMe": "Ricordami",
|
||||
"loginSuccess": "Accesso effettuato con successo",
|
||||
"logoutSuccess": "Uscita effettuata con successo",
|
||||
"sessionExpired": "La tua sessione è scaduta. Verrai disconnesso."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"personalSettings": "Impostazioni personali",
|
||||
"language": "Lingua",
|
||||
"languageDescription": "Scegli la tua lingua preferita per l'applicazione",
|
||||
"languageChanged": "Lingua modificata con successo",
|
||||
"selectLanguage": "Seleziona lingua"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Tedesco)",
|
||||
"en-GB": "English (Inglese (GB))",
|
||||
"en-US": "English (Inglese (US))",
|
||||
"en-AU": "English (Inglese (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Tedesco svizzero)",
|
||||
"fr": "Français (Francese)",
|
||||
"es": "Español (Spagnolo)",
|
||||
"it": "Italiano",
|
||||
"pl": "Polski (Polacco)",
|
||||
"ja": "日本語 (Giapponese)",
|
||||
"zh": "中文 (Cinese)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Tailandese)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Successo",
|
||||
"error": "Errore",
|
||||
"warning": "Avviso",
|
||||
"info": "Informazione",
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/ja.json
Normal file
102
frontend/src/i18n/locales/ja.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "トレーニング日記",
|
||||
"title": "トレーニング日記"
|
||||
},
|
||||
"common": {
|
||||
"loading": "読み込み中...",
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"add": "追加",
|
||||
"close": "閉じる",
|
||||
"confirm": "確認",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"search": "検索",
|
||||
"filter": "フィルター",
|
||||
"actions": "アクション",
|
||||
"back": "戻る",
|
||||
"next": "次へ",
|
||||
"previous": "前へ",
|
||||
"submit": "送信",
|
||||
"reset": "リセット"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "ホーム",
|
||||
"members": "メンバー",
|
||||
"diary": "日記",
|
||||
"approvals": "承認",
|
||||
"statistics": "トレーニング統計",
|
||||
"tournaments": "トーナメント",
|
||||
"schedule": "スケジュール",
|
||||
"clubSettings": "クラブ設定",
|
||||
"predefinedActivities": "事前定義された活動",
|
||||
"teamManagement": "チーム管理",
|
||||
"permissions": "権限",
|
||||
"logs": "システムログ",
|
||||
"memberTransfer": "メンバー転送",
|
||||
"myTischtennisAccount": "myTischtennisアカウント",
|
||||
"personalSettings": "個人設定",
|
||||
"logout": "ログアウト",
|
||||
"login": "ログイン",
|
||||
"register": "登録",
|
||||
"dailyBusiness": "日常業務",
|
||||
"competitions": "競技",
|
||||
"settings": "設定"
|
||||
},
|
||||
"club": {
|
||||
"select": "クラブを選択",
|
||||
"selectPlaceholder": "クラブを選択...",
|
||||
"new": "新しいクラブ",
|
||||
"load": "読み込む",
|
||||
"name": "クラブ名",
|
||||
"create": "クラブを作成"
|
||||
},
|
||||
"auth": {
|
||||
"login": "ログイン",
|
||||
"logout": "ログアウト",
|
||||
"register": "登録",
|
||||
"email": "メール",
|
||||
"password": "パスワード",
|
||||
"forgotPassword": "パスワードをお忘れですか?",
|
||||
"rememberMe": "ログイン状態を保持",
|
||||
"loginSuccess": "ログインに成功しました",
|
||||
"logoutSuccess": "ログアウトに成功しました",
|
||||
"sessionExpired": "セッションが期限切れです。ログアウトされます。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"personalSettings": "個人設定",
|
||||
"language": "言語",
|
||||
"languageDescription": "アプリケーションの優先言語を選択してください",
|
||||
"languageChanged": "言語が正常に変更されました",
|
||||
"selectLanguage": "言語を選択"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (ドイツ語)",
|
||||
"en-GB": "English (英語(GB))",
|
||||
"en-US": "English (英語(US))",
|
||||
"en-AU": "English (英語(AU))",
|
||||
"de-CH": "Schwiizerdütsch (スイスドイツ語)",
|
||||
"fr": "Français (フランス語)",
|
||||
"es": "Español (スペイン語)",
|
||||
"it": "Italiano (イタリア語)",
|
||||
"pl": "Polski (ポーランド語)",
|
||||
"ja": "日本語",
|
||||
"zh": "中文 (中国語)",
|
||||
"tl": "Tagalog (タガログ語)",
|
||||
"th": "ไทย (タイ語)",
|
||||
"fil": "Filipino (フィリピン語)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "成功",
|
||||
"error": "エラー",
|
||||
"warning": "警告",
|
||||
"info": "情報",
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/pl.json
Normal file
102
frontend/src/i18n/locales/pl.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Dziennik treningowy",
|
||||
"title": "Dziennik treningowy"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Ładowanie...",
|
||||
"save": "Zapisz",
|
||||
"cancel": "Anuluj",
|
||||
"delete": "Usuń",
|
||||
"edit": "Edytuj",
|
||||
"add": "Dodaj",
|
||||
"close": "Zamknij",
|
||||
"confirm": "Potwierdź",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"search": "Szukaj",
|
||||
"filter": "Filtruj",
|
||||
"actions": "Akcje",
|
||||
"back": "Wstecz",
|
||||
"next": "Dalej",
|
||||
"previous": "Poprzedni",
|
||||
"submit": "Wyślij",
|
||||
"reset": "Resetuj"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Strona główna",
|
||||
"members": "Członkowie",
|
||||
"diary": "Dziennik",
|
||||
"approvals": "Zatwierdzenia",
|
||||
"statistics": "Statystyki treningowe",
|
||||
"tournaments": "Turnieje",
|
||||
"schedule": "Harmonogramy",
|
||||
"clubSettings": "Ustawienia klubu",
|
||||
"predefinedActivities": "Predefiniowane aktywności",
|
||||
"teamManagement": "Zarządzanie zespołem",
|
||||
"permissions": "Uprawnienia",
|
||||
"logs": "Logi systemowe",
|
||||
"memberTransfer": "Transfer członka",
|
||||
"myTischtennisAccount": "Konto myTischtennis",
|
||||
"personalSettings": "Ustawienia osobiste",
|
||||
"logout": "Wyloguj",
|
||||
"login": "Zaloguj",
|
||||
"register": "Zarejestruj",
|
||||
"dailyBusiness": "Codzienne sprawy",
|
||||
"competitions": "Zawody",
|
||||
"settings": "Ustawienia"
|
||||
},
|
||||
"club": {
|
||||
"select": "Wybierz klub",
|
||||
"selectPlaceholder": "Wybierz klub...",
|
||||
"new": "Nowy klub",
|
||||
"load": "Załaduj",
|
||||
"name": "Nazwa klubu",
|
||||
"create": "Utwórz klub"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Zaloguj",
|
||||
"logout": "Wyloguj",
|
||||
"register": "Zarejestruj",
|
||||
"email": "Email",
|
||||
"password": "Hasło",
|
||||
"forgotPassword": "Zapomniałeś hasła?",
|
||||
"rememberMe": "Zapamiętaj mnie",
|
||||
"loginSuccess": "Pomyślnie zalogowano",
|
||||
"logoutSuccess": "Pomyślnie wylogowano",
|
||||
"sessionExpired": "Twoja sesja wygasła. Zostaniesz wylogowany."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ustawienia",
|
||||
"personalSettings": "Ustawienia osobiste",
|
||||
"language": "Język",
|
||||
"languageDescription": "Wybierz preferowany język aplikacji",
|
||||
"languageChanged": "Język został zmieniony pomyślnie",
|
||||
"selectLanguage": "Wybierz język"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Niemiecki)",
|
||||
"en-GB": "English (Angielski (GB))",
|
||||
"en-US": "English (Angielski (US))",
|
||||
"en-AU": "English (Angielski (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Niemiecki szwajcarski)",
|
||||
"fr": "Français (Francuski)",
|
||||
"es": "Español (Hiszpański)",
|
||||
"it": "Italiano (Włoski)",
|
||||
"pl": "Polski",
|
||||
"ja": "日本語 (Japoński)",
|
||||
"zh": "中文 (Chiński)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Tajski)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Sukces",
|
||||
"error": "Błąd",
|
||||
"warning": "Ostrzeżenie",
|
||||
"info": "Informacja",
|
||||
"confirm": "Potwierdź",
|
||||
"cancel": "Anuluj"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/th.json
Normal file
102
frontend/src/i18n/locales/th.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "สมุดบันทึกการฝึกซ้อม",
|
||||
"title": "สมุดบันทึกการฝึกซ้อม"
|
||||
},
|
||||
"common": {
|
||||
"loading": "กำลังโหลด...",
|
||||
"save": "บันทึก",
|
||||
"cancel": "ยกเลิก",
|
||||
"delete": "ลบ",
|
||||
"edit": "แก้ไข",
|
||||
"add": "เพิ่ม",
|
||||
"close": "ปิด",
|
||||
"confirm": "ยืนยัน",
|
||||
"yes": "ใช่",
|
||||
"no": "ไม่",
|
||||
"search": "ค้นหา",
|
||||
"filter": "กรอง",
|
||||
"actions": "การดำเนินการ",
|
||||
"back": "กลับ",
|
||||
"next": "ถัดไป",
|
||||
"previous": "ก่อนหน้า",
|
||||
"submit": "ส่ง",
|
||||
"reset": "รีเซ็ต"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "หน้าแรก",
|
||||
"members": "สมาชิก",
|
||||
"diary": "ไดอารี่",
|
||||
"approvals": "การอนุมัติ",
|
||||
"statistics": "สถิติการฝึกซ้อม",
|
||||
"tournaments": "การแข่งขัน",
|
||||
"schedule": "ตารางเวลา",
|
||||
"clubSettings": "การตั้งค่าสโมสร",
|
||||
"predefinedActivities": "กิจกรรมที่กำหนดไว้ล่วงหน้า",
|
||||
"teamManagement": "การจัดการทีม",
|
||||
"permissions": "สิทธิ์",
|
||||
"logs": "บันทึกระบบ",
|
||||
"memberTransfer": "การโอนสมาชิก",
|
||||
"myTischtennisAccount": "บัญชี myTischtennis",
|
||||
"personalSettings": "การตั้งค่าส่วนตัว",
|
||||
"logout": "ออกจากระบบ",
|
||||
"login": "เข้าสู่ระบบ",
|
||||
"register": "ลงทะเบียน",
|
||||
"dailyBusiness": "ธุรกิจประจำวัน",
|
||||
"competitions": "การแข่งขัน",
|
||||
"settings": "การตั้งค่า"
|
||||
},
|
||||
"club": {
|
||||
"select": "เลือกสโมสร",
|
||||
"selectPlaceholder": "เลือกสโมสร...",
|
||||
"new": "สโมสรใหม่",
|
||||
"load": "โหลด",
|
||||
"name": "ชื่อสโมสร",
|
||||
"create": "สร้างสโมสร"
|
||||
},
|
||||
"auth": {
|
||||
"login": "เข้าสู่ระบบ",
|
||||
"logout": "ออกจากระบบ",
|
||||
"register": "ลงทะเบียน",
|
||||
"email": "อีเมล",
|
||||
"password": "รหัสผ่าน",
|
||||
"forgotPassword": "ลืมรหัสผ่าน?",
|
||||
"rememberMe": "จดจำฉัน",
|
||||
"loginSuccess": "เข้าสู่ระบบสำเร็จ",
|
||||
"logoutSuccess": "ออกจากระบบสำเร็จ",
|
||||
"sessionExpired": "เซสชันของคุณหมดอายุแล้ว คุณจะถูกออกจากระบบ"
|
||||
},
|
||||
"settings": {
|
||||
"title": "การตั้งค่า",
|
||||
"personalSettings": "การตั้งค่าส่วนตัว",
|
||||
"language": "ภาษา",
|
||||
"languageDescription": "เลือกภาษาที่คุณต้องการสำหรับแอปพลิเคชัน",
|
||||
"languageChanged": "เปลี่ยนภาษาสำเร็จ",
|
||||
"selectLanguage": "เลือกภาษา"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (เยอรมัน)",
|
||||
"en-GB": "English (อังกฤษ (GB))",
|
||||
"en-US": "English (อังกฤษ (US))",
|
||||
"en-AU": "English (อังกฤษ (AU))",
|
||||
"de-CH": "Schwiizerdütsch (เยอรมันสวิส)",
|
||||
"fr": "Français (ฝรั่งเศส)",
|
||||
"es": "Español (สเปน)",
|
||||
"it": "Italiano (อิตาลี)",
|
||||
"pl": "Polski (โปแลนด์)",
|
||||
"ja": "日本語 (ญี่ปุ่น)",
|
||||
"zh": "中文 (จีน)",
|
||||
"tl": "Tagalog (ตากาล็อก)",
|
||||
"th": "ไทย",
|
||||
"fil": "Filipino (ฟิลิปปินส์)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "สำเร็จ",
|
||||
"error": "ข้อผิดพลาด",
|
||||
"warning": "คำเตือน",
|
||||
"info": "ข้อมูล",
|
||||
"confirm": "ยืนยัน",
|
||||
"cancel": "ยกเลิก"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/tl.json
Normal file
102
frontend/src/i18n/locales/tl.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Training Diary",
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Naglo-load...",
|
||||
"save": "I-save",
|
||||
"cancel": "Kanselahin",
|
||||
"delete": "Tanggalin",
|
||||
"edit": "I-edit",
|
||||
"add": "Magdagdag",
|
||||
"close": "Isara",
|
||||
"confirm": "Kumpirmahin",
|
||||
"yes": "Oo",
|
||||
"no": "Hindi",
|
||||
"search": "Maghanap",
|
||||
"filter": "I-filter",
|
||||
"actions": "Mga aksyon",
|
||||
"back": "Bumalik",
|
||||
"next": "Susunod",
|
||||
"previous": "Nakaraan",
|
||||
"submit": "Ipasa",
|
||||
"reset": "I-reset"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Mga miyembro",
|
||||
"diary": "Talaarawan",
|
||||
"approvals": "Mga pag-apruba",
|
||||
"statistics": "Mga istatistika ng pagsasanay",
|
||||
"tournaments": "Mga paligsahan",
|
||||
"schedule": "Mga iskedyul",
|
||||
"clubSettings": "Mga setting ng club",
|
||||
"predefinedActivities": "Mga paunang natukoy na aktibidad",
|
||||
"teamManagement": "Pamamahala ng koponan",
|
||||
"permissions": "Mga pahintulot",
|
||||
"logs": "Mga system log",
|
||||
"memberTransfer": "Paglipat ng miyembro",
|
||||
"myTischtennisAccount": "myTischtennis Account",
|
||||
"personalSettings": "Mga personal na setting",
|
||||
"logout": "Mag-logout",
|
||||
"login": "Mag-login",
|
||||
"register": "Magrehistro",
|
||||
"dailyBusiness": "Araw-araw na negosyo",
|
||||
"competitions": "Mga kompetisyon",
|
||||
"settings": "Mga setting"
|
||||
},
|
||||
"club": {
|
||||
"select": "Pumili ng club",
|
||||
"selectPlaceholder": "Pumili ng club...",
|
||||
"new": "Bagong club",
|
||||
"load": "I-load",
|
||||
"name": "Pangalan ng club",
|
||||
"create": "Gumawa ng club"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Mag-login",
|
||||
"logout": "Mag-logout",
|
||||
"register": "Magrehistro",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Nakalimutan ang password?",
|
||||
"rememberMe": "Tandaan ako",
|
||||
"loginSuccess": "Matagumpay na nag-login",
|
||||
"logoutSuccess": "Matagumpay na nag-logout",
|
||||
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Mga setting",
|
||||
"personalSettings": "Mga personal na setting",
|
||||
"language": "Wika",
|
||||
"languageDescription": "Pumili ng iyong gustong wika para sa aplikasyon",
|
||||
"languageChanged": "Matagumpay na nagbago ang wika",
|
||||
"selectLanguage": "Pumili ng wika"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (Aleman)",
|
||||
"en-GB": "English (Ingles (GB))",
|
||||
"en-US": "English (Ingles (US))",
|
||||
"en-AU": "English (Ingles (AU))",
|
||||
"de-CH": "Schwiizerdütsch (Swiss Aleman)",
|
||||
"fr": "Français (Pranses)",
|
||||
"es": "Español (Espanyol)",
|
||||
"it": "Italiano (Italyano)",
|
||||
"pl": "Polski (Polako)",
|
||||
"ja": "日本語 (Hapones)",
|
||||
"zh": "中文 (Intsik)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Tagumpay",
|
||||
"error": "Error",
|
||||
"warning": "Babala",
|
||||
"info": "Impormasyon",
|
||||
"confirm": "Kumpirmahin",
|
||||
"cancel": "Kanselahin"
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/i18n/locales/zh.json
Normal file
102
frontend/src/i18n/locales/zh.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "训练日记",
|
||||
"title": "训练日记"
|
||||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"actions": "操作",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"submit": "提交",
|
||||
"reset": "重置"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
"members": "成员",
|
||||
"diary": "日记",
|
||||
"approvals": "审批",
|
||||
"statistics": "训练统计",
|
||||
"tournaments": "锦标赛",
|
||||
"schedule": "赛程",
|
||||
"clubSettings": "俱乐部设置",
|
||||
"predefinedActivities": "预定义活动",
|
||||
"teamManagement": "团队管理",
|
||||
"permissions": "权限",
|
||||
"logs": "系统日志",
|
||||
"memberTransfer": "成员转移",
|
||||
"myTischtennisAccount": "myTischtennis账户",
|
||||
"personalSettings": "个人设置",
|
||||
"logout": "退出登录",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"dailyBusiness": "日常事务",
|
||||
"competitions": "比赛",
|
||||
"settings": "设置"
|
||||
},
|
||||
"club": {
|
||||
"select": "选择俱乐部",
|
||||
"selectPlaceholder": "选择俱乐部...",
|
||||
"new": "新俱乐部",
|
||||
"load": "加载",
|
||||
"name": "俱乐部名称",
|
||||
"create": "创建俱乐部"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"logout": "退出登录",
|
||||
"register": "注册",
|
||||
"email": "电子邮件",
|
||||
"password": "密码",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"rememberMe": "记住我",
|
||||
"loginSuccess": "登录成功",
|
||||
"logoutSuccess": "退出成功",
|
||||
"sessionExpired": "您的会话已过期。您将被登出。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"personalSettings": "个人设置",
|
||||
"language": "语言",
|
||||
"languageDescription": "选择您偏好的应用程序语言",
|
||||
"languageChanged": "语言已成功更改",
|
||||
"selectLanguage": "选择语言"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Deutsch (德语)",
|
||||
"en-GB": "English (英语(GB))",
|
||||
"en-US": "English (英语(US))",
|
||||
"en-AU": "English (英语(AU))",
|
||||
"de-CH": "Schwiizerdütsch (瑞士德语)",
|
||||
"fr": "Français (法语)",
|
||||
"es": "Español (西班牙语)",
|
||||
"it": "Italiano (意大利语)",
|
||||
"pl": "Polski (波兰语)",
|
||||
"ja": "日本語 (日语)",
|
||||
"zh": "中文",
|
||||
"tl": "Tagalog (他加禄语)",
|
||||
"th": "ไทย (泰语)",
|
||||
"fil": "Filipino (菲律宾语)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import i18n from './i18n';
|
||||
import '@/assets/css/main.scss';
|
||||
import './assets/css/vue-multiselect.css';
|
||||
import permissionDirectives from './directives/permissions.js';
|
||||
@@ -17,4 +18,5 @@ app.directive('owner', permissionDirectives.owner);
|
||||
app
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.mount('#app');
|
||||
|
||||
@@ -18,6 +18,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 PersonalSettings from './views/PersonalSettings.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -41,6 +42,7 @@ const routes = [
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/member-transfer-settings', component: MemberTransferSettingsView },
|
||||
{ path: '/personal-settings', component: PersonalSettings },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createStore } from 'vuex';
|
||||
import router from './router.js';
|
||||
import apiClient from './apiClient.js';
|
||||
import { safeSessionStorage, safeLocalStorage } from './utils/storage.js';
|
||||
import i18n from './i18n';
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
@@ -34,6 +35,14 @@ const store = createStore({
|
||||
// Standardmäßig kollabiert auf mobilen Geräten
|
||||
return window.innerWidth <= 480;
|
||||
})(),
|
||||
language: (() => {
|
||||
const saved = safeLocalStorage.getItem('userLanguage');
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
// Browser-Sprache wird in i18n/index.js erkannt
|
||||
return null;
|
||||
})(),
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
@@ -81,6 +90,10 @@ const store = createStore({
|
||||
state.sidebarCollapsed = collapsed;
|
||||
safeLocalStorage.setItem('sidebarCollapsed', collapsed.toString());
|
||||
},
|
||||
setLanguage(state, language) {
|
||||
state.language = language;
|
||||
safeLocalStorage.setItem('userLanguage', language);
|
||||
},
|
||||
clearToken(state) {
|
||||
state.token = null;
|
||||
safeSessionStorage.removeItem('token');
|
||||
@@ -172,6 +185,11 @@ const store = createStore({
|
||||
toggleSidebar({ commit, state }) {
|
||||
commit('setSidebarCollapsed', !state.sidebarCollapsed);
|
||||
},
|
||||
setLanguage({ commit }, language) {
|
||||
commit('setLanguage', language);
|
||||
// Aktualisiere auch die i18n-Instanz
|
||||
i18n.global.locale = language;
|
||||
},
|
||||
// Dialog-Actions
|
||||
openDialog({ commit }, dialog) {
|
||||
commit('openDialog', dialog);
|
||||
@@ -196,6 +214,7 @@ const store = createStore({
|
||||
currentClub: state => state.currentClub,
|
||||
clubs: state => state.clubs,
|
||||
sidebarCollapsed: state => state.sidebarCollapsed,
|
||||
language: state => state.language,
|
||||
currentClubName: state => {
|
||||
const club = state.clubs.find(club => club.id === parseInt(state.currentClub));
|
||||
return club ? club.name : '';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { translateErrorCode } from './errorMessages.js';
|
||||
|
||||
const INFO_TYPES = new Set(['info', 'success', 'warning', 'error']);
|
||||
const CONFIRM_TYPES = new Set(['info', 'warning', 'danger', 'success']);
|
||||
|
||||
@@ -38,17 +40,23 @@ export function buildInfoConfig({
|
||||
closeOnOverlay = true,
|
||||
okText = 'OK',
|
||||
icon = true,
|
||||
t = null, // Übersetzungsfunktion (optional)
|
||||
} = {}) {
|
||||
const safeType = INFO_TYPES.has(type) ? type : 'info';
|
||||
|
||||
// Übersetze Standard-Texte, falls t vorhanden
|
||||
const defaultTitle = t ? t('messages.info') : 'Information';
|
||||
const defaultOkText = t ? t('common.ok') : 'OK';
|
||||
|
||||
return {
|
||||
isOpen: true,
|
||||
title: sanitizeText(title, 'Information', 120),
|
||||
title: sanitizeText(title, defaultTitle, 120),
|
||||
message: sanitizeText(message, '', 600),
|
||||
details: sanitizeText(details, '', 1200),
|
||||
type: safeType,
|
||||
size,
|
||||
closeOnOverlay,
|
||||
okText: sanitizeText(okText, 'OK', 40),
|
||||
okText: sanitizeText(okText, defaultOkText, 40),
|
||||
icon: typeof icon === 'string' ? sanitizeText(icon, '', 10) : Boolean(icon),
|
||||
};
|
||||
}
|
||||
@@ -62,26 +70,39 @@ export function buildConfirmConfig({
|
||||
cancelText = 'Abbrechen',
|
||||
showCancel = true,
|
||||
resolveCallback = null,
|
||||
t = null, // Übersetzungsfunktion (optional)
|
||||
} = {}) {
|
||||
const safeType = CONFIRM_TYPES.has(type) ? type : 'info';
|
||||
|
||||
// Übersetze Standard-Texte, falls t vorhanden
|
||||
const defaultTitle = t ? t('messages.confirm') : 'Bestätigung';
|
||||
const defaultConfirmText = t ? t('common.ok') : 'OK';
|
||||
const defaultCancelText = t ? t('common.cancel') : 'Abbrechen';
|
||||
|
||||
return {
|
||||
isOpen: true,
|
||||
title: sanitizeText(title, 'Bestätigung', 120),
|
||||
title: sanitizeText(title, defaultTitle, 120),
|
||||
message: sanitizeText(message, '', 600),
|
||||
details: sanitizeText(details, '', 1200),
|
||||
type: safeType,
|
||||
confirmText: sanitizeText(confirmText, 'OK', 40),
|
||||
cancelText: sanitizeText(cancelText, 'Abbrechen', 40),
|
||||
confirmText: sanitizeText(confirmText, defaultConfirmText, 40),
|
||||
cancelText: sanitizeText(cancelText, defaultCancelText, 40),
|
||||
showCancel: Boolean(showCancel),
|
||||
resolveCallback,
|
||||
};
|
||||
}
|
||||
|
||||
export function safeErrorMessage(error, fallback = 'Es ist ein Fehler aufgetreten.') {
|
||||
export function safeErrorMessage(error, fallback = 'Es ist ein Fehler aufgetreten.', t = null) {
|
||||
if (!error) {
|
||||
return fallback;
|
||||
return t ? (t('errors.ERROR_UNKNOWN_ERROR') || fallback) : fallback;
|
||||
}
|
||||
|
||||
// Prüfe zuerst auf Fehlercode (neues Format)
|
||||
if (error?.response?.data?.code && t) {
|
||||
return translateErrorCode(error.response.data.code, error.response.data.params, t);
|
||||
}
|
||||
|
||||
// Fallback: Suche nach String-Nachricht
|
||||
const candidates = [
|
||||
error?.response?.data?.error,
|
||||
error?.response?.data?.message,
|
||||
@@ -93,7 +114,8 @@ export function safeErrorMessage(error, fallback = 'Es ist ein Fehler aufgetrete
|
||||
];
|
||||
|
||||
const message = candidates.find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
||||
return sanitizeText(message, fallback, 600);
|
||||
const finalFallback = t ? (t('errors.ERROR_UNKNOWN_ERROR') || fallback) : fallback;
|
||||
return sanitizeText(message, finalFallback, 600);
|
||||
}
|
||||
|
||||
export function sanitizeDetails(details) {
|
||||
|
||||
@@ -26,7 +26,92 @@ export function sanitizeText(value, fallback = '') {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
export function getSafeErrorMessage(error, fallback = 'Unbekannter Fehler') {
|
||||
/**
|
||||
* Übersetzt einen Fehlercode mit optionalen Parametern
|
||||
* @param {string} code - Fehlercode (z.B. 'ERROR_USER_NOT_FOUND')
|
||||
* @param {object} params - Optionale Parameter für die Übersetzung
|
||||
* @param {Function} t - Übersetzungsfunktion (z.B. this.$t oder t aus useI18n)
|
||||
* @returns {string} Übersetzte Fehlermeldung
|
||||
*/
|
||||
export function translateErrorCode(code, params = null, t = null) {
|
||||
if (!code || !t) {
|
||||
return code || 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
try {
|
||||
// Versuche, den Fehlercode zu übersetzen
|
||||
// Format: errors.ERROR_CODE_NAME
|
||||
const translationKey = `errors.${code}`;
|
||||
let translated = t(translationKey);
|
||||
|
||||
// Wenn keine Übersetzung gefunden wurde, gib den Code zurück
|
||||
if (translated === translationKey) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// Wenn Parameter vorhanden sind, ersetze Platzhalter
|
||||
if (params && typeof params === 'object') {
|
||||
Object.keys(params).forEach(key => {
|
||||
const placeholder = `{${key}}`;
|
||||
const value = String(params[key]);
|
||||
translated = translated.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
|
||||
});
|
||||
}
|
||||
|
||||
return translated;
|
||||
} catch (error) {
|
||||
console.error('[translateErrorCode] Error translating code:', code, error);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Fehlercode und Parameter aus einer API-Fehlerantwort
|
||||
* @param {object} error - Axios-Fehler oder Fehler-Objekt
|
||||
* @returns {object|null} { code, params } oder null
|
||||
*/
|
||||
export function extractErrorCode(error) {
|
||||
if (!error) return null;
|
||||
|
||||
// Prüfe response.data für Fehlercode
|
||||
const data = error?.response?.data;
|
||||
if (data && data.code) {
|
||||
return {
|
||||
code: data.code,
|
||||
params: data.params || null
|
||||
};
|
||||
}
|
||||
|
||||
// Prüfe direktes error-Objekt
|
||||
if (error.code && error.code.startsWith('ERROR_')) {
|
||||
return {
|
||||
code: error.code,
|
||||
params: error.params || null
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine sichere, übersetzte Fehlermeldung zurück
|
||||
* @param {object} error - Axios-Fehler oder Fehler-Objekt
|
||||
* @param {string} fallback - Fallback-Nachricht (wird übersetzt, wenn t vorhanden)
|
||||
* @param {Function} t - Übersetzungsfunktion (optional)
|
||||
* @returns {string} Übersetzte Fehlermeldung
|
||||
*/
|
||||
export function getSafeErrorMessage(error, fallback = 'Unbekannter Fehler', t = null) {
|
||||
if (!error) {
|
||||
return t ? translateErrorCode('ERROR_UNKNOWN_ERROR', null, t) : fallback;
|
||||
}
|
||||
|
||||
// Prüfe zuerst auf Fehlercode
|
||||
const errorCode = extractErrorCode(error);
|
||||
if (errorCode && t) {
|
||||
return translateErrorCode(errorCode.code, errorCode.params, t);
|
||||
}
|
||||
|
||||
// Fallback: Suche nach String-Nachricht
|
||||
const candidates = [
|
||||
error?.response?.data?.error,
|
||||
error?.response?.data?.message,
|
||||
@@ -35,10 +120,10 @@ export function getSafeErrorMessage(error, fallback = 'Unbekannter Fehler') {
|
||||
|
||||
const firstValid = candidates.find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
||||
if (!firstValid) {
|
||||
return fallback;
|
||||
return t ? translateErrorCode('ERROR_UNKNOWN_ERROR', null, t) : fallback;
|
||||
}
|
||||
|
||||
return sanitizeText(firstValid, fallback);
|
||||
return sanitizeText(firstValid, t ? translateErrorCode('ERROR_UNKNOWN_ERROR', null, t) : fallback);
|
||||
}
|
||||
|
||||
export function getSafeMessage(value, fallback = '') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Activate Account</h2>
|
||||
<button @click="activate">Activate</button>
|
||||
<h2>{{ $t('auth.activateAccount') }}</h2>
|
||||
<button @click="activate">{{ $t('auth.activate') }}</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,11 +64,11 @@ export default {
|
||||
try {
|
||||
const activationCode = this.$route.params.activationCode;
|
||||
await axios.get(`/api/auth/activate/${activationCode}`);
|
||||
await this.showInfo('Erfolg', 'Account aktiviert! Du kannst dich jetzt anmelden.', '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('auth.accountActivated'), '', 'success');
|
||||
this.$router.push('/login');
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Aktivierung fehlgeschlagen. Bitte überprüfe den Link oder versuche es erneut.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('auth.activationFailed'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="club-settings">
|
||||
<h1>Vereins-Einstellungen</h1>
|
||||
<h1>{{ $t('clubSettings.title') }}</h1>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-navigation">
|
||||
@@ -8,50 +8,50 @@
|
||||
:class="['tab-button', { active: activeTab === 'settings' }]"
|
||||
@click="activeTab = 'settings'"
|
||||
>
|
||||
⚙️ Einstellungen
|
||||
⚙️ {{ $t('clubSettings.settings') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-groups' }]"
|
||||
@click="activeTab = 'training-groups'"
|
||||
>
|
||||
👨👩👧👦 Trainingsgruppen
|
||||
👨👩👧👦 {{ $t('clubSettings.trainingGroups') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-times' }]"
|
||||
@click="activeTab = 'training-times'"
|
||||
>
|
||||
🕐 Trainingszeiten
|
||||
🕐 {{ $t('clubSettings.trainingTimes') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div v-if="activeTab === 'settings'">
|
||||
<section class="card">
|
||||
<h2>Begrüßungstext</h2>
|
||||
<h2>{{ $t('clubSettings.greetingText') }}</h2>
|
||||
<div class="greeting-grid">
|
||||
<textarea v-model="greeting" class="greeting-input" rows="10" placeholder="Begrüßungstext für Heimspiele..."></textarea>
|
||||
<textarea v-model="greeting" class="greeting-input" rows="10" :placeholder="$t('clubSettings.greetingPlaceholder')"></textarea>
|
||||
<div class="legend">
|
||||
<h3>Platzhalter</h3>
|
||||
<h3>{{ $t('clubSettings.placeholders') }}</h3>
|
||||
<ul>
|
||||
<li><code>{home}</code> Name Heimmannschaft</li>
|
||||
<li><code>{guest}</code> Name Gastmannschaft</li>
|
||||
<li><code>{homeplayers}</code> Spieler und Doppel Heimmannschaft</li>
|
||||
<li><code>{guestplayers}</code> Spieler und Doppel Gastmannschaft</li>
|
||||
<li><code>{home}</code> {{ $t('clubSettings.homeTeam') }}</li>
|
||||
<li><code>{guest}</code> {{ $t('clubSettings.guestTeam') }}</li>
|
||||
<li><code>{homeplayers}</code> {{ $t('clubSettings.homePlayers') }}</li>
|
||||
<li><code>{guestplayers}</code> {{ $t('clubSettings.guestPlayers') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">Dieser Text erscheint im Reiter "Begrüßung" des Spielberichtsbogens.</p>
|
||||
<p class="hint">{{ $t('clubSettings.greetingHint') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Verbands-Mitgliedsnummer</h2>
|
||||
<input v-model="associationMemberNumber" class="text-input" placeholder="z. B. 12-3456" />
|
||||
<h2>{{ $t('clubSettings.associationMemberNumber') }}</h2>
|
||||
<input v-model="associationMemberNumber" class="text-input" :placeholder="$t('clubSettings.associationMemberNumberPlaceholder')" />
|
||||
</section>
|
||||
|
||||
<section class="card actions-card">
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="save">Speichern</button>
|
||||
<span v-if="saved" class="saved-hint">Gespeichert</span>
|
||||
<button class="btn btn-primary" @click="save">{{ $t('clubSettings.save') }}</button>
|
||||
<span v-if="saved" class="saved-hint">{{ $t('clubSettings.saved') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
setTimeout(() => (this.saved = false), 1500);
|
||||
} catch (e) {
|
||||
this.saved = false;
|
||||
alert('Speichern fehlgeschlagen');
|
||||
alert(this.$t('clubSettings.saveFailed'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Verein {{ club.name }}</h2>
|
||||
<h2>{{ $t('club.title') }} {{ club.name }}</h2>
|
||||
<div v-if="accessAllowed">
|
||||
<div v-if="openRequests.length > 0">
|
||||
<h3>Offene Anfragen auf Zugriff</h3>
|
||||
<h3>{{ $t('club.openAccessRequests') }}</h3>
|
||||
<!-- Hier könntest du die offenen Anfragen anzeigen -->
|
||||
</div>
|
||||
<div>
|
||||
<h3>Mitglieder</h3>
|
||||
<h3>{{ $t('club.members') }}</h3>
|
||||
<ul class="members">
|
||||
<li v-for="member in displayedMembers" :key="member.id" class="member-item">
|
||||
<span class="gender-symbol" :class="'gender-' + (member.gender || 'unknown')" :title="labelGender(member.gender)">{{ genderSymbol(member.gender) }}</span>
|
||||
@@ -18,12 +18,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Trainingstagebuch</h3>
|
||||
<h3>{{ $t('club.diary') }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>Für diesen Verein wurde Dir noch kein Zugriff gestattet.</div>
|
||||
<button @click="requestAccess">Zugriff beantragen</button>
|
||||
<div>{{ $t('club.noAccess') }}</div>
|
||||
<button @click="requestAccess">{{ $t('club.requestAccess') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,9 +58,6 @@ import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
export default {
|
||||
name: "ClubView",
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs']),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Dialog States
|
||||
@@ -119,8 +116,8 @@ export default {
|
||||
this.club = response.data;
|
||||
this.accessAllowed = true;
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Zugriff auf den Verein nicht gestattet.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('club.accessDenied'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
async loadOpenRequests() {
|
||||
@@ -128,26 +125,26 @@ export default {
|
||||
const response = await apiClient.get(`/clubmembers/notapproved/${this.currentClub}`);
|
||||
this.openRequests = response.data;
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der offenen Anfragen', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('club.errorLoadingRequests'), '', 'error');
|
||||
}
|
||||
},
|
||||
async requestAccess() {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubs/request/${this.currentClub}`);
|
||||
if (response.status === 200) {
|
||||
await this.showInfo('Hinweis', 'Zugriff wurde angefragt.', '', 'info');
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('club.accessRequested'), '', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Zugriffsanfrage konnte nicht gestellt werden.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('club.accessRequestFailed'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
labelGender(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return 'Männlich';
|
||||
if (v === 'female') return 'Weiblich';
|
||||
if (v === 'diverse') return 'Divers';
|
||||
return 'Unbekannt';
|
||||
if (v === 'male') return this.$t('members.genderMale');
|
||||
if (v === 'female') return this.$t('members.genderFemale');
|
||||
if (v === 'diverse') return this.$t('members.genderDiverse');
|
||||
return this.$t('members.genderUnknown');
|
||||
},
|
||||
genderSymbol(g) {
|
||||
const v = (g || 'unknown');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Verein anlegen</h2>
|
||||
<label>Name des Vereins: <input type="text" v-model="clubName" /></label>
|
||||
<button @click="createClub">Verein anlegen</button>
|
||||
<h2>{{ $t('createClub.title') }}</h2>
|
||||
<label>{{ $t('createClub.clubName') }} <input type="text" v-model="clubName" /></label>
|
||||
<button @click="createClub">{{ $t('createClub.create') }}</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
...mapActions(['setClubs', 'setCurrentClub']),
|
||||
async createClub() {
|
||||
if (this.clubName.trim().length < 3) {
|
||||
await this.showInfo('Hinweis', 'Bitte gib dem Verein einen aussagekräftigen Namen.', '', 'warning');
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('createClub.nameRequired'), '', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -97,10 +97,10 @@ export default {
|
||||
this.setCurrentClub(newClub.data);
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
await this.showInfo('Hinweis', 'Der Verein existiert bereits.', '', 'info');
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('createClub.clubExists'), '', 'info');
|
||||
} else {
|
||||
const message = safeErrorMessage(error, 'Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('createClub.unknownError'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p class="back-home"><router-link to="/">Zur Startseite</router-link></p>
|
||||
<h1>{{ $t('legal.privacyPolicy') }}</h1>
|
||||
<p class="back-home"><router-link to="/">{{ $t('legal.backToHome') }}</router-link></p>
|
||||
|
||||
<section>
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
<template>
|
||||
<div class="diary">
|
||||
<h2>Trainingstagebuch</h2>
|
||||
<h2>{{ $t('diary.title') }}</h2>
|
||||
<div class="diary-header-row">
|
||||
<label>Datum:
|
||||
<label>{{ $t('diary.date') }}:
|
||||
<select v-model="date" @change="handleDateChange">
|
||||
<option :value="null" v-if="dates.length === 0">Keine Einträge</option>
|
||||
<option :value="null" v-if="dates.length === 0">{{ $t('diary.noEntries') }}</option>
|
||||
<option v-for="entry in dates" :key="entry.id" :value="entry">{{ getFormattedDate(entry.date) }}
|
||||
</option>
|
||||
</select>
|
||||
<button v-if="date && canDeleteCurrentDate" class="btn-secondary"
|
||||
@click="deleteCurrentDate">Datum löschen</button>
|
||||
<button @click="openNewDateDialog" class="btn-primary">Neu anlegen</button>
|
||||
@click="deleteCurrentDate">{{ $t('diary.deleteDate') }}</button>
|
||||
<button @click="openNewDateDialog" class="btn-primary">{{ $t('diary.createNew') }}</button>
|
||||
</label>
|
||||
<button
|
||||
class="btn-secondary gallery-trigger"
|
||||
:disabled="!currentClub || galleryLoading"
|
||||
@click="openGalleryDialog"
|
||||
>
|
||||
{{ galleryLoading ? 'Galerie wird erstellt…' : 'Mitglieder-Galerie' }}
|
||||
{{ galleryLoading ? $t('diary.galleryCreating') : $t('diary.gallery') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Training Group Selection Dialog -->
|
||||
<div v-if="showTrainingGroupDialog" class="dialog-overlay" @click.self="closeTrainingGroupDialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Trainingsgruppe auswählen</h3>
|
||||
<h3>{{ $t('diary.selectTrainingGroup') }}</h3>
|
||||
<div class="dialog-body">
|
||||
<label>
|
||||
<span>Trainingsgruppe:</span>
|
||||
<span>{{ $t('diary.selectTrainingGroup') }}:</span>
|
||||
<select v-model="selectedTrainingGroupId" class="input-field" @change="onTrainingGroupSelected">
|
||||
<option value="">Bitte wählen...</option>
|
||||
<option value="">{{ $t('diary.selectTrainingGroupPlaceholder') }}</option>
|
||||
<option v-for="group in trainingGroups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-if="suggestedDate" class="suggestion-info">
|
||||
<p><strong>Vorschlag:</strong></p>
|
||||
<p>Nächster Termin: <strong>{{ getFormattedDate(suggestedDate) }}</strong></p>
|
||||
<p><strong>{{ $t('diary.suggestion') }}:</strong></p>
|
||||
<p>{{ $t('diary.nextAppointment') }}: <strong>{{ getFormattedDate(suggestedDate) }}</strong></p>
|
||||
<p v-if="suggestedStartTime && suggestedEndTime">
|
||||
Zeit: <strong>{{ suggestedStartTime }} - {{ suggestedEndTime }}</strong>
|
||||
{{ $t('common.time') }}: <strong>{{ suggestedStartTime }} - {{ suggestedEndTime }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button @click="applySuggestion" class="btn-primary" :disabled="!suggestedDate">
|
||||
Vorschlag übernehmen
|
||||
{{ $t('diary.applySuggestion') }}
|
||||
</button>
|
||||
<button @click="closeTrainingGroupDialog" class="btn-secondary">
|
||||
Abbrechen
|
||||
{{ $t('messages.cancel') }}
|
||||
</button>
|
||||
<button @click="skipSuggestion" class="btn-secondary">
|
||||
Ohne Vorschlag fortfahren
|
||||
{{ $t('diary.skipSuggestion') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showForm">
|
||||
<h3>Neues Datum anlegen</h3>
|
||||
<h3>{{ $t('diary.createNewDate') }}</h3>
|
||||
<form @submit.prevent="createDate">
|
||||
<div>
|
||||
<label for="newDate">Datum:</label>
|
||||
<label for="newDate">{{ $t('diary.date') }}:</label>
|
||||
<input type="date" id="newDate" v-model="newDate" required />
|
||||
<button type="button" @click="setCurrentDate">Heute</button>
|
||||
<button type="button" @click="setCurrentDate">{{ $t('diary.today') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="trainingStart">Trainingsbeginn:</label>
|
||||
<label for="trainingStart">{{ $t('diary.trainingStart') }}:</label>
|
||||
<input type="time" step="300" id="trainingStart" v-model="trainingStart" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="trainingEnd">Trainingsende:</label>
|
||||
<label for="trainingEnd">{{ $t('diary.trainingEnd') }}:</label>
|
||||
<input type="time" step="300" id="trainingEnd" v-model="trainingEnd" />
|
||||
</div>
|
||||
<button type="submit">Datum anlegen</button>
|
||||
<button type="submit">{{ $t('diary.createDate') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="!showForm && date !== null">
|
||||
<h3>Trainingszeiten bearbeiten <span @click="toggleShowGeneralData" class="clickable">{{ showGeneralData ?
|
||||
<h3>{{ $t('diary.editTrainingTimes') }} <span @click="toggleShowGeneralData" class="clickable">{{ showGeneralData ?
|
||||
'-' : '+' }}</span></h3>
|
||||
<form @submit.prevent="updateTrainingTimes" v-if="showGeneralData">
|
||||
<div>
|
||||
<label for="editTrainingStart">Trainingsbeginn:</label>
|
||||
<label for="editTrainingStart">{{ $t('diary.trainingStart') }}:</label>
|
||||
<input type="time" step="300" id="editTrainingStart" v-model="trainingStart" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="editTrainingEnd">Trainingsende:</label>
|
||||
<label for="editTrainingEnd">{{ $t('diary.trainingEnd') }}:</label>
|
||||
<input type="time" step="300" id="editTrainingEnd" v-model="trainingEnd" />
|
||||
</div>
|
||||
<button type="submit">Zeiten aktualisieren</button>
|
||||
<button type="submit">{{ $t('diary.updateTimes') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="date !== null && !showForm" class="diary-content">
|
||||
@@ -99,28 +99,28 @@
|
||||
:class="{ active: activeTab === 'plan' }"
|
||||
@click="activeTab = 'plan'"
|
||||
>
|
||||
Trainingsplan
|
||||
{{ $t('diary.trainingPlan') }}
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'members' }"
|
||||
@click="activeTab = 'members'"
|
||||
>
|
||||
Teilnehmer
|
||||
{{ $t('diary.participants') }}
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'activities' }"
|
||||
@click="activeTab = 'activities'"
|
||||
>
|
||||
Aktivitäten
|
||||
{{ $t('diary.activities') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column" :class="{ 'mobile-tab-content': true, 'active': activeTab === 'plan' }">
|
||||
<h3 v-if="showGeneralData">Gruppenverwaltung</h3>
|
||||
<h3 v-if="showGeneralData">{{ $t('diary.groupManagement') }}</h3>
|
||||
<div v-if="showGeneralData">
|
||||
<h4>Vorhandene Gruppen</h4>
|
||||
<h4>{{ $t('diary.existingGroups') }}</h4>
|
||||
<ul>
|
||||
<li v-for="group in groups" :key="group.id">
|
||||
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)">{{ group.name
|
||||
@@ -128,7 +128,7 @@
|
||||
<input v-else type="text" v-model="group.name" @blur="saveGroup(group)"
|
||||
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
|
||||
style="display: inline;width:10em" />
|
||||
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)"> (Leiter: {{
|
||||
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)"> ({{ $t('diary.leader') }}: {{
|
||||
group.lead }}) </span>
|
||||
<input v-else type="text" v-model="group.lead" @blur="saveGroup(group)"
|
||||
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
|
||||
@@ -136,33 +136,33 @@
|
||||
<button v-if="editingGroupId !== group.id" @click="deleteGroup(group.id)"
|
||||
class="trash-btn"
|
||||
style="margin-left: 10px;"
|
||||
title="Gruppe löschen">🗑️</button>
|
||||
:title="$t('diary.deleteGroup')">🗑️</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="showGeneralData">
|
||||
<h4>Gruppen erstellen</h4>
|
||||
<h4>{{ $t('diary.createGroups') }}</h4>
|
||||
<div class="groups">
|
||||
<div>
|
||||
<label for="groupCount">Anzahl Gruppen:</label>
|
||||
<label for="groupCount">{{ $t('diary.numberOfGroups') }}:</label>
|
||||
<input type="number" id="groupCount" v-model="newGroupCount" :min="groups.length > 0 ? 1 : 2" max="10" required />
|
||||
</div>
|
||||
<div>
|
||||
<label> </label>
|
||||
<button type="submit" @click="createGroups">{{ groups.length > 0 ? 'Gruppe hinzufügen' : 'Gruppen erstellen' }}</button>
|
||||
<button type="submit" @click="createGroups">{{ groups.length > 0 ? $t('diary.addGroup') : $t('diary.createGroups') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Trainingsplan</h3>
|
||||
<h3>{{ $t('diary.trainingPlan') }}</h3>
|
||||
<div style="overflow: visible; position: relative;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th> <!-- Neue Spalte für Drag-Handle -->
|
||||
<th>Startzeit</th>
|
||||
<th>Aktivität / Zeitblock</th>
|
||||
<th>Gruppe</th>
|
||||
<th>Dauer (Min)</th>
|
||||
<th>{{ $t('diary.startTime') }}</th>
|
||||
<th>{{ $t('diary.activityOrTimeblock') }}</th>
|
||||
<th>{{ $t('diary.group') }}</th>
|
||||
<th>{{ $t('diary.durationMinutes') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -172,7 +172,7 @@
|
||||
<td class="drag-handle" style="cursor: move;">☰</td> <!-- Drag-Handle -->
|
||||
<td>{{ item.startTime }}</td>
|
||||
<td>
|
||||
<span v-if="item.isTimeblock"><i>Zeitblock</i></span>
|
||||
<span v-if="item.isTimeblock"><i>{{ $t('diary.timeblock') }}</i></span>
|
||||
<span v-else-if="editingActivityId === item.id">
|
||||
<div
|
||||
style="display: flex; gap: 5px; align-items: center;">
|
||||
@@ -202,7 +202,7 @@
|
||||
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
|
||||
<span v-if="hasActivityVisual(item.predefinedActivity)"
|
||||
@click.stop="openActivityVisual(item.predefinedActivity)"
|
||||
class="image-icon" title="Bild/Zeichnung anzeigen">🖼️</span>
|
||||
class="image-icon" :title="$t('diary.showImage')">🖼️</span>
|
||||
{{ (item.predefinedActivity && item.predefinedActivity.code &&
|
||||
item.predefinedActivity.code.trim() !== '')
|
||||
? item.predefinedActivity.code
|
||||
@@ -220,7 +220,7 @@
|
||||
placeholder="z.B. 2x7" style="width: 80px;" />
|
||||
<input type="number" v-model="editingDuration"
|
||||
@keyup.enter="saveActivityEdit(item)"
|
||||
style="width: 60px;" placeholder="Min" />
|
||||
style="width: 60px;" :placeholder="$t('diary.min')" />
|
||||
</div>
|
||||
</span>
|
||||
<span v-else class="clickable" @click="startActivityEdit(item)">
|
||||
@@ -231,23 +231,23 @@
|
||||
</td>
|
||||
<td>
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<button v-if="!item.isTimeblock" @click="toggleActivityMembers(item)" title="Teilnehmer zuordnen"
|
||||
<button v-if="!item.isTimeblock" @click="toggleActivityMembers(item)" :title="$t('diary.assignParticipants')"
|
||||
class="person-btn">👤</button>
|
||||
<button v-if="item.isTimeblock" @click="addGroupActivityToTimeblock(item.id)" title="Gruppen-Aktivität hinzufügen"
|
||||
class="btn-primary" style="font-size: 12px; padding: 2px 6px;">+ Gruppe</button>
|
||||
<button v-if="item.isTimeblock" @click="addGroupActivityToTimeblock(item.id)" :title="$t('diary.addGroupActivity')"
|
||||
class="btn-primary" style="font-size: 12px; padding: 2px 6px;">{{ $t('diary.addGroupButton') }}</button>
|
||||
<button @click="removePlanItem(item.id)" class="trash-btn">🗑️</button>
|
||||
<div v-if="activityMembersOpenId === item.id" class="dropdown"
|
||||
style="max-height: 12em; padding: 0.25rem;">
|
||||
<div style="margin-bottom: 0.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>Teilnehmer zuordnen</span>
|
||||
<span>{{ $t('diary.assignParticipants') }}</span>
|
||||
<button @click="assignAllMembersToActivity(item.id)"
|
||||
style="font-size: 10px; padding: 1px 4px; background: #28a745; color: white; border: none; border-radius: 2px;">
|
||||
Alle
|
||||
{{ $t('diary.all') }}
|
||||
</button>
|
||||
<select v-if="groups.length > 0"
|
||||
@change="assignGroupToActivity(item.id, $event.target.value)"
|
||||
style="font-size: 10px; width: 80px;">
|
||||
<option value="">Gruppe...</option>
|
||||
<option value="">{{ $t('diary.group') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
@@ -274,12 +274,12 @@
|
||||
<td colspan="2">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; padding: 0.5rem; background: #f0f0f0; border-radius: 4px;">
|
||||
<select v-model="newPlanItem.groupId" style="flex: 1;">
|
||||
<option value="">Gruppe auswählen...</option>
|
||||
<option value="">{{ $t('diary.selectGroup') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}</option>
|
||||
</select>
|
||||
<div style="flex: 1; position: relative;">
|
||||
<input type="text" v-model="newPlanItem.activity" placeholder="Aktivität"
|
||||
<input type="text" v-model="newPlanItem.activity" :placeholder="$t('diary.activityPlaceholder')"
|
||||
@input="onNewItemInputChange" style="width: 100%;" />
|
||||
<div v-if="newItemShowDropdown && newItemSearchResults.length"
|
||||
class="dropdown" style="max-height: 9.5em;">
|
||||
@@ -308,7 +308,7 @@
|
||||
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
|
||||
<span v-if="hasActivityVisual(groupItem.groupPredefinedActivity)"
|
||||
@click.stop="openActivityVisual(groupItem.groupPredefinedActivity)"
|
||||
class="image-icon" title="Bild/Zeichnung anzeigen">🖼️</span>
|
||||
class="image-icon" :title="$t('diary.showImage')">🖼️</span>
|
||||
|
||||
{{ (groupItem.groupPredefinedActivity &&
|
||||
groupItem.groupPredefinedActivity.code &&
|
||||
@@ -321,21 +321,21 @@
|
||||
<td></td>
|
||||
<td>
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<button @click="toggleGroupActivityMembers(groupItem)" title="Teilnehmer zuordnen"
|
||||
<button @click="toggleGroupActivityMembers(groupItem)" :title="$t('diary.assignParticipants')"
|
||||
class="person-btn">👤</button>
|
||||
<button @click="removeGroupActivity(groupItem.id)" class="trash-btn" title="Löschen">🗑️</button>
|
||||
<button @click="removeGroupActivity(groupItem.id)" class="trash-btn" :title="$t('diary.delete')">🗑️</button>
|
||||
<div v-if="groupActivityMembersOpenId === groupItem.id" class="dropdown"
|
||||
style="max-height: 12em; padding: 0.25rem;">
|
||||
<div style="margin-bottom: 0.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>Teilnehmer zuordnen</span>
|
||||
<span>{{ $t('diary.assignParticipants') }}</span>
|
||||
<button @click="assignAllMembersToGroupActivity(groupItem.id)"
|
||||
style="font-size: 10px; padding: 1px 4px; background: #28a745; color: white; border: none; border-radius: 2px;">
|
||||
Alle
|
||||
{{ $t('diary.all') }}
|
||||
</button>
|
||||
<select v-if="groups.length > 0"
|
||||
@change="assignGroupToGroupActivity(groupItem.id, $event.target.value)"
|
||||
style="font-size: 10px; width: 80px;">
|
||||
<option value="">Gruppe...</option>
|
||||
<option value="">{{ $t('diary.group') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
@@ -362,10 +362,10 @@
|
||||
<td></td>
|
||||
<td>{{ calculateNextTime }}</td>
|
||||
<td colspan="4" v-if="!addNewItem && !addNewTimeblock && !addNewGroupActivity">
|
||||
<button @click="openNewPlanItem()">Gesamt-Aktivität</button>
|
||||
<button @click="addTimeblock()">Zeitblock</button>
|
||||
<button @click="openNewPlanItem()">{{ $t('diary.overallActivity') }}</button>
|
||||
<button @click="addTimeblock()">{{ $t('diary.addTimeblock') }}</button>
|
||||
<button v-if="parentIsTimeblock()"
|
||||
@click="addGroupActivity">Gruppen-Aktivität</button>
|
||||
@click="addGroupActivity">{{ $t('diary.addGroupActivity') }}</button>
|
||||
</td>
|
||||
<td v-if="addNewItem || addNewGroupActivity">
|
||||
<div v-if="addtype === 'activity'" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
@@ -373,14 +373,14 @@
|
||||
type="button"
|
||||
class="btn-palette"
|
||||
@click="showDrawingDialog = true"
|
||||
title="Übungszeichnung erstellen"
|
||||
:title="$t('diary.createDrawing')"
|
||||
style="margin: 0; margin-left: 0;"
|
||||
>
|
||||
🎨
|
||||
</button>
|
||||
<div style="flex: 1; position: relative;">
|
||||
<input type="text" v-model="newPlanItem.activity"
|
||||
placeholder="Aktivität / Zeitblock" required
|
||||
:placeholder="$t('diary.activityOrTimeblock')" required
|
||||
@input="onNewItemInputChange"
|
||||
style="width: 100%;" />
|
||||
<div v-if="newItemShowDropdown && newItemSearchResults.length"
|
||||
@@ -394,11 +394,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td v-else-if="addNewTimeblock">Zeitblock</td>
|
||||
<td v-else-if="addNewTimeblock">{{ $t('diary.timeblock') }}</td>
|
||||
<td v-if="addNewGroupActivity" colspan="2">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<select v-model="newPlanItem.groupId" style="flex: 1;">
|
||||
<option value="">Gruppe auswählen...</option>
|
||||
<option value="">{{ $t('diary.selectGroup') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}</option>
|
||||
</select>
|
||||
@@ -420,7 +420,7 @@
|
||||
<td v-if="(addNewItem || addNewTimeblock) && !addNewGroupActivity">
|
||||
<input type="text" v-model="newPlanItem.durationText" @input="calculateDuration"
|
||||
placeholder="z.B. 2x7 oder 3*5" style="width:10em" />
|
||||
<input type="number" v-model="newPlanItem.duration" placeholder="Minuten" />
|
||||
<input type="number" v-model="newPlanItem.duration" :placeholder="$t('diary.minutes')" />
|
||||
</td>
|
||||
<td v-else-if="addNewGroupActivity"></td>
|
||||
<td v-if="addNewItem || addNewTimeblock || addNewGroupActivity">
|
||||
@@ -432,12 +432,10 @@
|
||||
</table>
|
||||
<div style="margin-top: 1rem; margin-bottom: 2rem; padding-bottom: 1rem;">
|
||||
<button v-if="trainingPlan && trainingPlan.length && trainingPlan.length > 0"
|
||||
@click="generatePDF">Trainingsplan als PDF
|
||||
herunterladen</button>
|
||||
@click="generatePDF">{{ $t('diary.trainingPlanAsPDF') }} {{ $t('common.download') }}</button>
|
||||
<button v-if="date && participants && participants.length > 0"
|
||||
@click="generateTrainingDayPDF"
|
||||
style="margin-left: 1rem;">Trainingstag als PDF
|
||||
herunterladen</button>
|
||||
style="margin-left: 1rem;">{{ $t('diary.trainingDayAsPDF') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,11 +443,11 @@
|
||||
<!-- Mobile: Nur Mitglieder-Tab -->
|
||||
<div class="mobile-tab-section" :class="{ 'active': activeTab === 'members' }" v-show="activeTab === 'members'">
|
||||
<div>
|
||||
<button @click="addAccident">Unfall buchen</button>
|
||||
<button @click="addAccident">{{ $t('diary.bookAccident') }}</button>
|
||||
<div v-if="accidents.length > 0">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Teilnehmer ({{ participants.length }})</h3>
|
||||
<h3>{{ $t('diary.participants') }} ({{ participants.length }})</h3>
|
||||
<ul>
|
||||
<li v-for="member in sortedMembers()" :key="member.id" class="checkbox-item participant-row"
|
||||
:class="{
|
||||
@@ -462,8 +460,8 @@
|
||||
:checked="isParticipant(member.id)">
|
||||
</label>
|
||||
<span class="clickable participant-name" @click.stop="openNotesModal(member)">
|
||||
<span v-if="member && member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" title="6 oder mehr Trainingsteilnahmen">🛑</span>
|
||||
<span v-else-if="member && member.testMembership && member.trainingParticipations >= 3" class="warning-icon" title="3 oder mehr Trainingsteilnahmen">⚠️</span>
|
||||
<span v-if="member && member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" :title="$t('members.sixOrMoreParticipations')">🛑</span>
|
||||
<span v-else-if="member && member.testMembership && member.trainingParticipations >= 3" class="warning-icon" :title="$t('members.threeOrMoreParticipations')">⚠️</span>
|
||||
{{
|
||||
member ? member.firstName : ''
|
||||
}} {{
|
||||
@@ -488,7 +486,7 @@
|
||||
<span v-if="member.testMembership === true && member.memberFormHandedOver !== true"
|
||||
@click.stop="markFormHandedOver(member)"
|
||||
class="pointer form-handover-icon"
|
||||
title="Mitgliedsformular ausgehändigt">
|
||||
:title="$t('diary.formHandedOver')">
|
||||
📄
|
||||
</span>
|
||||
<span class="pointer" @click="openTagInfos(member)">ℹ️</span>
|
||||
@@ -496,23 +494,23 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div class="add-participant">
|
||||
<button @click="openQuickAddDialog" class="quick-add-btn">+ Schnell hinzufügen</button>
|
||||
<button @click="openQuickAddDialog" class="quick-add-btn">{{ $t('diary.quickAdd') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile: Nur Aktivitäten-Tab -->
|
||||
<div class="mobile-tab-section" :class="{ 'active': activeTab === 'activities' }" v-show="activeTab === 'activities'">
|
||||
<h3 class="clickable" @click="toggleActivitiesBox">Aktivitäten <span>{{ showActivitiesBox ? '-' :
|
||||
<h3 class="clickable" @click="toggleActivitiesBox">{{ $t('diary.activities') }} <span>{{ showActivitiesBox ? '-' :
|
||||
'+' }}</span></h3>
|
||||
<div v-if="showActivitiesBox" class="collapsible-box">
|
||||
<textarea v-model="newActivity"></textarea>
|
||||
<button @click="addActivity">Aktivität hinzufügen</button>
|
||||
<button @click="addActivity">{{ $t('diary.addActivity') }}</button>
|
||||
<ul>
|
||||
<li v-for="activity in activities" :key="activity.id">
|
||||
{{ activity.description }}
|
||||
</li>
|
||||
</ul>
|
||||
<multiselect v-model="selectedActivityTags" :options="availableTags"
|
||||
placeholder="Tags auswählen" label="name" track-by="id" multiple :close-on-select="true"
|
||||
:placeholder="$t('diary.selectTags')" label="name" track-by="id" multiple :close-on-select="true"
|
||||
@tag="addNewTag" @remove="removeActivityTag" :allow-empty="false"
|
||||
@keydown.enter.prevent="addNewTagFromInput" />
|
||||
</div>
|
||||
@@ -520,26 +518,26 @@
|
||||
<!-- Desktop: Beide Bereiche immer sichtbar -->
|
||||
<div class="desktop-sidebar">
|
||||
<div>
|
||||
<button @click="addAccident">Unfall buchen</button>
|
||||
<button @click="addAccident">{{ $t('diary.bookAccident') }}</button>
|
||||
<div v-if="accidents.length > 0">
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="clickable" @click="toggleActivitiesBox">Aktivitäten <span>{{ showActivitiesBox ? '-' :
|
||||
<h3 class="clickable" @click="toggleActivitiesBox">{{ $t('diary.activities') }} <span>{{ showActivitiesBox ? '-' :
|
||||
'+' }}</span></h3>
|
||||
<div v-if="showActivitiesBox" class="collapsible-box">
|
||||
<textarea v-model="newActivity"></textarea>
|
||||
<button @click="addActivity">Aktivität hinzufügen</button>
|
||||
<button @click="addActivity">{{ $t('diary.addActivity') }}</button>
|
||||
<ul>
|
||||
<li v-for="activity in activities" :key="activity.id">
|
||||
{{ activity.description }}
|
||||
</li>
|
||||
</ul>
|
||||
<multiselect v-model="selectedActivityTags" :options="availableTags"
|
||||
placeholder="Tags auswählen" label="name" track-by="id" multiple :close-on-select="true"
|
||||
:placeholder="$t('diary.selectTags')" label="name" track-by="id" multiple :close-on-select="true"
|
||||
@tag="addNewTag" @remove="removeActivityTag" :allow-empty="false"
|
||||
@keydown.enter.prevent="addNewTagFromInput" />
|
||||
</div>
|
||||
<h3>Teilnehmer ({{ participants.length }})</h3>
|
||||
<h3>{{ $t('diary.participants') }} ({{ participants.length }})</h3>
|
||||
<ul>
|
||||
<li v-for="member in sortedMembers()" :key="member.id" class="checkbox-item participant-row"
|
||||
:class="{
|
||||
@@ -578,7 +576,7 @@
|
||||
<span v-if="member.testMembership === true && member.memberFormHandedOver !== true"
|
||||
@click.stop="markFormHandedOver(member)"
|
||||
class="pointer form-handover-icon"
|
||||
title="Mitgliedsformular ausgehändigt">
|
||||
:title="$t('diary.formHandedOver')">
|
||||
📄
|
||||
</span>
|
||||
<span class="pointer" @click="openTagInfos(member)">ℹ️</span>
|
||||
@@ -586,7 +584,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div class="add-participant">
|
||||
<button @click="openQuickAddDialog" class="quick-add-btn">+ Schnell hinzufügen</button>
|
||||
<button @click="openQuickAddDialog" class="quick-add-btn">{{ $t('diary.quickAdd') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -630,14 +628,14 @@
|
||||
<!-- Image Dialog für normale Bilder -->
|
||||
<ImageDialog
|
||||
v-model="showImage"
|
||||
title="Aktivitätsbild"
|
||||
:title="$t('diary.activityImage')"
|
||||
:image-url="imageUrl"
|
||||
/>
|
||||
|
||||
<!-- Dialog für gerenderte Zeichnungen -->
|
||||
<BaseDialog
|
||||
v-model="showRenderModal"
|
||||
title="Aktivitätszeichnung"
|
||||
:title="$t('diary.activityDrawing')"
|
||||
size="large"
|
||||
>
|
||||
<div class="render-container">
|
||||
@@ -1220,7 +1218,7 @@ export default {
|
||||
// Direkt auf das leere Tagebuch des neuen Datums wechseln
|
||||
await this.handleDateChange();
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1232,9 +1230,9 @@ export default {
|
||||
trainingStart: this.trainingStart || null,
|
||||
trainingEnd: this.trainingEnd || null,
|
||||
});
|
||||
this.showInfo('Erfolg', 'Trainingszeiten erfolgreich aktualisiert.', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('diary.trainingTimesUpdated'), '', 'success');
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1399,7 +1397,7 @@ export default {
|
||||
const response = await apiClient.get('/predefined-activities');
|
||||
this.predefinedActivities = response.data;
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der vordefinierten Aktivitäten', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorLoadingPredefinedActivities'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1459,10 +1457,10 @@ export default {
|
||||
await apiClient.post(`/clubmembers/set/${this.currentClub}`, memberData);
|
||||
// Lokales Member-Objekt aktualisieren
|
||||
member.memberFormHandedOver = true;
|
||||
this.showInfo('Erfolg', 'Mitgliedsformular als ausgehändigt markiert', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('diary.formMarkedAsHandedOver'), '', 'success');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Markieren des Formulars:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Markieren des Mitgliedsformulars', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorMarkingForm'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1507,7 +1505,7 @@ export default {
|
||||
label: tag.tag.label
|
||||
}));
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
this.doMemberTagUpdates = true;
|
||||
},
|
||||
@@ -1523,7 +1521,7 @@ export default {
|
||||
this.newNoteContent = '';
|
||||
this.selectedTagsNotes = [];
|
||||
} else {
|
||||
this.showInfo('Hinweis', 'Bitte wählen Sie einen Teilnehmer aus und geben Sie einen Notiztext ein.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.note'), this.$t('diary.selectParticipantAndNote'), '', 'warning');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1552,7 +1550,7 @@ export default {
|
||||
this.availableTags.push(newTag);
|
||||
this.selectedActivityTags.push(newTag);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1571,7 +1569,7 @@ export default {
|
||||
this.selectedMemberTags.push(newTag);
|
||||
await this.linkTagToMemberAndDate(newTag);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1586,7 +1584,7 @@ export default {
|
||||
tagId: tagId
|
||||
});
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1599,7 +1597,7 @@ export default {
|
||||
tagId: tagId
|
||||
});
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1616,7 +1614,7 @@ export default {
|
||||
}
|
||||
this.previousActivityTags = [...selectedTags];
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1632,7 +1630,7 @@ export default {
|
||||
}
|
||||
this.previousMemberTags = [...this.selectedMemberTags];
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1645,7 +1643,7 @@ export default {
|
||||
});
|
||||
this.selectedMemberTags = this.selectedMemberTags.filter(tag => tag.id !== tagId);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1658,7 +1656,7 @@ export default {
|
||||
});
|
||||
this.notes = this.notes.filter(note => note.content !== noteContent);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1670,7 +1668,7 @@ export default {
|
||||
});
|
||||
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== tagId);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1696,7 +1694,7 @@ export default {
|
||||
const list = await apiClient.get(`/diary/${this.currentClub}`).then(r => r.data);
|
||||
if (!list.some(e => String(e.id) === String(this.date?.id))) {
|
||||
await this.refreshDates();
|
||||
this.showInfo('Hinweis', 'Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.note'), this.$t('diary.dateNoLongerCurrent'), '', 'warning');
|
||||
return;
|
||||
}
|
||||
await apiClient.post(`/diary-date-activities/${this.currentClub}`, {
|
||||
@@ -1709,7 +1707,7 @@ export default {
|
||||
});
|
||||
} else if (this.addNewGroupActivity) {
|
||||
if (!this.newPlanItem.groupId || !this.newPlanItem.activity) {
|
||||
this.showInfo('Hinweis', 'Bitte wählen Sie eine Gruppe und geben Sie eine Aktivität ein.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.note'), this.$t('diary.selectGroupAndActivity'), '', 'warning');
|
||||
return;
|
||||
}
|
||||
await apiClient.post(`/diary-date-activities/group`, {
|
||||
@@ -1741,10 +1739,10 @@ export default {
|
||||
|
||||
async deleteCurrentDate() {
|
||||
if (!this.canDeleteCurrentDate) {
|
||||
this.showInfo('Hinweis', 'Datum kann nicht gelöscht werden', 'Es sind noch Inhalte vorhanden (Trainingplan, Teilnehmer, Aktivitäten, Unfälle oder Notizen).', 'warning');
|
||||
this.showInfo(this.$t('messages.note'), this.$t('diary.dateCannotBeDeleted'), this.$t('diary.dateCannotBeDeletedDetails'), 'warning');
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.showConfirm('Löschen bestätigen', 'Möchten Sie dieses Datum wirklich löschen?', 'Alle zugehörigen Daten werden ebenfalls gelöscht.', 'danger');
|
||||
const confirmed = await this.showConfirm(this.$t('diary.confirmDelete'), this.$t('diary.confirmDeleteDate'), this.$t('diary.confirmDeleteDateDetails'), 'danger');
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.delete(`/diary/${this.currentClub}/${this.date.id}`);
|
||||
@@ -1767,7 +1765,7 @@ export default {
|
||||
});
|
||||
this.calculateIntermediateTimes();
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1876,7 +1874,7 @@ export default {
|
||||
await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`);
|
||||
this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
async removeGroupActivity(groupActivityId) {
|
||||
@@ -1893,7 +1891,7 @@ export default {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
async onDragEnd(evt) {
|
||||
@@ -1904,7 +1902,7 @@ export default {
|
||||
});
|
||||
this.recalculateTimes();
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
/* async loadMemberImage(member) {
|
||||
@@ -1919,7 +1917,7 @@ export default {
|
||||
}
|
||||
},*/
|
||||
async generatePDF() {
|
||||
const pdf = new PDFGenerator();
|
||||
const pdf = new PDFGenerator(20, 10, this.$t);
|
||||
pdf.addTrainingPlan(this.currentClubName, this.date.date, this.trainingStart, this.trainingEnd, this.trainingPlan);
|
||||
pdf.save('trainingsplan.pdf');
|
||||
},
|
||||
@@ -1950,7 +1948,7 @@ export default {
|
||||
console.log('[generateTrainingDayPDF] groupActivityMembersMap keys:', Object.keys(this.groupActivityMembersMap || {}));
|
||||
console.log('[generateTrainingDayPDF] participantMapByMemberId:', this.participantMapByMemberId);
|
||||
|
||||
const pdf = new PDFGenerator();
|
||||
const pdf = new PDFGenerator(20, 10, this.$t);
|
||||
pdf.addTrainingDaySummary(
|
||||
this.currentClubName,
|
||||
this.date.date,
|
||||
@@ -2069,7 +2067,7 @@ export default {
|
||||
try {
|
||||
// Validierung: Wenn keine Gruppen existieren, müssen mindestens 2 erstellt werden
|
||||
if (this.groups.length === 0 && this.newGroupCount < 2) {
|
||||
this.showInfo('Fehler', 'Beim ersten Erstellen müssen mindestens 2 Gruppen erstellt werden!', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.mustCreateAtLeastTwoGroups'), '', 'error');
|
||||
this.newGroupCount = 2;
|
||||
return;
|
||||
}
|
||||
@@ -2096,12 +2094,12 @@ export default {
|
||||
// Setze newGroupCount zurück: 1 wenn bereits Gruppen existieren, 2 wenn keine
|
||||
this.newGroupCount = this.groups.length > 0 ? 1 : 2;
|
||||
const message = countCreated === 1
|
||||
? '1 Gruppe wurde erfolgreich hinzugefügt!'
|
||||
: `${countCreated} Gruppen wurden erfolgreich erstellt!`;
|
||||
this.showInfo('Erfolg', message, '', 'success');
|
||||
? this.$t('diary.oneGroupAdded')
|
||||
: this.$t('diary.groupsCreated', { count: countCreated });
|
||||
this.showInfo(this.$t('messages.success'), message, '', 'success');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Gruppen:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Erstellen der Gruppen', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorCreatingGroups'), '', 'error');
|
||||
}
|
||||
},
|
||||
parentIsTimeblock() {
|
||||
@@ -2154,7 +2152,7 @@ export default {
|
||||
});
|
||||
this.editingGroupId = null;
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorOccurred'), '', 'error');
|
||||
}
|
||||
},
|
||||
cancelEditGroup() {
|
||||
@@ -2167,7 +2165,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`);
|
||||
const confirmed = confirm(this.$t('diary.confirmDeleteGroup', { name: group.name }));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
@@ -2180,10 +2178,10 @@ export default {
|
||||
});
|
||||
|
||||
await this.loadGroups();
|
||||
this.showInfo('Erfolg', 'Gruppe wurde erfolgreich gelöscht!', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('diary.groupDeletedSuccessfully'), '', 'success');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Gruppe:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Löschen der Gruppe', '', 'error');
|
||||
console.error(this.$t('diary.errorDeletingGroup'), error);
|
||||
this.showInfo(this.$t('messages.error'), this.$t('diary.errorDeletingGroup'), '', 'error');
|
||||
}
|
||||
},
|
||||
async openTagInfos(member) {
|
||||
|
||||
@@ -3,223 +3,214 @@
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-card card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Willkommen im TrainingsTagebuch</h2>
|
||||
<h2 class="card-title">{{ $t('home.welcome') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="!isAuthenticated" class="marketing">
|
||||
<section class="hero">
|
||||
<h1 class="hero-title">Vereinsverwaltung, Trainingsplanung und Turniere – alles an einem Ort</h1>
|
||||
<h1 class="hero-title">{{ $t('home.heroTitle') }}</h1>
|
||||
<p class="hero-subtitle">
|
||||
Das TrainingsTagebuch ist die umfassende Lösung für Vereine: Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten,
|
||||
Trainingstagebuch, Turnierorganisation, Team-Management, MyTischtennis-Integration, Statistiken und mehr –
|
||||
DSGVO‑konform und einfach zu bedienen.
|
||||
{{ $t('home.heroSubtitle') }}
|
||||
</p>
|
||||
<div class="auth-actions">
|
||||
<router-link to="/register" class="btn-primary">
|
||||
<span class="btn-icon">🚀</span>
|
||||
Kostenlos starten
|
||||
{{ $t('home.startFree') }}
|
||||
</router-link>
|
||||
<router-link to="/login" class="btn-secondary">
|
||||
<span class="btn-icon">🔐</span>
|
||||
Einloggen
|
||||
{{ $t('navigation.login') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<ul class="hero-bullets">
|
||||
<li>✔️ Mitglieder- und Gruppenverwaltung</li>
|
||||
<li>✔️ Trainingsgruppen & Trainingszeiten</li>
|
||||
<li>✔️ Trainingstagebuch & Dokumentation</li>
|
||||
<li>✔️ Turniere (intern, offen, offiziell)</li>
|
||||
<li>✔️ Team-Management & Ligen</li>
|
||||
<li>✔️ MyTischtennis-Integration</li>
|
||||
<li>✔️ Statistiken & Auswertungen</li>
|
||||
<li>✔️ Rollen, Berechtigungen & DSGVO</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.memberManagement') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.trainingGroups') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.trainingDiary') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.tournaments') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.teamManagement') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.myTischtennis') }}</li>
|
||||
<li>✔️ {{ $t('home.heroFeatures.statistics') }}</li>
|
||||
<li>✔️ {{ $t('home.rolesAndPrivacy') }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<h3 class="section-title">Was kannst du mit dem TrainingsTagebuch machen?</h3>
|
||||
<h3 class="section-title">{{ $t('home.whatCanYouDo') }}</h3>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Mitglieder verwalten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.manageMembers') }}</h4>
|
||||
<p class="feature-description">
|
||||
Erstelle Mitgliedsprofile, bilde Gruppen und halte Kontakt‑ und Freigabestände aktuell.
|
||||
{{ $t('home.features.manageMembersDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📝</div>
|
||||
<h4 class="feature-title">Trainingstagebuch führen</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.keepDiary') }}</h4>
|
||||
<p class="feature-description">
|
||||
Dokumentiere Inhalte, Umfang und Anwesenheiten jeder Einheit – nachvollziehbar und strukturiert.
|
||||
{{ $t('home.features.keepDiaryDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📅</div>
|
||||
<h4 class="feature-title">Spielpläne organisieren</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.organizeSchedules') }}</h4>
|
||||
<p class="feature-description">
|
||||
Plane Spiele, Turniere und Veranstaltungen inklusive Gruppen, Runden und Ergebnissen.
|
||||
{{ $t('home.features.organizeSchedulesDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h4 class="feature-title">Statistiken & Auswertung</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.statistics') }}</h4>
|
||||
<p class="feature-description">
|
||||
Erhalte Trainings‑ und Teilnahmeübersichten, erkenne Entwicklung und plane gezielt.
|
||||
{{ $t('home.features.statisticsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h4 class="feature-title">Sicherheit & DSGVO</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.security') }}</h4>
|
||||
<p class="feature-description">
|
||||
Datenschutzfreundliche Architektur, Freigaben durch Mitglieder und transparente Zugriffe.
|
||||
{{ $t('home.features.securityDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">⚙️</div>
|
||||
<h4 class="feature-title">Vordefinierte Aktivitäten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.predefinedActivities') }}</h4>
|
||||
<p class="feature-description">
|
||||
Nutze Vorlagen für wiederkehrende Übungen und beschleunige deine Dokumentation.
|
||||
{{ $t('home.features.predefinedActivitiesDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Trainingsgruppen & Zeiten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.trainingGroups') }}</h4>
|
||||
<p class="feature-description">
|
||||
Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
|
||||
{{ $t('home.features.trainingGroupsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🏅</div>
|
||||
<h4 class="feature-title">Offizielle Turniere</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.officialTournaments') }}</h4>
|
||||
<p class="feature-description">
|
||||
Importiere und verwalte offizielle Turniere, verwalte Teilnahmen und Ergebnisse.
|
||||
{{ $t('home.features.officialTournamentsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👔</div>
|
||||
<h4 class="feature-title">Team-Management</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.teamManagement') }}</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
|
||||
{{ $t('home.features.teamManagementDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h4 class="feature-title">MyTischtennis-Integration</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.myTischtennisIntegration') }}</h4>
|
||||
<p class="feature-description">
|
||||
Automatische Synchronisation mit MyTischtennis.de für Spielergebnisse und Statistiken.
|
||||
{{ $t('home.features.myTischtennisIntegrationDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📄</div>
|
||||
<h4 class="feature-title">PDF-Export</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.pdfExport') }}</h4>
|
||||
<p class="feature-description">
|
||||
Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.
|
||||
{{ $t('home.features.pdfExportDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<h4 class="feature-title">Berechtigungssystem</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.permissionSystem') }}</h4>
|
||||
<p class="feature-description">
|
||||
Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied) mit individuellen Berechtigungen.
|
||||
{{ $t('home.features.permissionSystemDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📋</div>
|
||||
<h4 class="feature-title">Aktivitätsprotokoll</h4>
|
||||
<h4 class="feature-title">{{ $t('home.features.activityLog') }}</h4>
|
||||
<p class="feature-description">
|
||||
Vollständiges Logging aller Aktionen für Transparenz und Nachvollziehbarkeit.
|
||||
{{ $t('home.features.activityLogDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="how-it-works">
|
||||
<h3 class="section-title">So funktioniert es</h3>
|
||||
<h3 class="section-title">{{ $t('home.howItWorks') }}</h3>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<h4 class="step-title">Registrieren</h4>
|
||||
<p>Lege kostenlos einen Account an und aktiviere ihn per E‑Mail.</p>
|
||||
<h4 class="step-title">{{ $t('home.step1.title') }}</h4>
|
||||
<p>{{ $t('home.step1.description') }}</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<h4 class="step-title">Verein anlegen</h4>
|
||||
<p>Erstelle deinen Verein, lade Mitglieder ein und richte Gruppen ein.</p>
|
||||
<h4 class="step-title">{{ $t('home.step2.title') }}</h4>
|
||||
<p>{{ $t('home.step2.description') }}</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<h4 class="step-title">Planen & dokumentieren</h4>
|
||||
<p>Plane Termine, dokumentiere Trainings und verfolge Fortschritte.</p>
|
||||
<h4 class="step-title">{{ $t('home.step3.title') }}</h4>
|
||||
<p>{{ $t('home.step3.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="seo-copy">
|
||||
<h3 class="section-title">Für wen ist das TrainingsTagebuch?</h3>
|
||||
<h3 class="section-title">{{ $t('home.forWhom') }}</h3>
|
||||
<p class="long-text">
|
||||
Das TrainingsTagebuch ist die umfassende Plattform für Vereine, Abteilungen und Trainerteams.
|
||||
Es vereint Mitgliederverwaltung mit Trainingsgruppen und -zeiten, detailliertes Trainingstagebuch,
|
||||
umfassende Turnierorganisation (interne, offene und offizielle Turniere), Team-Management mit Liga-Integration,
|
||||
MyTischtennis-Synchronisation, aussagekräftige Statistiken und Auswertungen sowie ein flexibles
|
||||
Berechtigungssystem in einer modernen Web‑Anwendung. Durch klare Rollen (Admin, Trainer, Mannschaftsführer, Mitglied)
|
||||
und individuelle Berechtigungen behalten Verantwortliche die Kontrolle, während Mitglieder selbstbestimmt mitwirken können.
|
||||
Ideal für Mannschafts‑, Racket‑ und Individualsportarten – vom Nachwuchs bis zum Leistungsbereich.
|
||||
DSGVO‑konform mit transparenten Freigaben und vollständigem Aktivitätsprotokoll.
|
||||
{{ $t('home.forWhomText') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="faq">
|
||||
<h3 class="section-title">Häufige Fragen</h3>
|
||||
<h3 class="section-title">{{ $t('home.faq') }}</h3>
|
||||
<details>
|
||||
<summary>Ist die Nutzung kostenlos?</summary>
|
||||
<p>Ja, du kannst kostenlos starten. Erweiterungen können später folgen.</p>
|
||||
<summary>{{ $t('home.faqFree.question') }}</summary>
|
||||
<p>{{ $t('home.faqFree.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Wie steht es um den Datenschutz?</summary>
|
||||
<p>Wir setzen auf Datensparsamkeit, transparente Freigaben, rollenbasierte Zugriffe und vollständiges Aktivitätsprotokoll. Die Anwendung ist DSGVO‑konform.</p>
|
||||
<summary>{{ $t('home.faqPrivacy.question') }}</summary>
|
||||
<p>{{ $t('home.faqPrivacy.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Benötige ich eine Installation?</summary>
|
||||
<p>Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser – auf Desktop, Tablet und Smartphone.</p>
|
||||
<summary>{{ $t('home.faqInstallation.question') }}</summary>
|
||||
<p>{{ $t('home.faqInstallation.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Welche Turnierarten werden unterstützt?</summary>
|
||||
<p>Du kannst interne Turniere, offene Turniere und offizielle Turniere (z.B. von Verbänden) verwalten. Offizielle Turniere können importiert werden.</p>
|
||||
<summary>{{ $t('home.faqTournaments.question') }}</summary>
|
||||
<p>{{ $t('home.faqTournaments.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Funktioniert die MyTischtennis-Integration automatisch?</summary>
|
||||
<p>Ja, nach der Einrichtung synchronisiert sich die Anwendung automatisch mit MyTischtennis.de und importiert Spielergebnisse und Statistiken.</p>
|
||||
<summary>{{ $t('home.faqMyTischtennis.question') }}</summary>
|
||||
<p>{{ $t('home.faqMyTischtennis.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Kann ich Trainingsgruppen und -zeiten verwalten?</summary>
|
||||
<p>Ja, du kannst Trainingsgruppen anlegen, Trainingszeiten definieren und Mitglieder den Gruppen zuordnen. Das Trainingstagebuch schlägt automatisch passende Gruppen und Zeiten vor.</p>
|
||||
<summary>{{ $t('home.faqTrainingGroups.question') }}</summary>
|
||||
<p>{{ $t('home.faqTrainingGroups.answer') }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Wie funktioniert das Berechtigungssystem?</summary>
|
||||
<p>Es gibt vier Rollen: Admin, Trainer, Mannschaftsführer und Mitglied. Jede Rolle hat spezifische Berechtigungen, die individuell angepasst werden können.</p>
|
||||
<summary>{{ $t('home.faqPermissions.question') }}</summary>
|
||||
<p>{{ $t('home.faqPermissions.answer') }}</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<div class="cta-bottom">
|
||||
<router-link to="/register" class="btn-primary">
|
||||
<span class="btn-icon">✅</span>
|
||||
Jetzt kostenlos registrieren
|
||||
{{ $t('home.registerNow') }}
|
||||
</router-link>
|
||||
<router-link to="/login" class="btn-secondary">
|
||||
<span class="btn-icon">🔐</span>
|
||||
Ich habe schon einen Account
|
||||
{{ $t('home.haveAccount') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,12 +220,12 @@
|
||||
<span class="avatar-icon">👋</span>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
Herzlich Willkommen zurück! Du bist erfolgreich eingeloggt.
|
||||
{{ $t('home.welcomeBack') }}
|
||||
</p>
|
||||
<div class="user-actions">
|
||||
<button @click="logout" class="btn-secondary">
|
||||
<span class="btn-icon">🚪</span>
|
||||
Ausloggen
|
||||
{{ $t('navigation.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,69 +234,69 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isAuthenticated" class="features-section">
|
||||
<h3 class="section-title">Was kannst du hier machen?</h3>
|
||||
<h3 class="section-title">{{ $t('home.whatCanYouDo') }}</h3>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Mitglieder verwalten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.manageMembers') }}</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte deine Vereinsmitglieder, erstelle Trainingsgruppen und behalte den Überblick über alle Teilnehmer.
|
||||
{{ $t('home.authenticatedFeatures.manageMembersDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📝</div>
|
||||
<h4 class="feature-title">Trainingstagebuch führen</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.keepDiary') }}</h4>
|
||||
<p class="feature-description">
|
||||
Dokumentiere Trainingsaktivitäten, Teilnehmer, Aktivitäten und Notizen für jeden Trainingstag.
|
||||
{{ $t('home.authenticatedFeatures.keepDiaryDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Trainingsgruppen & Zeiten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.trainingGroups') }}</h4>
|
||||
<p class="feature-description">
|
||||
Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
|
||||
{{ $t('home.authenticatedFeatures.trainingGroupsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🏆</div>
|
||||
<h4 class="feature-title">Turniere verwalten</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.manageTournaments') }}</h4>
|
||||
<p class="feature-description">
|
||||
Erstelle interne und offene Turniere, importiere offizielle Turniere und verwalte Teilnahmen.
|
||||
{{ $t('home.authenticatedFeatures.manageTournamentsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👔</div>
|
||||
<h4 class="feature-title">Team-Management</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.teamManagement') }}</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
|
||||
{{ $t('home.authenticatedFeatures.teamManagementDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h4 class="feature-title">MyTischtennis-Integration</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.myTischtennisIntegration') }}</h4>
|
||||
<p class="feature-description">
|
||||
Synchronisiere automatisch Spielergebnisse und Statistiken mit MyTischtennis.de.
|
||||
{{ $t('home.authenticatedFeatures.myTischtennisIntegrationDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h4 class="feature-title">Statistiken & Auswertungen</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.statistics') }}</h4>
|
||||
<p class="feature-description">
|
||||
Erhalte detaillierte Trainings- und Teilnahmeübersichten sowie Aktivitätsstatistiken.
|
||||
{{ $t('home.authenticatedFeatures.statisticsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📄</div>
|
||||
<h4 class="feature-title">PDF-Export</h4>
|
||||
<h4 class="feature-title">{{ $t('home.authenticatedFeatures.pdfExport') }}</h4>
|
||||
<p class="feature-description">
|
||||
Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.
|
||||
{{ $t('home.authenticatedFeatures.pdfExportDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Impressum</h1>
|
||||
<p class="back-home"><router-link to="/">Zur Startseite</router-link></p>
|
||||
<h1>{{ $t('legal.imprint') }}</h1>
|
||||
<p class="back-home"><router-link to="/">{{ $t('legal.backToHome') }}</router-link></p>
|
||||
<section>
|
||||
<h2>Diensteanbieter</h2>
|
||||
<p>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Login</h2>
|
||||
<h2>{{ $t('auth.login') }}</h2>
|
||||
<form @submit.prevent="executeLogin">
|
||||
<input v-model="email" type="email" placeholder="Email" required />
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Login</button>
|
||||
<input v-model="email" type="email" :placeholder="$t('auth.email')" required />
|
||||
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
|
||||
<button type="submit">{{ $t('auth.login') }}</button>
|
||||
</form>
|
||||
<div class="register-link">
|
||||
<p>Noch kein Konto? <router-link to="/register">Registrieren</router-link></p>
|
||||
<p>{{ $t('auth.noAccount') }} <router-link to="/register">{{ $t('auth.register') }}</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,8 +98,8 @@ export default {
|
||||
await this.login({ token: response.data.token, username: this.email });
|
||||
this.$router.push('/');
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Login fehlgeschlagen. Bitte Zugangsdaten prüfen und erneut versuchen.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('auth.loginFailed'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<template>
|
||||
<div class="logs-view">
|
||||
<div class="header">
|
||||
<h1>System-Logs</h1>
|
||||
<p class="subtitle">Übersicht über alle API-Requests, Responses und Ausführungen</p>
|
||||
<h1>{{ $t('logs.title') }}</h1>
|
||||
<p class="subtitle">{{ $t('logs.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<div class="filter-controls">
|
||||
<div class="filter-group">
|
||||
<label>Backend:</label>
|
||||
<label>{{ $t('logs.backend') }}:</label>
|
||||
<select v-model="filters.backend" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="mytischtennis">myTischtennis</option>
|
||||
<option value="own">Eigenes Backend</option>
|
||||
<option value="">{{ $t('logs.all') }}</option>
|
||||
<option value="mytischtennis">{{ $t('logs.mytischtennis') }}</option>
|
||||
<option value="own">{{ $t('logs.ownBackend') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Log-Typ:</label>
|
||||
<label>{{ $t('logs.logType') }}:</label>
|
||||
<select v-model="filters.logType" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="api_request">API-Requests</option>
|
||||
<option value="scheduler">Scheduler</option>
|
||||
<option value="cron_job">Cron-Jobs</option>
|
||||
<option value="manual">Manuelle Ausführungen</option>
|
||||
<option value="">{{ $t('logs.all') }}</option>
|
||||
<option value="api_request">{{ $t('logs.apiRequests') }}</option>
|
||||
<option value="scheduler">{{ $t('logs.scheduler') }}</option>
|
||||
<option value="cron_job">{{ $t('logs.cronJobs') }}</option>
|
||||
<option value="manual">{{ $t('logs.manual') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>HTTP-Methode:</label>
|
||||
<label>{{ $t('logs.httpMethod') }}:</label>
|
||||
<select v-model="filters.method" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="">{{ $t('logs.all') }}</option>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
@@ -40,9 +40,9 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Status:</label>
|
||||
<label>{{ $t('logs.status') }}:</label>
|
||||
<select v-model="filters.statusCode" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="">{{ $t('logs.all') }}</option>
|
||||
<option value="200">200 - OK</option>
|
||||
<option value="400">400 - Bad Request</option>
|
||||
<option value="401">401 - Unauthorized</option>
|
||||
@@ -53,27 +53,27 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Pfad:</label>
|
||||
<label>{{ $t('logs.path') }}:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="filters.path"
|
||||
placeholder="z.B. /api/diary"
|
||||
:placeholder="$t('logs.pathPlaceholder')"
|
||||
class="filter-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Von:</label>
|
||||
<label>{{ $t('logs.from') }}:</label>
|
||||
<input type="date" v-model="filters.startDate" class="filter-input" />
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Bis:</label>
|
||||
<label>{{ $t('logs.to') }}:</label>
|
||||
<input type="date" v-model="filters.endDate" class="filter-input" />
|
||||
</div>
|
||||
|
||||
<button @click="applyFilters" class="btn-primary">Filter anwenden</button>
|
||||
<button @click="clearFilters" class="btn-secondary">Zurücksetzen</button>
|
||||
<button @click="applyFilters" class="btn-primary">{{ $t('logs.applyFilters') }}</button>
|
||||
<button @click="clearFilters" class="btn-secondary">{{ $t('logs.clearFilters') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,29 +83,29 @@
|
||||
<span class="status-icon">{{ statusIcon }}</span>
|
||||
<span class="status-text">
|
||||
<template v-if="!loading && !error">
|
||||
<strong>Erfolgreich geladen</strong> – {{ total }} Datensätze gefunden, {{ logs.length }} angezeigt
|
||||
<strong>{{ $t('logs.successfullyLoaded') }}</strong> – {{ total }} {{ $t('logs.recordsFound') }}, {{ logs.length }} {{ $t('logs.displayed') }}
|
||||
</template>
|
||||
<template v-else-if="loading">
|
||||
Lade Logs...
|
||||
{{ $t('logs.loadingLogs') }}
|
||||
</template>
|
||||
<template v-else-if="error">
|
||||
<strong>Fehler:</strong> {{ error }}
|
||||
<strong>{{ $t('logs.error') }}:</strong> {{ error }}
|
||||
</template>
|
||||
</span>
|
||||
<span class="status-time">{{ formatLastLoadTime(lastLoadTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !lastLoadTime" class="loading">Lade Logs...</div>
|
||||
<div v-if="loading && !lastLoadTime" class="loading">{{ $t('logs.loadingLogs') }}</div>
|
||||
<div v-else-if="error && !lastLoadTime" class="error">{{ error }}</div>
|
||||
<div v-else class="logs-content">
|
||||
<div class="logs-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Gesamt:</span>
|
||||
<span class="stat-label">{{ $t('logs.total') }}:</span>
|
||||
<span class="stat-value">{{ total }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Angezeigt:</span>
|
||||
<span class="stat-label">{{ $t('logs.displayedLabel') }}:</span>
|
||||
<span class="stat-value">{{ logs.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,14 +114,14 @@
|
||||
<table class="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Typ</th>
|
||||
<th>Methode</th>
|
||||
<th>Pfad</th>
|
||||
<th>Status</th>
|
||||
<th>Ausführungszeit</th>
|
||||
<th>Fehler</th>
|
||||
<th>Aktionen</th>
|
||||
<th>{{ $t('logs.time') }}</th>
|
||||
<th>{{ $t('logs.type') }}</th>
|
||||
<th>{{ $t('logs.method') }}</th>
|
||||
<th>{{ $t('logs.path') }}</th>
|
||||
<th>{{ $t('logs.status') }}</th>
|
||||
<th>{{ $t('logs.executionTime') }}</th>
|
||||
<th>{{ $t('logs.errorLabel') }}</th>
|
||||
<th>{{ $t('logs.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -152,7 +152,7 @@
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="viewLogDetails(log)" class="btn-view">Details</button>
|
||||
<button @click="viewLogDetails(log)" class="btn-view">{{ $t('logs.details') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -165,17 +165,17 @@
|
||||
:disabled="offset === 0"
|
||||
class="btn-pagination"
|
||||
>
|
||||
← Vorherige
|
||||
← {{ $t('logs.previous') }}
|
||||
</button>
|
||||
<span class="page-info">
|
||||
Seite {{ currentPage }} von {{ totalPages }}
|
||||
{{ $t('logs.page') }} {{ currentPage }} {{ $t('logs.of') }} {{ totalPages }}
|
||||
</span>
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="offset + logs.length >= total"
|
||||
class="btn-pagination"
|
||||
>
|
||||
Nächste →
|
||||
{{ $t('logs.next') }} →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,6 +193,7 @@
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import apiClient from '../apiClient.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
|
||||
@@ -202,6 +203,7 @@ export default {
|
||||
InfoDialog
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const logs = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
@@ -269,12 +271,12 @@ export default {
|
||||
error.value = null;
|
||||
lastLoadTime.value = new Date();
|
||||
} else {
|
||||
error.value = 'Fehler beim Laden der Logs';
|
||||
error.value = t('logs.errorLoading');
|
||||
lastLoadTime.value = new Date();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading logs:', err);
|
||||
error.value = err.response?.data?.error || 'Fehler beim Laden der Logs';
|
||||
error.value = err.response?.data?.error || t('logs.errorLoading');
|
||||
lastLoadTime.value = new Date();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -322,7 +324,7 @@ export default {
|
||||
const logDetails = response.data.data;
|
||||
logDetailsDialog.value = {
|
||||
isOpen: true,
|
||||
title: `Log-Details #${log.id}`,
|
||||
title: `${t('logs.logDetails')} #${log.id}`,
|
||||
message: `${logDetails.method} ${logDetails.path}`,
|
||||
details: formatLogDetails(logDetails),
|
||||
type: logDetails.statusCode >= 400 ? 'error' : 'info'
|
||||
@@ -334,37 +336,37 @@ export default {
|
||||
};
|
||||
|
||||
const formatLogDetails = (log) => {
|
||||
let details = `Zeit: ${formatDate(log.createdAt)}\n`;
|
||||
details += `Typ: ${getLogTypeLabel(log.logType)}\n`;
|
||||
details += `Methode: ${log.method}\n`;
|
||||
details += `Pfad: ${log.path}\n`;
|
||||
let details = `${t('logs.logDetailsLabels.time')}: ${formatDate(log.createdAt)}\n`;
|
||||
details += `${t('logs.logDetailsLabels.type')}: ${getLogTypeLabel(log.logType)}\n`;
|
||||
details += `${t('logs.logDetailsLabels.method')}: ${log.method}\n`;
|
||||
details += `${t('logs.logDetailsLabels.path')}: ${log.path}\n`;
|
||||
|
||||
if (log.statusCode) {
|
||||
details += `Status: ${log.statusCode}\n`;
|
||||
details += `${t('logs.logDetailsLabels.status')}: ${log.statusCode}\n`;
|
||||
}
|
||||
|
||||
if (log.executionTime) {
|
||||
details += `Ausführungszeit: ${formatExecutionTime(log.executionTime)}\n`;
|
||||
details += `${t('logs.logDetailsLabels.executionTime')}: ${formatExecutionTime(log.executionTime)}\n`;
|
||||
}
|
||||
|
||||
if (log.ipAddress) {
|
||||
details += `IP: ${log.ipAddress}\n`;
|
||||
details += `${t('logs.logDetailsLabels.ip')}: ${log.ipAddress}\n`;
|
||||
}
|
||||
|
||||
if (log.schedulerJobType) {
|
||||
details += `Scheduler-Job: ${log.schedulerJobType}\n`;
|
||||
details += `${t('logs.logDetailsLabels.schedulerJob')}: ${log.schedulerJobType}\n`;
|
||||
}
|
||||
|
||||
if (log.errorMessage) {
|
||||
details += `\nFehler:\n${log.errorMessage}\n`;
|
||||
details += `\n${t('logs.logDetailsLabels.error')}:\n${log.errorMessage}\n`;
|
||||
}
|
||||
|
||||
if (log.requestBody) {
|
||||
details += `\nRequest Body:\n${log.requestBody}\n`;
|
||||
details += `\n${t('logs.logDetailsLabels.requestBody')}:\n${log.requestBody}\n`;
|
||||
}
|
||||
|
||||
if (log.responseBody) {
|
||||
details += `\nResponse Body:\n${log.responseBody}\n`;
|
||||
details += `\n${t('logs.logDetailsLabels.responseBody')}:\n${log.responseBody}\n`;
|
||||
}
|
||||
|
||||
return details;
|
||||
@@ -391,10 +393,10 @@ export default {
|
||||
|
||||
const getLogTypeLabel = (type) => {
|
||||
const labels = {
|
||||
api_request: 'API-Request',
|
||||
scheduler: 'Scheduler',
|
||||
cron_job: 'Cron-Job',
|
||||
manual: 'Manuell'
|
||||
api_request: t('logs.logTypeLabels.api_request'),
|
||||
scheduler: t('logs.logTypeLabels.scheduler'),
|
||||
cron_job: t('logs.logTypeLabels.cron_job'),
|
||||
manual: t('logs.logTypeLabels.manual')
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
@@ -437,9 +439,9 @@ export default {
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diff < 5) return 'gerade eben';
|
||||
if (diff < 60) return `vor ${diff} Sekunden`;
|
||||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Minuten`;
|
||||
if (diff < 5) return t('logs.justNow');
|
||||
if (diff < 60) return t('logs.secondsAgo', { n: diff });
|
||||
if (diff < 3600) return t('logs.minutesAgo', { n: Math.floor(diff / 60) });
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +1,52 @@
|
||||
<template>
|
||||
<div class="member-transfer-settings">
|
||||
<h2>Mitgliederübertragung - Einstellungen</h2>
|
||||
<h2>{{ $t('memberTransfer.title') }}</h2>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
Konfiguration wird geladen...
|
||||
{{ $t('memberTransfer.loadingConfig') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="settings-container">
|
||||
<div class="settings-card">
|
||||
<h3>Übertragungskonfiguration</h3>
|
||||
<h3>{{ $t('memberTransfer.transferConfiguration') }}</h3>
|
||||
<p class="info-text">
|
||||
Konfigurieren Sie hier die Einstellungen für die Übertragung von Mitgliedern an externe Systeme.
|
||||
Diese Einstellungen werden vereinsspezifisch gespeichert.
|
||||
{{ $t('memberTransfer.configInfo') }}
|
||||
</p>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Server-Konfiguration</h4>
|
||||
<h4>{{ $t('memberTransfer.serverConfiguration') }}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="server">Server-Basis-URL: <span class="required">*</span></label>
|
||||
<label for="server">{{ $t('memberTransfer.serverBaseUrl') }}: <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="server"
|
||||
v-model="config.server"
|
||||
placeholder="https://example.com"
|
||||
:placeholder="$t('memberTransfer.serverBaseUrlPlaceholder')"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<span class="hint">Basis-URL des Servers (z.B. https://example.com)</span>
|
||||
<span class="hint">{{ $t('memberTransfer.serverBaseUrlHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Login-Konfiguration</h4>
|
||||
<h4>{{ $t('memberTransfer.loginConfiguration') }}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loginEndpoint">Login-Endpoint Pfad:</label>
|
||||
<label for="loginEndpoint">{{ $t('memberTransfer.loginEndpointPath') }}:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="loginEndpoint"
|
||||
v-model="config.loginEndpoint"
|
||||
placeholder="/api/auth/login"
|
||||
:placeholder="$t('memberTransfer.loginEndpointPlaceholder')"
|
||||
class="form-input"
|
||||
/>
|
||||
<span class="hint">Optional: Relativer Pfad zum Login-Endpoint (z.B. /api/auth/login)</span>
|
||||
<span class="hint">{{ $t('memberTransfer.loginEndpointHint') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loginFormat">Login-Format:</label>
|
||||
<label for="loginFormat">{{ $t('memberTransfer.loginFormat') }}:</label>
|
||||
<select id="loginFormat" v-model="config.loginFormat" class="form-select">
|
||||
<option value="json">JSON</option>
|
||||
<option value="form-data">Form Data</option>
|
||||
@@ -56,19 +55,19 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Login-Daten:</label>
|
||||
<label>{{ $t('memberTransfer.loginData') }}:</label>
|
||||
<div class="credentials-group">
|
||||
<div class="credential-row">
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.username"
|
||||
placeholder="Benutzername / Email"
|
||||
:placeholder="$t('memberTransfer.usernameEmail')"
|
||||
class="form-input"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
v-model="loginCredentials.password"
|
||||
placeholder="Passwort"
|
||||
:placeholder="$t('memberTransfer.password')"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -76,40 +75,40 @@
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.additionalField1"
|
||||
placeholder="Zusätzliches Feld (z.B. client_id)"
|
||||
:placeholder="$t('memberTransfer.additionalFieldExample1')"
|
||||
class="form-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.additionalField2"
|
||||
placeholder="Zusätzliches Feld (z.B. client_secret)"
|
||||
:placeholder="$t('memberTransfer.additionalFieldExample2')"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hint">Passwörter werden verschlüsselt gespeichert</span>
|
||||
<span class="hint">{{ $t('memberTransfer.passwordEncrypted') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Übertragungs-Konfiguration</h4>
|
||||
<h4>{{ $t('memberTransfer.transferConfigurationTitle') }}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferEndpoint">Übertragungs-Endpoint Pfad: <span class="required">*</span></label>
|
||||
<label for="transferEndpoint">{{ $t('memberTransfer.transferEndpointPath') }}: <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="transferEndpoint"
|
||||
v-model="config.transferEndpoint"
|
||||
placeholder="/api/members/bulk"
|
||||
:placeholder="$t('memberTransfer.transferEndpointPlaceholder')"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<span class="hint">Relativer Pfad zum Übertragungs-Endpoint (z.B. /api/members/bulk)</span>
|
||||
<span class="hint">{{ $t('memberTransfer.transferEndpointHint') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="transferMethod">HTTP-Methode:</label>
|
||||
<label for="transferMethod">{{ $t('memberTransfer.httpMethod') }}:</label>
|
||||
<select id="transferMethod" v-model="config.transferMethod" class="form-select">
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
@@ -118,7 +117,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferFormat">Übertragungs-Format:</label>
|
||||
<label for="transferFormat">{{ $t('memberTransfer.transferFormat') }}:</label>
|
||||
<select id="transferFormat" v-model="config.transferFormat" class="form-select">
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
@@ -131,20 +130,20 @@
|
||||
<div class="form-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="config.useBulkMode" />
|
||||
<span>Bulk-Import-Modus (alle Mitglieder auf einmal übertragen)</span>
|
||||
<span>{{ $t('memberTransfer.bulkMode') }}</span>
|
||||
</label>
|
||||
<span class="hint">Wenn aktiviert, werden alle Mitglieder in einem Request als Array übertragen.</span>
|
||||
<span class="hint">{{ $t('memberTransfer.bulkModeHint') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Template aus vollständigem Beispiel importieren:</label>
|
||||
<label>{{ $t('memberTransfer.importTemplate') }}:</label>
|
||||
<div class="template-import">
|
||||
<textarea
|
||||
ref="importTextarea"
|
||||
v-model="importTemplate"
|
||||
rows="8"
|
||||
class="form-textarea import-area"
|
||||
placeholder='Fügen Sie hier ein vollständiges Beispiel-Template ein, z.B.: { "members": [ { "firstName": "Max", "lastName": "Mustermann", "email": "max@example.com" } ] }'
|
||||
:placeholder="$t('memberTransfer.importTemplatePlaceholder')"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
@@ -152,28 +151,26 @@
|
||||
@click="parseAndImportTemplate"
|
||||
:disabled="!importTemplate.trim()"
|
||||
>
|
||||
Template analysieren und importieren
|
||||
{{ $t('memberTransfer.analyzeAndImport') }}
|
||||
</button>
|
||||
<span class="hint">Fügen Sie ein vollständiges Beispiel-Template (mit Beispiel-Mitgliedern) ein. Das System erkennt automatisch das Mitglied-Template und das Bulk-Wrapper-Template.</span>
|
||||
<span class="hint">{{ $t('memberTransfer.importTemplateHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="config.useBulkMode">
|
||||
<label for="bulkWrapperTemplate">Bulk-Wrapper-Template (optional):</label>
|
||||
<label for="bulkWrapperTemplate">{{ $t('memberTransfer.bulkWrapperTemplate') }}:</label>
|
||||
<div class="template-explanation">
|
||||
<p class="template-intro">
|
||||
<strong>Was ist ein Bulk-Wrapper-Template?</strong><br>
|
||||
Optional können Sie die äußere Struktur definieren, in die die Mitglieder-Array eingefügt wird.
|
||||
Verwenden Sie <code>{{members}}</code> als Platzhalter für das Array der Mitglieder.
|
||||
<strong>{{ $t('memberTransfer.bulkWrapperWhat') }}</strong><br>
|
||||
{{ $t('memberTransfer.bulkWrapperDescription') }}
|
||||
</p>
|
||||
<div class="template-examples">
|
||||
<div class="example-section">
|
||||
<strong>Beispiel:</strong>
|
||||
<strong>{{ $t('memberTransfer.example') }}:</strong>
|
||||
<pre class="example-code">{{ bulkWrapperExample }}</pre>
|
||||
</div>
|
||||
<div class="bulk-mode-note">
|
||||
<strong>ℹ️ Hinweis:</strong> Wenn kein Wrapper-Template angegeben wird, wird automatisch
|
||||
<code>{"members": [...]}</code> verwendet.
|
||||
<strong>ℹ️ {{ $t('memberTransfer.bulkWrapperNote') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,21 +181,21 @@
|
||||
v-model="config.bulkWrapperTemplate"
|
||||
rows="6"
|
||||
class="form-textarea"
|
||||
placeholder='{"members": [{{members}}]}'
|
||||
:placeholder="$t('memberTransfer.bulkWrapperPlaceholder')"
|
||||
@focus="onBulkWrapperFocus"
|
||||
@click="onBulkWrapperClick"
|
||||
></textarea>
|
||||
<div class="template-help">
|
||||
<strong>Verfügbare Platzhalter:</strong>
|
||||
<strong>{{ $t('memberTransfer.availablePlaceholders') }}:</strong>
|
||||
<div class="placeholders-grid">
|
||||
<button
|
||||
type="button"
|
||||
class="placeholder-button"
|
||||
title="Platzhalter für das Mitglieder-Array"
|
||||
:title="$t('memberTransfer.membersArray')"
|
||||
@click="insertPlaceholder('{{members}}', 'bulkWrapper')"
|
||||
>
|
||||
<code>{{members}}</code>
|
||||
<span>Mitglieder-Array</span>
|
||||
<span>{{ $t('memberTransfer.membersArray') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,31 +203,28 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferTemplate">Übertragungs-Template: <span class="required">*</span></label>
|
||||
<label for="transferTemplate">{{ $t('memberTransfer.transferTemplate') }}: <span class="required">*</span></label>
|
||||
<div class="template-explanation">
|
||||
<p class="template-intro">
|
||||
<strong>Was ist ein Template?</strong><br>
|
||||
Das Template definiert das Format, in dem die Mitgliederdaten an das externe System übertragen werden.
|
||||
Verwenden Sie Platzhalter wie <code>{{firstName}}</code>, um die Daten automatisch zu ersetzen.
|
||||
<strong>{{ $t('memberTransfer.templateWhat') }}</strong><br>
|
||||
{{ $t('memberTransfer.templateDescription') }}
|
||||
</p>
|
||||
<div class="template-examples">
|
||||
<div class="example-section">
|
||||
<strong>Beispiel für JSON-Format (empfohlen):</strong>
|
||||
<strong>{{ $t('memberTransfer.exampleJson') }}:</strong>
|
||||
<pre class="example-code">{{ jsonExample }}</pre>
|
||||
</div>
|
||||
<div class="example-section" v-if="config.transferFormat === 'xml'">
|
||||
<strong>Beispiel für XML-Format:</strong>
|
||||
<strong>{{ $t('memberTransfer.exampleXml') }}:</strong>
|
||||
<pre class="example-code">{{ xmlExample }}</pre>
|
||||
</div>
|
||||
<div class="example-section" v-if="config.transferFormat === 'form-data' || config.transferFormat === 'x-www-form-urlencoded'">
|
||||
<strong>Beispiel für Form-Data Format:</strong>
|
||||
<p class="example-hint">Für Form-Data verwenden Sie ein einfaches Text-Template mit Zeilen im Format <code>feldname=platzhalter</code>:</p>
|
||||
<strong>{{ $t('memberTransfer.exampleFormData') }}:</strong>
|
||||
<p class="example-hint">{{ $t('memberTransfer.formDataHint') }}</p>
|
||||
<pre class="example-code">{{ formDataExample }}</pre>
|
||||
</div>
|
||||
<div class="bulk-mode-note" v-if="config.useBulkMode">
|
||||
<strong>ℹ️ Bulk-Modus aktiv:</strong> Das Template definiert das Format für <strong>ein einzelnes Mitglied</strong>.
|
||||
Die Mitglieder werden automatisch in ein Array gewrappt.
|
||||
Die äußere Struktur können Sie optional im "Bulk-Wrapper-Template" definieren (siehe unten).
|
||||
<strong>ℹ️ {{ $t('memberTransfer.bulkModeActive') }}:</strong> {{ $t('memberTransfer.bulkModeActiveDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,8 +241,8 @@
|
||||
required
|
||||
></textarea>
|
||||
<div class="template-help">
|
||||
<strong>Verfügbare Platzhalter:</strong>
|
||||
<p class="placeholder-hint">Klicken Sie auf einen Platzhalter, um ihn an der aktuellen Cursor-Position einzufügen:</p>
|
||||
<strong>{{ $t('memberTransfer.availablePlaceholders') }}:</strong>
|
||||
<p class="placeholder-hint">{{ $t('memberTransfer.placeholderHint') }}</p>
|
||||
<div class="placeholders-grid">
|
||||
<button
|
||||
v-for="placeholder in placeholders"
|
||||
@@ -263,8 +257,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="template-tip">
|
||||
<strong>💡 Tipp:</strong> Setzen Sie den Cursor an die gewünschte Stelle im Template und klicken Sie auf einen Platzhalter, um ihn einzufügen.
|
||||
Platzhalter werden beim Übertragen automatisch durch die tatsächlichen Mitgliederdaten ersetzt.
|
||||
<strong>💡 {{ $t('memberTransfer.templateTip') }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,10 +266,10 @@
|
||||
|
||||
<div class="actions">
|
||||
<button @click="saveConfig" class="btn-primary" :disabled="!isValid || saving">
|
||||
{{ saving ? 'Speichere...' : 'Speichern' }}
|
||||
{{ saving ? $t('memberTransfer.saving') : $t('memberTransfer.save') }}
|
||||
</button>
|
||||
<button @click="deleteConfig" class="btn-danger" :disabled="!hasConfig || deleting">
|
||||
{{ deleting ? 'Lösche...' : 'Konfiguration löschen' }}
|
||||
{{ deleting ? $t('memberTransfer.deleting') : $t('memberTransfer.deleteConfig') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,6 +368,24 @@ address={{address}}`;
|
||||
} else {
|
||||
return 'firstName={{firstName}}\nlastName={{lastName}}\nemail={{email}}';
|
||||
}
|
||||
},
|
||||
|
||||
placeholders() {
|
||||
return [
|
||||
{ code: '{{firstName}}', label: this.$t('memberTransfer.placeholders.firstName'), description: this.$t('memberTransfer.placeholders.firstNameDesc') },
|
||||
{ code: '{{lastName}}', label: this.$t('memberTransfer.placeholders.lastName'), description: this.$t('memberTransfer.placeholders.lastNameDesc') },
|
||||
{ code: '{{fullName}}', label: this.$t('memberTransfer.placeholders.fullName'), description: this.$t('memberTransfer.placeholders.fullNameDesc') },
|
||||
{ code: '{{email}}', label: this.$t('memberTransfer.placeholders.email'), description: this.$t('memberTransfer.placeholders.emailDesc') },
|
||||
{ code: '{{phone}}', label: this.$t('memberTransfer.placeholders.phone'), description: this.$t('memberTransfer.placeholders.phoneDesc') },
|
||||
{ code: '{{street}}', label: this.$t('memberTransfer.placeholders.street'), description: this.$t('memberTransfer.placeholders.streetDesc') },
|
||||
{ code: '{{city}}', label: this.$t('memberTransfer.placeholders.city'), description: this.$t('memberTransfer.placeholders.cityDesc') },
|
||||
{ code: '{{birthDate}}', label: this.$t('memberTransfer.placeholders.birthDate'), description: this.$t('memberTransfer.placeholders.birthDateDesc') },
|
||||
{ code: '{{geburtsdatum}}', label: this.$t('memberTransfer.placeholders.birthDateAlt'), description: this.$t('memberTransfer.placeholders.birthDateAltDesc') },
|
||||
{ code: '{{address}}', label: this.$t('memberTransfer.placeholders.address'), description: this.$t('memberTransfer.placeholders.addressDesc') },
|
||||
{ code: '{{ttr}}', label: this.$t('memberTransfer.placeholders.ttr'), description: this.$t('memberTransfer.placeholders.ttrDesc') },
|
||||
{ code: '{{qttr}}', label: this.$t('memberTransfer.placeholders.qttr'), description: this.$t('memberTransfer.placeholders.qttrDesc') },
|
||||
{ code: '{{gender}}', label: this.$t('memberTransfer.placeholders.gender'), description: this.$t('memberTransfer.placeholders.genderDesc') }
|
||||
];
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -393,7 +404,7 @@ address={{address}}`;
|
||||
details: '',
|
||||
type: 'info',
|
||||
confirmText: 'OK',
|
||||
cancelText: 'Abbrechen',
|
||||
cancelText: '',
|
||||
showCancel: true,
|
||||
resolveCallback: null
|
||||
},
|
||||
@@ -403,21 +414,6 @@ address={{address}}`;
|
||||
configId: null,
|
||||
currentTextarea: 'template', // 'template' oder 'bulkWrapper'
|
||||
importTemplate: '',
|
||||
placeholders: [
|
||||
{ code: '{{firstName}}', label: 'Vorname', description: 'Vorname des Mitglieds' },
|
||||
{ code: '{{lastName}}', label: 'Nachname', description: 'Nachname des Mitglieds' },
|
||||
{ code: '{{fullName}}', label: 'Vollständiger Name', description: 'Vorname und Nachname kombiniert' },
|
||||
{ code: '{{email}}', label: 'E-Mail-Adresse', description: 'E-Mail-Adresse des Mitglieds' },
|
||||
{ code: '{{phone}}', label: 'Telefonnummer', description: 'Telefonnummer des Mitglieds' },
|
||||
{ code: '{{street}}', label: 'Straße', description: 'Straße und Hausnummer' },
|
||||
{ code: '{{city}}', label: 'Ort', description: 'Wohnort' },
|
||||
{ code: '{{birthDate}}', label: 'Geburtsdatum', description: 'Geburtsdatum im Format YYYY-MM-DD' },
|
||||
{ code: '{{geburtsdatum}}', label: 'Geburtsdatum (alt)', description: 'Geburtsdatum im Format YYYY-MM-DD (alternative Bezeichnung)' },
|
||||
{ code: '{{address}}', label: 'Kombinierte Adresse', description: 'Straße und Ort kombiniert' },
|
||||
{ code: '{{ttr}}', label: 'TTR-Wert', description: 'TTR-Wert des Mitglieds' },
|
||||
{ code: '{{qttr}}', label: 'QTTR-Wert', description: 'QTTR-Wert des Mitglieds' },
|
||||
{ code: '{{gender}}', label: 'Geschlecht', description: 'Geschlecht des Mitglieds' }
|
||||
],
|
||||
config: {
|
||||
server: '',
|
||||
loginEndpoint: '',
|
||||
@@ -459,8 +455,10 @@ address={{address}}`;
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
resolveCallback: resolve,
|
||||
...options,
|
||||
confirmText: options.confirmText || 'OK',
|
||||
cancelText: options.cancelText || this.$t('common.cancel'),
|
||||
showCancel: options.showCancel !== false,
|
||||
resolveCallback: resolve
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -510,7 +508,7 @@ address={{address}}`;
|
||||
// Keine Konfiguration vorhanden - das ist OK
|
||||
} else {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error);
|
||||
await this.showInfo('Fehler', 'Fehler beim Laden der Konfiguration', getSafeErrorMessage(error), 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('memberTransfer.errorLoading'), getSafeErrorMessage(error), 'error');
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -569,18 +567,18 @@ address={{address}}`;
|
||||
|
||||
if (response.data.success) {
|
||||
this.configId = response.data.config?.id || null;
|
||||
const successMessage = getSafeMessage(response.data?.message, 'Konfiguration erfolgreich gespeichert!');
|
||||
await this.showInfo('Erfolg', successMessage, '', 'success');
|
||||
const successMessage = getSafeMessage(response.data?.message, this.$t('memberTransfer.configSaved'));
|
||||
await this.showInfo(this.$t('messages.success'), successMessage, '', 'success');
|
||||
|
||||
// Passwort-Feld leeren nach erfolgreichem Speichern
|
||||
this.loginCredentials.password = '';
|
||||
} else {
|
||||
const errorMessage = getSafeMessage(response.data?.error, 'Fehler beim Speichern');
|
||||
await this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
const errorMessage = getSafeMessage(response.data?.error, this.$t('memberTransfer.errorSaving'));
|
||||
await this.showInfo(this.$t('messages.error'), errorMessage, '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
await this.showInfo('Fehler', 'Fehler beim Speichern', getSafeErrorMessage(error), 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('memberTransfer.errorSaving'), getSafeErrorMessage(error), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -591,7 +589,7 @@ address={{address}}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.showConfirm('Konfiguration löschen', 'Möchten Sie die Konfiguration wirklich löschen?', '', 'danger');
|
||||
const confirmed = await this.showConfirm(this.$t('memberTransfer.confirmDelete'), this.$t('memberTransfer.confirmDeleteMessage'), '', 'danger');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
@@ -621,13 +619,13 @@ address={{address}}`;
|
||||
additionalField1: '',
|
||||
additionalField2: ''
|
||||
};
|
||||
await this.showInfo('Erfolg', getSafeMessage(response.data?.message, 'Konfiguration erfolgreich gelöscht!'), '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), getSafeMessage(response.data?.message, this.$t('memberTransfer.configDeleted')), '', 'success');
|
||||
} else {
|
||||
await this.showInfo('Fehler', getSafeMessage(response.data?.error, 'Fehler beim Löschen'), '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), getSafeMessage(response.data?.error, this.$t('memberTransfer.errorDeleting')), '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
await this.showInfo('Fehler', 'Fehler beim Löschen', getSafeErrorMessage(error), 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('memberTransfer.errorDeleting'), getSafeErrorMessage(error), 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
@@ -763,7 +761,7 @@ address={{address}}`;
|
||||
// Import-Bereich leeren
|
||||
this.importTemplate = '';
|
||||
|
||||
await this.showInfo('Erfolg', 'Template erfolgreich importiert!', 'Mitglied- und Bulk-Wrapper-Template wurden erkannt und ausgefüllt.', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('memberTransfer.templateImported'), this.$t('memberTransfer.templateImportedDetails'), 'success');
|
||||
} else if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
// Direktes Array-Format (ohne Wrapper)
|
||||
const firstMember = parsed[0];
|
||||
@@ -774,7 +772,7 @@ address={{address}}`;
|
||||
this.config.bulkWrapperTemplate = '{"members": "{{members}}"}';
|
||||
|
||||
this.importTemplate = '';
|
||||
await this.showInfo('Erfolg', 'Template erfolgreich importiert!', 'Mitglied-Template erkannt, Bulk-Modus aktiviert.', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('memberTransfer.templateImported'), this.$t('memberTransfer.templateImportedBulk'), 'success');
|
||||
} else if (typeof parsed === 'object' && parsed !== null) {
|
||||
// Einzelnes Mitglied-Objekt
|
||||
const memberTemplateWithPlaceholders = this.convertValuesToPlaceholders(parsed);
|
||||
@@ -782,13 +780,13 @@ address={{address}}`;
|
||||
this.config.useBulkMode = false;
|
||||
|
||||
this.importTemplate = '';
|
||||
await this.showInfo('Erfolg', 'Template erfolgreich importiert!', 'Einzelnes Mitglied-Template erkannt.', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('memberTransfer.templateImported'), this.$t('memberTransfer.templateImportedSingle'), 'success');
|
||||
} else {
|
||||
throw new Error('Ungültiges Template-Format');
|
||||
throw new Error(this.$t('memberTransfer.invalidTemplateFormat'));
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = `${getSafeMessage(error.message, 'Unbekannter Fehler')}\n\nBitte stellen Sie sicher, dass gültiges JSON verwendet wird.`;
|
||||
await this.showInfo('Fehler', 'Fehler beim Parsen des Templates', detail, 'error');
|
||||
const detail = `${getSafeMessage(error.message, this.$t('messages.error'))}\n\n${this.$t('memberTransfer.invalidJson')}`;
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('memberTransfer.errorParsingTemplate'), detail, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Mitglieder</h2>
|
||||
<h2>{{ $t('members.title') }}</h2>
|
||||
<div class="action-buttons">
|
||||
<div class="dropdown-container">
|
||||
<button @click="toggleMemberInfo" class="btn-dropdown">
|
||||
Mitglieder-Info {{ showMemberInfo ? '▼' : '▶' }}
|
||||
{{ $t('members.memberInfo') }} {{ showMemberInfo ? '▼' : '▶' }}
|
||||
</button>
|
||||
<div v-if="showMemberInfo" class="dropdown-content">
|
||||
<div class="member-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Aktive Mitglieder:</span>
|
||||
<span class="stat-label">{{ $t('members.activeMembers') }}:</span>
|
||||
<span class="stat-value">{{ activeMembersCount }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Testmitglieder:</span>
|
||||
<span class="stat-label">{{ $t('members.testMembers') }}:</span>
|
||||
<span class="stat-value">{{ testMembersCount }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Inaktive Mitglieder:</span>
|
||||
<span class="stat-label">{{ $t('members.inactiveMembers') }}:</span>
|
||||
<span class="stat-value">{{ inactiveMembersCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="createPhoneList">Telefonliste generieren</button>
|
||||
<span class="info-text">Es werden nur aktive Mitglieder ausgegeben</span>
|
||||
<button @click="createPhoneList">{{ $t('members.generatePhoneList') }}</button>
|
||||
<span class="info-text">{{ $t('members.onlyActiveMembers') }}</span>
|
||||
<button @click="updateRatingsFromMyTischtennis" class="btn-update-ratings" :disabled="isUpdatingRatings">
|
||||
{{ isUpdatingRatings ? 'Aktualisiere...' : 'TTR/QTTR von myTischtennis aktualisieren' }}
|
||||
{{ isUpdatingRatings ? $t('members.updating') : $t('members.updateRatings') }}
|
||||
</button>
|
||||
<button @click="openTransferDialog" class="btn-transfer">
|
||||
Mitglieder übertragen
|
||||
{{ $t('members.transferMembers') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="newmember">
|
||||
<div class="toggle-new-member">
|
||||
<span @click="toggleNewMember">
|
||||
<span class="add">{{ memberFormIsOpen ? '-' : '+' }}</span>
|
||||
{{ memberToEdit === null ? "Neues Mitglied" : "Mitglied bearbeiten" }}
|
||||
{{ memberToEdit === null ? $t('members.newMember') : $t('members.editMember') }}
|
||||
</span>
|
||||
<button v-if="memberToEdit !== null" @click="resetToNewMember">Neues Mitglied anlegen</button>
|
||||
<button v-if="memberToEdit !== null" @click="resetToNewMember">{{ $t('members.createNewMember') }}</button>
|
||||
</div>
|
||||
<div v-if="memberFormIsOpen" class="new-member-form">
|
||||
<label><span>Vorname:</span> <input type="text" v-model="newFirstname"></label>
|
||||
<label><span>Nachname:</span> <input type="text" v-model="newLastname"></label>
|
||||
<label><span>Straße:</span> <input type="text" v-model="newStreet"></label>
|
||||
<label><span>PLZ:</span> <input type="text" v-model="newPostalCode" maxlength="10"></label>
|
||||
<label><span>Ort:</span> <input type="text" v-model="newCity"></label>
|
||||
<label><span>Geburtsdatum:</span> <input type="date" v-model="newBirthdate"></label>
|
||||
<label><span>{{ $t('members.firstName') }}:</span> <input type="text" v-model="newFirstname"></label>
|
||||
<label><span>{{ $t('members.lastName') }}:</span> <input type="text" v-model="newLastname"></label>
|
||||
<label><span>{{ $t('members.street') }}:</span> <input type="text" v-model="newStreet"></label>
|
||||
<label><span>{{ $t('members.postalCode') }}:</span> <input type="text" v-model="newPostalCode" maxlength="10"></label>
|
||||
<label><span>{{ $t('members.city') }}:</span> <input type="text" v-model="newCity"></label>
|
||||
<label><span>{{ $t('members.birthdate') }}:</span> <input type="date" v-model="newBirthdate"></label>
|
||||
|
||||
<!-- Telefonnummern -->
|
||||
<div class="contact-section">
|
||||
<label><span>Telefonnummern:</span></label>
|
||||
<label><span>{{ $t('members.phones') }}:</span></label>
|
||||
<div v-for="(phone, index) in memberContacts.phones" :key="'phone-' + index" class="contact-item">
|
||||
<input type="text" v-model="phone.value" placeholder="Telefonnummer" class="contact-input">
|
||||
<input type="text" v-model="phone.value" :placeholder="$t('members.phoneNumber')" class="contact-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="phone.isParent"> Elternteil
|
||||
<input type="checkbox" v-model="phone.isParent"> {{ $t('members.parent') }}
|
||||
</label>
|
||||
<input v-if="phone.isParent" type="text" v-model="phone.parentName" placeholder="Name (z.B. Mutter, Vater)" class="parent-name-input">
|
||||
<input v-if="phone.isParent" type="text" v-model="phone.parentName" :placeholder="$t('members.parentName')" class="parent-name-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="phone.isPrimary"> Primär
|
||||
<input type="checkbox" v-model="phone.isPrimary"> {{ $t('members.primary') }}
|
||||
</label>
|
||||
<button type="button" @click="removeContact('phone', index)" class="btn-remove-contact">✕</button>
|
||||
</div>
|
||||
<button type="button" @click="addContact('phone')" class="btn-add-contact">+ Telefonnummer hinzufügen</button>
|
||||
<button type="button" @click="addContact('phone')" class="btn-add-contact">+ {{ $t('members.addPhone') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail-Adressen -->
|
||||
<div class="contact-section">
|
||||
<label><span>E-Mail-Adressen:</span></label>
|
||||
<label><span>{{ $t('members.emails') }}:</span></label>
|
||||
<div v-for="(email, index) in memberContacts.emails" :key="'email-' + index" class="contact-item">
|
||||
<input type="email" v-model="email.value" placeholder="E-Mail-Adresse" class="contact-input">
|
||||
<input type="email" v-model="email.value" :placeholder="$t('members.emailAddress')" class="contact-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="email.isParent"> Elternteil
|
||||
<input type="checkbox" v-model="email.isParent"> {{ $t('members.parent') }}
|
||||
</label>
|
||||
<input v-if="email.isParent" type="text" v-model="email.parentName" placeholder="Name (z.B. Mutter, Vater)" class="parent-name-input">
|
||||
<input v-if="email.isParent" type="text" v-model="email.parentName" :placeholder="$t('members.parentName')" class="parent-name-input">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" v-model="email.isPrimary"> Primär
|
||||
<input type="checkbox" v-model="email.isPrimary"> {{ $t('members.primary') }}
|
||||
</label>
|
||||
<button type="button" @click="removeContact('email', index)" class="btn-remove-contact">✕</button>
|
||||
</div>
|
||||
<button type="button" @click="addContact('email')" class="btn-add-contact">+ E-Mail-Adresse hinzufügen</button>
|
||||
<button type="button" @click="addContact('email')" class="btn-add-contact">+ {{ $t('members.addEmail') }}</button>
|
||||
</div>
|
||||
<label><span>Geschlecht:</span>
|
||||
<label><span>{{ $t('members.gender') }}:</span>
|
||||
<select v-model="newGender">
|
||||
<option value="unknown">Unbekannt</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
<option value="unknown">{{ $t('members.genderUnknown') }}</option>
|
||||
<option value="male">{{ $t('members.genderMale') }}</option>
|
||||
<option value="female">{{ $t('members.genderFemale') }}</option>
|
||||
<option value="diverse">{{ $t('members.genderDiverse') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="checkbox-item"><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
|
||||
<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>
|
||||
<label class="checkbox-item"><span>{{ $t('members.active') }}:</span> <input type="checkbox" v-model="newActive"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.picsInInternetAllowed') }}:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.testMembership') }}:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.memberFormHandedOver') }}:</span> <input type="checkbox" v-model="newMemberFormHandedOver"></label>
|
||||
|
||||
<!-- Trainingsgruppen -->
|
||||
<div class="contact-section" v-if="memberToEdit">
|
||||
<label><span>Trainingsgruppen:</span></label>
|
||||
<label><span>{{ $t('members.trainingGroups') }}:</span></label>
|
||||
<div v-if="memberTrainingGroups.length > 0" class="member-groups-list">
|
||||
<span
|
||||
v-for="group in memberTrainingGroups"
|
||||
@@ -107,13 +107,13 @@
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id)"
|
||||
class="remove-group-btn"
|
||||
title="Entfernen"
|
||||
:title="$t('members.remove')"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="no-groups-hint">Keine Gruppen zugeordnet</div>
|
||||
<div v-else class="no-groups-hint">{{ $t('members.noGroupsAssigned') }}</div>
|
||||
<select
|
||||
v-model="selectedGroupToAdd"
|
||||
class="group-select"
|
||||
@@ -121,7 +121,7 @@
|
||||
:disabled="availableGroupsForMember.length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableGroupsForMember.length === 0 ? 'Keine Gruppen verfügbar' : 'Gruppe hinzufügen...' }}
|
||||
{{ availableGroupsForMember.length === 0 ? $t('members.noGroupsAvailable') : $t('members.addGroup') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="group in availableGroupsForMember"
|
||||
@@ -133,22 +133,21 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label><span>Bild:</span>
|
||||
<label><span>{{ $t('members.image') }}:</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">
|
||||
<label for="member-image-file" style="cursor: pointer; padding: 5px 10px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5;">📁 Datei auswählen</label>
|
||||
<label for="member-image-file" style="cursor: pointer; padding: 5px 10px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5;">📁 {{ $t('members.selectFile') }}</label>
|
||||
<input type="file" accept="image/*" capture="environment" @change="onFileSelected" ref="cameraInput" style="display: none;" id="member-image-camera">
|
||||
<label for="member-image-camera" style="cursor: pointer; padding: 5px 10px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5;">📷 Kamera</label>
|
||||
<label for="member-image-camera" style="cursor: pointer; padding: 5px 10px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5;">📷 {{ $t('members.camera') }}</label>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="memberImagePreview">
|
||||
<img :src="memberImagePreview" alt="Vorschau des Mitgliedsbildes"
|
||||
<img :src="memberImagePreview" :alt="$t('members.imagePreview')"
|
||||
style="max-width: 200px; max-height: 200px;">
|
||||
</div>
|
||||
<div>
|
||||
<button @click="addNewMember">{{ memberToEdit ? 'Ändern' : 'Anlegen' }}</button>
|
||||
<button @click="resetNewMember" v-if="memberToEdit === null" class="cancel-action">Felder
|
||||
leeren</button>
|
||||
<button @click="addNewMember">{{ memberToEdit ? $t('members.change') : $t('members.create') }}</button>
|
||||
<button @click="resetNewMember" v-if="memberToEdit === null" class="cancel-action">{{ $t('members.clearFields') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,52 +155,52 @@
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="showInactiveMembers">
|
||||
<span>Inaktive Mitglieder anzeigen</span>
|
||||
<span>{{ $t('members.showInactiveMembers') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<div class="filter-group">
|
||||
<label>Altersklasse:</label>
|
||||
<label>{{ $t('members.ageGroup') }}:</label>
|
||||
<select v-model="selectedAgeGroup" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="adult">Erwachsene (20+)</option>
|
||||
<option value="J19">J19 (19 und jünger)</option>
|
||||
<option value="J17">J17 (17 und jünger)</option>
|
||||
<option value="J15">J15 (15 und jünger)</option>
|
||||
<option value="J13">J13 (13 und jünger)</option>
|
||||
<option value="J11">J11 (11 und jünger)</option>
|
||||
<option value="">{{ $t('common.all') }}</option>
|
||||
<option value="adult">{{ $t('members.adults') }}</option>
|
||||
<option value="J19">{{ $t('members.j19') }}</option>
|
||||
<option value="J17">{{ $t('members.j17') }}</option>
|
||||
<option value="J15">{{ $t('members.j15') }}</option>
|
||||
<option value="J13">{{ $t('members.j13') }}</option>
|
||||
<option value="J11">{{ $t('members.j11') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Geschlecht:</label>
|
||||
<label>{{ $t('members.gender') }}:</label>
|
||||
<select v-model="selectedGender" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
<option value="unknown">Unbekannt</option>
|
||||
<option value="">{{ $t('common.all') }}</option>
|
||||
<option value="female">{{ $t('members.genderFemale') }}</option>
|
||||
<option value="male">{{ $t('members.genderMale') }}</option>
|
||||
<option value="diverse">{{ $t('members.genderDiverse') }}</option>
|
||||
<option value="unknown">{{ $t('members.genderUnknown') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button @click="clearFilters" class="btn-clear-filters">Filter zurücksetzen</button>
|
||||
<button @click="clearFilters" class="btn-clear-filters">{{ $t('members.clearFilters') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bild (Inet?)</th>
|
||||
<th>Testm.</th>
|
||||
<th>Name, Vorname</th>
|
||||
<th>TTR / QTTR</th>
|
||||
<th>Adresse</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Telefon-Nr.</th>
|
||||
<th>Email-Adresse</th>
|
||||
<th v-if="hasTestMembers">Trainingsteilnahmen</th>
|
||||
<th>Aktionen</th>
|
||||
<th>{{ $t('members.imageInternet') }}</th>
|
||||
<th>{{ $t('members.testMember') }}</th>
|
||||
<th>{{ $t('members.name') }}</th>
|
||||
<th>{{ $t('members.ttrQttr') }}</th>
|
||||
<th>{{ $t('members.address') }}</th>
|
||||
<th>{{ $t('members.birthdate') }}</th>
|
||||
<th>{{ $t('members.phoneNumberShort') }}</th>
|
||||
<th>{{ $t('members.emailAddressShort') }}</th>
|
||||
<th v-if="hasTestMembers">{{ $t('members.trainingParticipations') }}</th>
|
||||
<th>{{ $t('members.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -212,7 +211,7 @@
|
||||
<img
|
||||
v-if="member.latestImageUrl"
|
||||
:src="member.latestImageUrl"
|
||||
alt="Mitgliedsbild"
|
||||
:alt="$t('members.memberImage')"
|
||||
class="member-image-thumb-small"
|
||||
>
|
||||
<div v-else class="member-image-thumb__placeholder">+</div>
|
||||
@@ -223,10 +222,10 @@
|
||||
<td>
|
||||
<span class="gender-symbol" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]" :title="labelGender(member.gender)">{{ genderSymbol(member.gender) }}</span>
|
||||
<span class="gender-name" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]">
|
||||
<span v-if="member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" title="6 oder mehr Trainingsteilnahmen">🛑</span>
|
||||
<span v-else-if="member.testMembership && member.trainingParticipations >= 3" class="warning-icon" title="3 oder mehr Trainingsteilnahmen">⚠️</span>
|
||||
<span v-if="member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" :title="$t('members.sixOrMoreParticipations')">🛑</span>
|
||||
<span v-else-if="member.testMembership && member.trainingParticipations >= 3" class="warning-icon" :title="$t('members.threeOrMoreParticipations')">⚠️</span>
|
||||
{{ member.lastName }}, {{ member.firstName }}
|
||||
<span v-if="!member.active && showInactiveMembers" class="inactive-badge">inaktiv</span>
|
||||
<span v-if="!member.active && showInactiveMembers" class="inactive-badge">{{ $t('members.inactive') }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="rating-cell">
|
||||
@@ -247,19 +246,19 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-icons-row">
|
||||
<span v-if="member.testMembership" @click.stop="quickRemoveTestMembership(member)" class="action-icon" title="Keine Testmitgliedschaft mehr">
|
||||
<span v-if="member.testMembership" @click.stop="quickRemoveTestMembership(member)" class="action-icon" :title="$t('members.noTestMembership')">
|
||||
✅
|
||||
</span>
|
||||
<span v-if="member.testMembership && !member.memberFormHandedOver" @click.stop="quickMarkFormHandedOver(member)" class="action-icon" title="Mitgliedsformular ausgehändigt">
|
||||
<span v-if="member.testMembership && !member.memberFormHandedOver" @click.stop="quickMarkFormHandedOver(member)" class="action-icon" :title="$t('members.formHandedOver')">
|
||||
📄
|
||||
</span>
|
||||
<span v-if="member.active" @click.stop="quickDeactivateMember(member)" class="action-icon action-icon-deactivate" title="Mitglied deaktivieren">
|
||||
<span v-if="member.active" @click.stop="quickDeactivateMember(member)" class="action-icon action-icon-deactivate" :title="$t('members.deactivateMember')">
|
||||
⛔
|
||||
</span>
|
||||
<span @click.stop="openNotesModal(member)" class="action-icon" title="Notizen">
|
||||
<span @click.stop="openNotesModal(member)" class="action-icon" :title="$t('members.notes')">
|
||||
📝
|
||||
</span>
|
||||
<span @click.stop="openActivitiesModal(member)" class="action-icon" title="Übungen">
|
||||
<span @click.stop="openActivitiesModal(member)" class="action-icon" :title="$t('members.exercises')">
|
||||
🏃
|
||||
</span>
|
||||
</div>
|
||||
@@ -273,7 +272,7 @@
|
||||
<!-- Image Viewer Dialog -->
|
||||
<ImageViewerDialog
|
||||
v-model="showImageModal"
|
||||
title="Mitgliedsbilder"
|
||||
:title="$t('members.memberImages')"
|
||||
:images="selectedMemberImages"
|
||||
:active-image-id="selectedImageId"
|
||||
:member-id="selectedMemberId"
|
||||
@@ -580,7 +579,7 @@ export default {
|
||||
member.trainingParticipations = finalCount;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Trainingsteilnahmen:', error);
|
||||
console.error(this.$t('members.errorLoadingTrainingParticipations'), error);
|
||||
// Bei Fehler setze 0 für alle Testmitglieder
|
||||
this.members.forEach(member => {
|
||||
if (member.testMembership) {
|
||||
@@ -596,14 +595,14 @@ export default {
|
||||
if (response.data.success) {
|
||||
member.testMembership = false;
|
||||
member.trainingParticipations = undefined; // Entferne die Anzeige
|
||||
await this.showInfo('Erfolg', sanitizeText(response.data.message, 'Testmitgliedschaft entfernt.', 300), '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), sanitizeText(response.data.message, this.$t('members.testMembershipRemoved'), 300), '', 'success');
|
||||
} else {
|
||||
await this.showInfo('Fehler', sanitizeText(response.data.error, 'Fehler beim Entfernen der Testmitgliedschaft.', 300), '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), sanitizeText(response.data.error, this.$t('members.errorRemovingTestMembership'), 300), '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen der Testmitgliedschaft:', error);
|
||||
const errorMessage = getSafeErrorMessage(error, 'Fehler beim Entfernen der Testmitgliedschaft');
|
||||
this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
console.error(this.$t('members.errorRemovingTestMembership'), error);
|
||||
const errorMessage = getSafeErrorMessage(error, this.$t('members.errorRemovingTestMembership'));
|
||||
this.showInfo(this.$t('messages.error'), errorMessage, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -612,21 +611,21 @@ export default {
|
||||
const response = await apiClient.post(`/clubmembers/quick-update-member-form/${this.currentClub}/${member.id}`);
|
||||
if (response.data.success) {
|
||||
member.memberFormHandedOver = true;
|
||||
await this.showInfo('Erfolg', sanitizeText(response.data.message, 'Mitgliedsformular als ausgehändigt markiert.', 300), '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), sanitizeText(response.data.message, this.$t('members.formMarkedAsHandedOver'), 300), '', 'success');
|
||||
} else {
|
||||
await this.showInfo('Fehler', sanitizeText(response.data.error, 'Fehler beim Markieren des Formulars.', 300), '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), sanitizeText(response.data.error, this.$t('members.errorMarkingForm'), 300), '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Markieren des Formulars:', error);
|
||||
const errorMessage = getSafeErrorMessage(error, 'Fehler beim Markieren des Formulars');
|
||||
this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
console.error(this.$t('members.errorMarkingForm'), error);
|
||||
const errorMessage = getSafeErrorMessage(error, this.$t('members.errorMarkingForm'));
|
||||
this.showInfo(this.$t('messages.error'), errorMessage, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async quickDeactivateMember(member) {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Mitglied deaktivieren',
|
||||
`Möchten Sie "${member.firstName} ${member.lastName}" wirklich deaktivieren?`,
|
||||
this.$t('members.deactivateMemberTitle'),
|
||||
this.$t('members.deactivateMemberConfirm', { name: `${member.firstName} ${member.lastName}` }),
|
||||
'',
|
||||
'warning'
|
||||
);
|
||||
@@ -638,14 +637,14 @@ export default {
|
||||
const response = await apiClient.post(`/clubmembers/quick-deactivate/${this.currentClub}/${member.id}`);
|
||||
if (response.data.success) {
|
||||
member.active = false;
|
||||
this.showInfo('Erfolg', getSafeMessage(response.data.message, 'Mitglied deaktiviert'), '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), getSafeMessage(response.data.message, this.$t('members.memberDeactivated')), '', 'success');
|
||||
} else {
|
||||
this.showInfo('Fehler', getSafeMessage(response.data.error, 'Fehler beim Deaktivieren des Mitglieds'), '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), getSafeMessage(response.data.error, this.$t('members.errorDeactivatingMember')), '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Deaktivieren des Mitglieds:', error);
|
||||
const errorMessage = getSafeErrorMessage(error, 'Fehler beim Deaktivieren des Mitglieds');
|
||||
this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
console.error(this.$t('members.errorDeactivatingMember'), error);
|
||||
const errorMessage = getSafeErrorMessage(error, this.$t('members.errorDeactivatingMember'));
|
||||
this.showInfo(this.$t('messages.error'), errorMessage, '', 'error');
|
||||
}
|
||||
},
|
||||
toggleNewMember() {
|
||||
@@ -709,12 +708,12 @@ export default {
|
||||
}
|
||||
const maxSize = 5 * 1024 * 1024; // 5 MB
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.showInfo('Ungültige Datei', 'Bitte wähle eine Bilddatei aus.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.invalidFile'), this.$t('messages.pleaseSelectImageFile'), '', 'warning');
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
if (file.size > maxSize) {
|
||||
this.showInfo('Datei zu groß', 'Das Bild darf maximal 5 MB groß sein.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.fileTooLarge'), this.$t('messages.imageMaxSize'), '', 'warning');
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
@@ -898,7 +897,7 @@ export default {
|
||||
response = await apiClient.post(`/clubmembers/set/${this.currentClub}`, memberData);
|
||||
this.loadMembers();
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Speichern des Mitglieds', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('members.errorSavingMember'), '', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1040,8 +1039,8 @@ export default {
|
||||
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');
|
||||
const msg = getSafeErrorMessage(error, this.$t('members.errorAddingToGroup'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1055,8 +1054,8 @@ export default {
|
||||
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');
|
||||
const msg = getSafeErrorMessage(error, this.$t('members.errorRemovingFromGroup'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1124,14 +1123,14 @@ export default {
|
||||
|
||||
if (response.data?.success && member) {
|
||||
await this.applyMemberImageUpdate(member, response.data);
|
||||
this.showInfo('Erfolg', getSafeMessage(response.data.message, 'Bild wurde aktualisiert'), '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), getSafeMessage(response.data.message, this.$t('members.imageUpdated')), '', 'success');
|
||||
} else {
|
||||
const msg = getSafeErrorMessage(response.data?.error || null, 'Fehler beim Drehen des Bildes');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
const msg = getSafeErrorMessage(response.data?.error || null, this.$t('members.errorRotatingImage'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Drehen des Bildes:', error);
|
||||
this.showInfo('Fehler', getSafeErrorMessage(error, 'Fehler beim Drehen des Bildes'), '', 'error');
|
||||
console.error(this.$t('members.errorRotatingImage'), error);
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, this.$t('members.errorRotatingImage')), '', 'error');
|
||||
}
|
||||
},
|
||||
async handleDeleteImage(event) {
|
||||
@@ -1154,14 +1153,14 @@ export default {
|
||||
const response = await apiClient.delete(`/clubmembers/image/${this.currentClub}/${memberId}/${imageId}`);
|
||||
if (response.data?.success) {
|
||||
this.applyMemberImageUpdate(member, response.data);
|
||||
this.showInfo('Erfolg', 'Bild wurde gelöscht.', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('members.imageDeleted'), '', 'success');
|
||||
} else {
|
||||
const msg = getSafeMessage(response.data?.error, 'Bild konnte nicht gelöscht werden.');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
const msg = getSafeMessage(response.data?.error, this.$t('members.errorDeletingImage'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Bildes:', error);
|
||||
this.showInfo('Fehler', getSafeErrorMessage(error, 'Bild konnte nicht gelöscht werden'), '', 'error');
|
||||
console.error(this.$t('members.errorDeletingImage'), error);
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, this.$t('members.errorDeletingImage')), '', 'error');
|
||||
}
|
||||
},
|
||||
async handleSetPrimaryImage(event) {
|
||||
@@ -1174,14 +1173,14 @@ export default {
|
||||
const response = await apiClient.post(`/clubmembers/image/${this.currentClub}/${memberId}/${imageId}/primary`);
|
||||
if (response.data?.success) {
|
||||
this.applyMemberImageUpdate(member, response.data);
|
||||
this.showInfo('Erfolg', 'Hauptbild wurde aktualisiert.', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('members.primaryImageUpdated'), '', 'success');
|
||||
} else {
|
||||
const msg = getSafeMessage(response.data?.error, 'Hauptbild konnte nicht gesetzt werden.');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
const msg = getSafeMessage(response.data?.error, this.$t('members.errorSettingPrimaryImage'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen des Hauptbildes:', error);
|
||||
this.showInfo('Fehler', getSafeErrorMessage(error, 'Hauptbild konnte nicht gesetzt werden'), '', 'error');
|
||||
console.error(this.$t('members.errorSettingPrimaryImage'), error);
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, this.$t('members.errorSettingPrimaryImage')), '', 'error');
|
||||
}
|
||||
},
|
||||
async handleUploadImages(event) {
|
||||
@@ -1209,12 +1208,12 @@ export default {
|
||||
if (response.data?.success) {
|
||||
await this.applyMemberImageUpdate(member, response.data);
|
||||
} else {
|
||||
const msg = getSafeMessage(response.data?.error, 'Bild konnte nicht hochgeladen werden.');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
const msg = getSafeMessage(response.data?.error, this.$t('members.errorUploadingImage'));
|
||||
this.showInfo(this.$t('messages.error'), msg, '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hochladen des Bildes:', error);
|
||||
this.showInfo('Fehler', getSafeErrorMessage(error, 'Bild konnte nicht hochgeladen werden'), '', 'error');
|
||||
console.error(this.$t('members.errorUploadingImage'), error);
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, this.$t('members.errorUploadingImage')), '', 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1319,7 +1318,7 @@ export default {
|
||||
image.url = objectUrl;
|
||||
return objectUrl;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Bildes:', error);
|
||||
console.error(this.$t('members.errorLoadingImage'), error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@@ -1328,9 +1327,9 @@ export default {
|
||||
},
|
||||
async createPhoneList() {
|
||||
const activeMembers = this.members.filter(member => member.active && !member.testMembership);
|
||||
const pdfGenerator = new PDFGenerator();
|
||||
const pdfGenerator = new PDFGenerator(20, 10, this.$t);
|
||||
pdfGenerator.addPhoneList(activeMembers);
|
||||
pdfGenerator.save('Telefonliste.pdf');
|
||||
pdfGenerator.save(this.$t('members.phoneList'));
|
||||
},
|
||||
getFormattedBirthdate(birthDate) {
|
||||
if (!birthDate) return '–';
|
||||
@@ -1428,15 +1427,15 @@ export default {
|
||||
const response = await apiClient.post(`/clubmembers/update-ratings/${this.currentClub}`);
|
||||
|
||||
if (response.data.message) {
|
||||
await this.showInfo('Hinweis', response.data.message, '', 'info');
|
||||
await this.showInfo(this.$t('messages.note'), response.data.message, '', 'info');
|
||||
}
|
||||
|
||||
// Mitglieder neu laden um aktualisierte Werte anzuzeigen
|
||||
await this.loadMembers();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Ratings:', error);
|
||||
const message = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der TTR/QTTR-Werte');
|
||||
this.showInfo('Fehler', message, '', 'error');
|
||||
console.error(this.$t('members.errorUpdatingRatings'), error);
|
||||
const message = getSafeErrorMessage(error, this.$t('members.errorUpdatingRatings'));
|
||||
this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
} finally {
|
||||
this.isUpdatingRatings = false;
|
||||
}
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>myTischtennis-Account</h1>
|
||||
<h1>{{ $t('myTischtennisAccount.title') }}</h1>
|
||||
|
||||
<div class="account-container">
|
||||
<div v-if="loading" class="loading">Lade...</div>
|
||||
<div v-if="loading" class="loading">{{ $t('myTischtennisAccount.loading') }}</div>
|
||||
|
||||
<div v-else-if="account" class="account-info">
|
||||
<div class="info-section">
|
||||
<h2>Verknüpfter Account</h2>
|
||||
<h2>{{ $t('myTischtennisAccount.linkedAccount') }}</h2>
|
||||
|
||||
<div class="info-row">
|
||||
<label>E-Mail:</label>
|
||||
<label>{{ $t('myTischtennisAccount.email') }}</label>
|
||||
<span>{{ account.email }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<label>Passwort gespeichert:</label>
|
||||
<span>{{ accountStatus && accountStatus.hasPassword ? 'Ja' : 'Nein' }}</span>
|
||||
<label>{{ $t('myTischtennisAccount.passwordSaved') }}</label>
|
||||
<span>{{ accountStatus && accountStatus.hasPassword ? $t('myTischtennisAccount.yes') : $t('myTischtennisAccount.no') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.clubId">
|
||||
<label>Verein (myTischtennis):</label>
|
||||
<label>{{ $t('myTischtennisAccount.club') }}</label>
|
||||
<span>{{ account.clubName }} ({{ account.clubId }}{{ account.fedNickname ? ' - ' + account.fedNickname : '' }})</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginSuccess">
|
||||
<label>Letzter erfolgreicher Login:</label>
|
||||
<label>{{ $t('myTischtennisAccount.lastSuccessfulLogin') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginAttempt">
|
||||
<label>Letzter Login-Versuch:</label>
|
||||
<label>{{ $t('myTischtennisAccount.lastLoginAttempt') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastUpdateRatings">
|
||||
<label>Letzter Abruf:</label>
|
||||
<label>{{ $t('myTischtennisAccount.lastFetch') }}</label>
|
||||
<span>{{ formatDate(account.lastUpdateRatings) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.autoUpdateRatings !== undefined">
|
||||
<label>Automatische Updates:</label>
|
||||
<span>{{ account.autoUpdateRatings ? 'Aktiviert' : 'Deaktiviert' }}</span>
|
||||
<label>{{ $t('myTischtennisAccount.autoUpdates') }}</label>
|
||||
<span>{{ account.autoUpdateRatings ? $t('myTischtennisAccount.enabled') : $t('myTischtennisAccount.disabled') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
|
||||
<button class="btn-secondary" @click="testConnection">Erneut einloggen</button>
|
||||
<button class="btn-info" @click="openHistoryDialog" v-if="account">Update-History</button>
|
||||
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.editAccount') }}</button>
|
||||
<button class="btn-secondary" @click="testConnection">{{ $t('myTischtennisAccount.loginAgain') }}</button>
|
||||
<button class="btn-info" @click="openHistoryDialog" v-if="account">{{ $t('myTischtennisAccount.updateHistory') }}</button>
|
||||
<button class="btn-danger" @click="deleteAccount">{{ $t('myTischtennisAccount.unlinkAccount') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fetch Statistics Section -->
|
||||
<div class="info-section fetch-stats-section" v-if="account">
|
||||
<h2>Datenabruf-Statistiken</h2>
|
||||
<h2>{{ $t('myTischtennisAccount.fetchStatistics') }}</h2>
|
||||
|
||||
<div v-if="loadingStats" class="loading-stats">Lade Statistiken...</div>
|
||||
<div v-if="loadingStats" class="loading-stats">{{ $t('myTischtennisAccount.loadingStatistics') }}</div>
|
||||
|
||||
<div v-else-if="latestFetches" class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<h3>Spielerwertungen</h3>
|
||||
<h3>{{ $t('myTischtennisAccount.playerRatings') }}</h3>
|
||||
<div v-if="latestFetches.ratings">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.ratings.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.ratings.recordsProcessed }} Spieler aktualisiert</p>
|
||||
<p class="stat-detail">{{ latestFetches.ratings.recordsProcessed }} {{ $t('myTischtennisAccount.playersUpdated') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.ratings.executionTime">{{ latestFetches.ratings.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🏓</div>
|
||||
<div class="stat-content">
|
||||
<h3>Spielergebnisse</h3>
|
||||
<h3>{{ $t('myTischtennisAccount.matchResults') }}</h3>
|
||||
<div v-if="latestFetches.match_results">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.match_results.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.match_results.recordsProcessed }} Ergebnisse</p>
|
||||
<p class="stat-detail">{{ latestFetches.match_results.recordsProcessed }} {{ $t('myTischtennisAccount.results') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.match_results.executionTime">{{ latestFetches.match_results.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-content">
|
||||
<h3>Ligatabellen</h3>
|
||||
<h3>{{ $t('myTischtennisAccount.leagueTables') }}</h3>
|
||||
<div v-if="latestFetches.league_table">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.league_table.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.league_table.recordsProcessed }} Teams</p>
|
||||
<p class="stat-detail">{{ latestFetches.league_table.recordsProcessed }} {{ $t('myTischtennisAccount.teams') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.league_table.executionTime">{{ latestFetches.league_table.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-secondary refresh-stats-btn" @click="loadLatestFetches">
|
||||
🔄 Statistiken aktualisieren
|
||||
🔄 {{ $t('myTischtennisAccount.refreshStatistics') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-account">
|
||||
<p>Kein myTischtennis-Account verknüpft.</p>
|
||||
<button class="btn-primary" @click="openEditDialog">Account verknüpfen</button>
|
||||
<p>{{ $t('myTischtennisAccount.noAccountLinked') }}</p>
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.linkAccount') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Über myTischtennis</h3>
|
||||
<p>Durch die Verknüpfung Ihres myTischtennis-Accounts können Sie:</p>
|
||||
<h3>{{ $t('myTischtennisAccount.aboutMyTischtennis') }}</h3>
|
||||
<p>{{ $t('myTischtennisAccount.aboutDescription') }}</p>
|
||||
<ul>
|
||||
<li>Automatisch Turnierdaten importieren</li>
|
||||
<li>Spielerergebnisse synchronisieren</li>
|
||||
<li>Wettkampfdaten direkt abrufen</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature1') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature2') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature3') }}</li>
|
||||
</ul>
|
||||
<p><strong>Hinweis:</strong> Das Speichern des Passworts ist optional. Wenn Sie es nicht speichern, werden Sie bei jeder Synchronisation nach dem Passwort gefragt.</p>
|
||||
<p><strong>{{ $t('messages.info') }}:</strong> {{ $t('myTischtennisAccount.aboutHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ export default {
|
||||
this.account = null;
|
||||
this.accountStatus = null;
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Fehler beim Laden des myTischtennis-Accounts',
|
||||
text: this.$t('myTischtennisAccount.errorLoadingAccount'),
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
@@ -281,7 +281,7 @@ export default {
|
||||
this.closeDialog();
|
||||
await this.loadAccount();
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'myTischtennis-Account erfolgreich gespeichert',
|
||||
text: this.$t('myTischtennisAccount.accountSaved'),
|
||||
type: 'success'
|
||||
});
|
||||
},
|
||||
@@ -290,7 +290,7 @@ export default {
|
||||
try {
|
||||
await apiClient.post('/mytischtennis/verify');
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Login erfolgreich! Verbindungsdaten aktualisiert.',
|
||||
text: this.$t('myTischtennisAccount.loginSuccessful'),
|
||||
type: 'success'
|
||||
});
|
||||
await this.loadAccount(); // Aktualisiere Account-Daten inkl. clubId, fedNickname
|
||||
@@ -300,9 +300,9 @@ export default {
|
||||
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
|
||||
// Passwort-Dialog öffnen
|
||||
this.showDialog = true;
|
||||
this.showInfo('Passwort benötigt', message, '', 'warning');
|
||||
this.showInfo(this.$t('myTischtennisAccount.passwordRequired'), message, '', 'warning');
|
||||
} else {
|
||||
this.showInfo('Login fehlgeschlagen', message, '', 'error');
|
||||
this.showInfo(this.$t('myTischtennisAccount.loginFailed'), message, '', 'error');
|
||||
}
|
||||
|
||||
this.$store.dispatch('showMessage', {
|
||||
@@ -314,8 +314,8 @@ export default {
|
||||
|
||||
async deleteAccount() {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Account trennen',
|
||||
'Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?',
|
||||
this.$t('myTischtennisAccount.unlinkAccountTitle'),
|
||||
this.$t('myTischtennisAccount.unlinkAccountConfirm'),
|
||||
'',
|
||||
'danger'
|
||||
);
|
||||
@@ -326,10 +326,10 @@ export default {
|
||||
await apiClient.delete('/mytischtennis/account');
|
||||
this.account = null;
|
||||
this.accountStatus = null;
|
||||
this.showInfo('Erfolg', 'myTischtennis-Account erfolgreich getrennt', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('myTischtennisAccount.accountUnlinked'), '', 'success');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Accounts:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Trennen des Accounts', error.message, 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('myTischtennisAccount.errorUnlinking'), error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -358,7 +358,7 @@ export default {
|
||||
},
|
||||
|
||||
formatDateRelative(dateString) {
|
||||
if (!dateString) return 'Nie';
|
||||
if (!dateString) return this.$t('myTischtennisAccount.never');
|
||||
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
@@ -367,11 +367,11 @@ export default {
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Gerade eben';
|
||||
if (diffMins < 60) return `vor ${diffMins} Min.`;
|
||||
if (diffHours < 24) return `vor ${diffHours} Std.`;
|
||||
if (diffDays === 1) return 'Gestern';
|
||||
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||
if (diffMins < 1) return this.$t('myTischtennisAccount.justNow');
|
||||
if (diffMins < 60) return this.$t('myTischtennisAccount.minutesAgo', { count: diffMins });
|
||||
if (diffHours < 24) return this.$t('myTischtennisAccount.hoursAgo', { count: diffHours });
|
||||
if (diffDays === 1) return this.$t('myTischtennisAccount.yesterday');
|
||||
if (diffDays < 7) return this.$t('myTischtennisAccount.daysAgo', { count: diffDays });
|
||||
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
|
||||
@@ -3,47 +3,47 @@
|
||||
|
||||
<div class="uploader">
|
||||
<input type="file" accept="application/pdf" @change="onFile" />
|
||||
<button class="btn-primary" :disabled="!selectedFile" @click="upload">PDF hochladen</button>
|
||||
<button class="btn-primary" :disabled="!selectedFile" @click="upload">{{ $t('officialTournaments.uploadPdf') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="list && list.length > 0" class="list">
|
||||
<div class="tabs">
|
||||
<button :class="['tab', topActiveTab==='events' ? 'active' : '']" @click="switchTopTab('events')" title="Gespeicherte Veranstaltungen anzeigen">Veranstaltungen</button>
|
||||
<button :class="['tab', topActiveTab==='participations' ? 'active' : '']" @click="switchTopTab('participations')" title="Turnierbeteiligungen anzeigen">Turnierbeteiligungen</button>
|
||||
<button :class="['tab', topActiveTab==='events' ? 'active' : '']" @click="switchTopTab('events')" :title="$t('officialTournaments.showEvents')">{{ $t('officialTournaments.events') }}</button>
|
||||
<button :class="['tab', topActiveTab==='participations' ? 'active' : '']" @click="switchTopTab('participations')" :title="$t('officialTournaments.showParticipations')">{{ $t('officialTournaments.participations') }}</button>
|
||||
</div>
|
||||
<div v-if="topActiveTab==='events'">
|
||||
<h3>Gespeicherte Veranstaltungen</h3>
|
||||
<h3>{{ $t('officialTournaments.savedEvents') }}</h3>
|
||||
<ul>
|
||||
<li v-for="t in list" :key="t.id" style="display:flex; align-items:center; gap:.5rem;">
|
||||
<a href="#" @click.prevent="uploadedId = String(t.id); reload();" style="flex:1;">
|
||||
{{ t.title || ('Turnier #' + t.id) }}
|
||||
{{ t.title || ($t('officialTournaments.tournament') + ' #' + t.id) }}
|
||||
</a>
|
||||
<span v-if="t.termin || t.eventDate"> — {{ t.termin || t.eventDate }}</span>
|
||||
<button class="btn-secondary" @click.prevent="removeTournament(t)" title="Löschen">🗑️</button>
|
||||
<button class="btn-secondary" @click.prevent="removeTournament(t)" :title="$t('officialTournaments.delete')">🗑️</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="topActiveTab==='participations'">
|
||||
<h3>Turnierbeteiligungen</h3>
|
||||
<h3>{{ $t('officialTournaments.participations') }}</h3>
|
||||
<div class="filters">
|
||||
<label for="participationRange">Zeitraum:</label>
|
||||
<label for="participationRange">{{ $t('officialTournaments.timeRange') }}</label>
|
||||
<select id="participationRange" v-model="participationRange" @change="onParticipationRangeChange">
|
||||
<option value="3m">Letzte 3 Monate</option>
|
||||
<option value="6m">Letzte 6 Monate</option>
|
||||
<option value="12m">Letzte 12 Monate</option>
|
||||
<option value="2y">Letzte 2 Jahre</option>
|
||||
<option value="prev">Vorherige Saison</option>
|
||||
<option value="all">Alle</option>
|
||||
<option value="3m">{{ $t('officialTournaments.last3Months') }}</option>
|
||||
<option value="6m">{{ $t('officialTournaments.last6Months') }}</option>
|
||||
<option value="12m">{{ $t('officialTournaments.last12Months') }}</option>
|
||||
<option value="2y">{{ $t('officialTournaments.last2Years') }}</option>
|
||||
<option value="prev">{{ $t('officialTournaments.previousSeason') }}</option>
|
||||
<option value="all">{{ $t('officialTournaments.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitglied</th>
|
||||
<th>Turnier</th>
|
||||
<th>Konkurrenz</th>
|
||||
<th>Datum</th>
|
||||
<th>Platzierung</th>
|
||||
<th>{{ $t('officialTournaments.member') }}</th>
|
||||
<th>{{ $t('officialTournaments.tournament') }}</th>
|
||||
<th>{{ $t('officialTournaments.competition') }}</th>
|
||||
<th>{{ $t('officialTournaments.date') }}</th>
|
||||
<th>{{ $t('officialTournaments.placement') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,32 +60,32 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!list || list.length === 0" class="empty-state">
|
||||
<p>Noch keine Veranstaltungen vorhanden. Laden Sie ein PDF hoch, um zu beginnen.</p>
|
||||
<p>{{ $t('officialTournaments.noEvents') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="parsed">
|
||||
<div class="meta">
|
||||
<div><strong>Titel:</strong> {{ parsed.parsedData.title || '–' }}</div>
|
||||
<div><strong>Termin:</strong> {{ parsed.parsedData.termin || '–' }}</div>
|
||||
<div><strong>Austragungsorte:</strong>
|
||||
<div><strong>{{ $t('officialTournaments.title') }}</strong> {{ parsed.parsedData.title || '–' }}</div>
|
||||
<div><strong>{{ $t('officialTournaments.dateTime') }}</strong> {{ parsed.parsedData.termin || '–' }}</div>
|
||||
<div><strong>{{ $t('officialTournaments.venues') }}</strong>
|
||||
<ul>
|
||||
<li v-for="(o,i) in parsed.parsedData.austragungsorte" :key="i">{{ o }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div><strong>Konkurrenztypen:</strong> {{ (parsed.parsedData.konkurrenztypen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>Meldeschlüsse:</strong> {{ (parsed.parsedData.meldeschluesse||[]).join(' | ') || '–' }}</div>
|
||||
<div><strong>Altersklassen:</strong> {{ (parsed.parsedData.altersklassen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>Startzeiten:</strong>
|
||||
<div><strong>{{ $t('officialTournaments.competitionTypes') }}</strong> {{ (parsed.parsedData.konkurrenztypen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>{{ $t('officialTournaments.deadlines') }}</strong> {{ (parsed.parsedData.meldeschluesse||[]).join(' | ') || '–' }}</div>
|
||||
<div><strong>{{ $t('officialTournaments.ageClasses') }}</strong> {{ (parsed.parsedData.altersklassen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>{{ $t('officialTournaments.startTimes') }}</strong>
|
||||
<span v-for="(t,ak) in parsed.parsedData.startzeiten" :key="ak" style="margin-right:.5rem;">{{ ak }}: {{ t }}</span>
|
||||
</div>
|
||||
<div v-if="parsed.parsedData.meldeschluesseByAk && Object.keys(parsed.parsedData.meldeschluesseByAk).length">
|
||||
<strong>Meldeschlüsse je AK:</strong>
|
||||
<strong>{{ $t('officialTournaments.deadlinesByAgeClass') }}</strong>
|
||||
<span v-for="(arr,ak) in parsed.parsedData.meldeschluesseByAk" :key="ak" style="margin-right:.5rem;">
|
||||
{{ ak }}: {{ arr.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="parsed.parsedData.entryFees && Object.keys(parsed.parsedData.entryFees).length">
|
||||
<strong>Teilnahmegebühren:</strong>
|
||||
<strong>{{ $t('officialTournaments.entryFees') }}</strong>
|
||||
<div class="entry-fees">
|
||||
<div v-for="(fee, ageClass) in parsed.parsedData.entryFees" :key="ageClass" class="fee-item">
|
||||
<span class="age-class">{{ ageClass }}:</span>
|
||||
@@ -100,23 +100,23 @@
|
||||
|
||||
<div v-if="parsed && parsed.parsedData.competitions && parsed.parsedData.competitions.length">
|
||||
<div class="top-actions">
|
||||
<button class="btn-secondary" @click="openMemberDialog" :disabled="!parsed || !activeMembers.length">Mitglieder auswählen</button>
|
||||
<button class="btn-primary" :disabled="!selectedMemberIds.length" @click="generateMembersPdf">PDF für markierte Mitglieder</button>
|
||||
<button class="btn-secondary" @click="openMemberDialog" :disabled="!parsed || !activeMembers.length">{{ $t('officialTournaments.selectMembers') }}</button>
|
||||
<button class="btn-primary" :disabled="!selectedMemberIds.length" @click="generateMembersPdf">{{ $t('officialTournaments.pdfForSelectedMembers') }}</button>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button :class="['tab', activeTab==='competitions' ? 'active' : '']" @click="activeTab='competitions'" title="Konkurrenzen anzeigen">Konkurrenzen</button>
|
||||
<button :class="['tab', activeTab==='participants' ? 'active' : '']" @click="activeTab='participants'" title="Teilnehmer anzeigen">Teilnehmer</button>
|
||||
<button :class="['tab', activeTab==='results' ? 'active' : '']" @click="activeTab='results'" title="Ergebnisse anzeigen">Ergebnisse</button>
|
||||
<button :class="['tab', activeTab==='competitions' ? 'active' : '']" @click="activeTab='competitions'" :title="$t('officialTournaments.showCompetitions')">{{ $t('officialTournaments.competitions') }}</button>
|
||||
<button :class="['tab', activeTab==='participants' ? 'active' : '']" @click="activeTab='participants'" :title="$t('officialTournaments.showParticipants')">{{ $t('officialTournaments.participants') }}</button>
|
||||
<button :class="['tab', activeTab==='results' ? 'active' : '']" @click="activeTab='results'" :title="$t('officialTournaments.showResults')">{{ $t('officialTournaments.results') }}</button>
|
||||
</div>
|
||||
<div v-if="activeTab==='competitions'">
|
||||
<h3>Konkurrenzen</h3>
|
||||
<h3>{{ $t('officialTournaments.competitions') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Altersklasse/Wettbewerb</th>
|
||||
<th>Startzeit</th>
|
||||
<th>Startgeld</th>
|
||||
<th>{{ $t('officialTournaments.ageClassCompetition') }}</th>
|
||||
<th>{{ $t('officialTournaments.startTime') }}</th>
|
||||
<th>{{ $t('officialTournaments.entryFee') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -134,23 +134,23 @@
|
||||
<tr v-if="isExpanded(c, idx)" class="comp-details">
|
||||
<td :colspan="4">
|
||||
<div class="details">
|
||||
<div class="detail-item"><strong>Meldeschluss (Datum):</strong> {{ c.registrationDeadlineDate || c.meldeschlussDatum || '–' }}</div>
|
||||
<div class="detail-item"><strong>Meldeschluss (Online):</strong> {{ c.registrationDeadlineOnline || c.meldeschlussOnline || '–' }}</div>
|
||||
<div class="detail-item"><strong>Stichtag:</strong> {{ c.cutoffDate || c.stichtag || '–' }}</div>
|
||||
<div class="detail-item"><strong>Offen für:</strong> {{ c.openTo || c.offenFuer || '–' }}</div>
|
||||
<div class="detail-item"><strong>Vorrunde:</strong> {{ c.preliminaryRound || c.vorrunde || '–' }}</div>
|
||||
<div class="detail-item"><strong>Endrunde:</strong> {{ c.finalRound || c.endrunde || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.deadlineDate') }}</strong> {{ c.registrationDeadlineDate || c.meldeschlussDatum || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.deadlineOnline') }}</strong> {{ c.registrationDeadlineOnline || c.meldeschlussOnline || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.cutoffDate') }}</strong> {{ c.cutoffDate || c.stichtag || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.openTo') }}</strong> {{ c.openTo || c.offenFuer || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.preliminaryRound') }}</strong> {{ c.preliminaryRound || c.vorrunde || '–' }}</div>
|
||||
<div class="detail-item"><strong>{{ $t('officialTournaments.finalRound') }}</strong> {{ c.finalRound || c.endrunde || '–' }}</div>
|
||||
<div class="detail-item">
|
||||
<strong>Teilnahmeberechtigt ({{ eligibleMembers(c).length }}):</strong>
|
||||
<strong>{{ $t('officialTournaments.eligible') }} ({{ eligibleMembers(c).length }}):</strong>
|
||||
<table class="eligible-table" v-if="eligibleMembers(c).length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Alter</th>
|
||||
<th>Status</th>
|
||||
<th>Aktion</th>
|
||||
<th>Platzierung</th>
|
||||
<th>{{ $t('officialTournaments.name') }}</th>
|
||||
<th>{{ $t('officialTournaments.birthDate') }}</th>
|
||||
<th>{{ $t('officialTournaments.age') }}</th>
|
||||
<th>{{ $t('officialTournaments.status') }}</th>
|
||||
<th>{{ $t('officialTournaments.action') }}</th>
|
||||
<th>{{ $t('officialTournaments.placement') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -167,34 +167,34 @@
|
||||
<td>{{ formatDateStr(m.birthDate) }}</td>
|
||||
<td>{{ ageOnRef(m, c) ?? '–' }}</td>
|
||||
<td class="status-cell">
|
||||
<span v-if="getParticipation(c.id, m.id).participated" class="status-badge status-played">Hat gespielt</span>
|
||||
<span v-else-if="getParticipation(c.id, m.id).registered" class="status-badge status-registered">Angemeldet</span>
|
||||
<span v-else-if="getParticipation(c.id, m.id).wants" class="status-badge status-wants">Möchte teilnehmen</span>
|
||||
<span v-else class="status-badge status-none">Nicht interessiert</span>
|
||||
<span v-if="getParticipation(c.id, m.id).participated" class="status-badge status-played">{{ $t('officialTournaments.hasPlayed') }}</span>
|
||||
<span v-else-if="getParticipation(c.id, m.id).registered" class="status-badge status-registered">{{ $t('officialTournaments.registered') }}</span>
|
||||
<span v-else-if="getParticipation(c.id, m.id).wants" class="status-badge status-wants">{{ $t('officialTournaments.wantsToParticipate') }}</span>
|
||||
<span v-else class="status-badge status-none">{{ $t('officialTournaments.notInterested') }}</span>
|
||||
</td>
|
||||
<td class="action-cell">
|
||||
<button
|
||||
v-if="!getParticipation(c.id, m.id).participated && !getParticipation(c.id, m.id).registered && getParticipation(c.id, m.id).wants"
|
||||
@click="updateStatusForCompetition(c, m, 'register')"
|
||||
class="btn-status btn-register"
|
||||
title="Als angemeldet markieren">
|
||||
Angemeldet
|
||||
:title="$t('officialTournaments.markAsRegistered')">
|
||||
{{ $t('officialTournaments.registered') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="getParticipation(c.id, m.id).registered && !getParticipation(c.id, m.id).participated"
|
||||
@click="updateStatusForCompetition(c, m, 'participate')"
|
||||
class="btn-status btn-participate"
|
||||
title="Als teilgenommen markieren">
|
||||
Teilgenommen
|
||||
:title="$t('officialTournaments.markAsParticipated')">
|
||||
{{ $t('officialTournaments.participated') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="getParticipation(c.id, m.id).participated"
|
||||
@click="updateStatusForCompetition(c, m, 'reset')"
|
||||
class="btn-status btn-reset"
|
||||
title="Status zurücksetzen">
|
||||
Zurücksetzen
|
||||
:title="$t('officialTournaments.resetStatus')">
|
||||
{{ $t('common.reset') }}
|
||||
</button>
|
||||
<span v-else class="no-action">Checkbox aktivieren</span>
|
||||
<span v-else class="no-action">{{ $t('officialTournaments.checkBoxToActivate') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
@@ -219,7 +219,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="activeTab==='participants'">
|
||||
<h3>Teilnehmer</h3>
|
||||
<h3>{{ $t('officialTournaments.participants') }}</h3>
|
||||
<div class="filters">
|
||||
<label for="participantsFilter">Status:</label>
|
||||
<select id="participantsFilter" v-model="participantsFilter">
|
||||
@@ -228,7 +228,7 @@
|
||||
<option value="participated">Hat gespielt</option>
|
||||
</select>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn-primary" @click="generateParticipantsPdf">Teilnehmer-PDF</button>
|
||||
<button class="btn-primary" @click="generateParticipantsPdf">{{ $t('officialTournaments.participantsPdf') }}</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -299,7 +299,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3>Ergebnisse</h3>
|
||||
<h3>{{ $t('officialTournaments.results') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -687,7 +687,7 @@ export default {
|
||||
}
|
||||
groups.sort((a, b) => this.collator.compare(a.memberName, b.memberName));
|
||||
|
||||
const pdf = new PDFGenerator();
|
||||
const pdf = new PDFGenerator(20, 10, this.$t);
|
||||
pdf.addParticipantsSummary(title, dateText, groups);
|
||||
pdf.save('teilnehmer.pdf');
|
||||
},
|
||||
@@ -840,7 +840,7 @@ export default {
|
||||
await this.loadList();
|
||||
await this.showInfo('Erfolg', 'PDF erfolgreich hochgeladen.', '', 'success');
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Fehler beim Hochladen der PDF.');
|
||||
const message = safeErrorMessage(error, this.$t('officialTournaments.uploadError'));
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
}
|
||||
},
|
||||
@@ -941,7 +941,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Status:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Aktualisieren des Status', getSafeErrorMessage(error), 'error');
|
||||
this.showInfo(this.$t('common.error'), this.$t('officialTournaments.updateStatusError'), getSafeErrorMessage(error), 'error');
|
||||
}
|
||||
},
|
||||
async updatePlacement(item, value) {
|
||||
@@ -952,7 +952,7 @@ export default {
|
||||
await this.saveParticipation(competitionId, memberId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Platzierung:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Aktualisieren der Platzierung', getSafeErrorMessage(error), 'error');
|
||||
this.showInfo(this.$t('common.error'), this.$t('officialTournaments.updatePlacementError'), getSafeErrorMessage(error), 'error');
|
||||
}
|
||||
},
|
||||
async updateStatusForCompetition(competition, member, action) {
|
||||
@@ -977,7 +977,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Status:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Aktualisieren des Status', getSafeErrorMessage(error), 'error');
|
||||
this.showInfo(this.$t('common.error'), this.$t('officialTournaments.updateStatusError'), getSafeErrorMessage(error), 'error');
|
||||
}
|
||||
},
|
||||
// Auswahl Helfer + PDF-Generierung
|
||||
@@ -1076,7 +1076,7 @@ export default {
|
||||
},
|
||||
async generateMembersPdf() {
|
||||
if (!this.selectedMemberIds.length) return;
|
||||
const pdf = new PDFGenerator();
|
||||
const pdf = new PDFGenerator(20, 10, this.$t);
|
||||
const title = (this.parsed && this.parsed.parsedData && this.parsed.parsedData.title) ? this.parsed.parsedData.title : 'Offizielles Turnier';
|
||||
let first = true;
|
||||
for (const mid of this.selectedMemberIds) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Ausstehende Benutzeranfragen</h2>
|
||||
<h2>{{ $t('pendingApprovals.title') }}</h2>
|
||||
<div v-if="pendingUsers.length > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vorname</th>
|
||||
<th>Nachname</th>
|
||||
<th>Email</th>
|
||||
<th>Aktionen</th>
|
||||
<th>{{ $t('pendingApprovals.firstName') }}</th>
|
||||
<th>{{ $t('pendingApprovals.lastName') }}</th>
|
||||
<th>{{ $t('pendingApprovals.email') }}</th>
|
||||
<th>{{ $t('pendingApprovals.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -17,15 +17,15 @@
|
||||
<td>{{ user.lastName }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<button @click="approveUser(user.id)">Genehmigen</button>
|
||||
<button @click="rejectUser(user.id)">Ablehnen</button>
|
||||
<button @click="approveUser(user.id)">{{ $t('pendingApprovals.approve') }}</button>
|
||||
<button @click="rejectUser(user.id)">{{ $t('pendingApprovals.reject') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Keine ausstehenden Benutzeranfragen.</p>
|
||||
<p>{{ $t('pendingApprovals.noPendingRequests') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,10 +126,10 @@ export default {
|
||||
this.pendingUsers = response.data.map(entry => entry.user);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 403) {
|
||||
await this.showInfo('Keine Berechtigung', 'Sie haben keine Berechtigung, Freigaben zu verwalten.', 'Nur Administratoren können Mitgliedsanfragen bearbeiten.', 'error');
|
||||
await this.showInfo(this.$t('pendingApprovals.noPermission'), this.$t('pendingApprovals.noPermissionMessage'), this.$t('pendingApprovals.noPermissionDetails'), 'error');
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der ausstehenden Anfragen', getSafeErrorMessage(error), 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('pendingApprovals.errorLoadingRequests'), getSafeErrorMessage(error), 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
});
|
||||
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Genehmigen des Benutzers', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('pendingApprovals.errorApproving'), '', 'error');
|
||||
}
|
||||
},
|
||||
async rejectUser(userId) {
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
});
|
||||
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Ablehnen des Benutzers', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('pendingApprovals.errorRejecting'), '', 'error');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="permissions-view">
|
||||
<div class="header">
|
||||
<h1>Berechtigungsverwaltung</h1>
|
||||
<p class="subtitle">Verwalten Sie die Zugriffsrechte für Clubmitglieder</p>
|
||||
<h1>{{ $t('permissions.title') }}</h1>
|
||||
<p class="subtitle">{{ $t('permissions.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Lade Mitglieder...</div>
|
||||
<div v-if="loading" class="loading">{{ $t('permissions.loadingMembers') }}</div>
|
||||
<div v-else-if="error" class="error">{{ error }}</div>
|
||||
<div v-else class="permissions-content">
|
||||
<!-- Role Legend -->
|
||||
<div class="role-legend">
|
||||
<h3>Verfügbare Rollen</h3>
|
||||
<h3>{{ $t('permissions.availableRoles') }}</h3>
|
||||
<div class="roles-grid">
|
||||
<div v-for="role in availableRoles" :key="role.value" class="role-card">
|
||||
<div class="role-name">{{ role.label }}</div>
|
||||
@@ -21,14 +21,14 @@
|
||||
|
||||
<!-- Members Table -->
|
||||
<div class="members-table">
|
||||
<h3>Clubmitglieder</h3>
|
||||
<h3>{{ $t('permissions.clubMembers') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th v-if="!isReadOnly">Aktionen</th>
|
||||
<th>{{ $t('permissions.email') }}</th>
|
||||
<th>{{ $t('permissions.role') }}</th>
|
||||
<th>{{ $t('permissions.status') }}</th>
|
||||
<th v-if="!isReadOnly">{{ $t('permissions.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -47,7 +47,7 @@
|
||||
</select>
|
||||
<span v-else class="role-badge" :class="`role-${member.role}`">
|
||||
{{ getRoleLabel(member.role) }}
|
||||
<span v-if="member.isOwner" class="owner-badge">👑 Ersteller</span>
|
||||
<span v-if="member.isOwner" class="owner-badge">👑 {{ $t('permissions.creator') }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -56,18 +56,18 @@
|
||||
class="status-badge status-active"
|
||||
:class="{ 'clickable': !member.isOwner && !isReadOnly }"
|
||||
@click="!member.isOwner && !isReadOnly ? toggleMemberStatus(member) : null"
|
||||
:title="!member.isOwner && !isReadOnly ? 'Klicken zum Deaktivieren' : ''"
|
||||
:title="!member.isOwner && !isReadOnly ? $t('permissions.clickToDeactivate') : ''"
|
||||
>
|
||||
✓ Aktiv
|
||||
✓ {{ $t('permissions.active') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="status-badge status-inactive"
|
||||
:class="{ 'clickable': !isReadOnly }"
|
||||
@click="!isReadOnly ? toggleMemberStatus(member) : null"
|
||||
:title="!isReadOnly ? 'Klicken zum Aktivieren' : ''"
|
||||
:title="!isReadOnly ? $t('permissions.clickToActivate') : ''"
|
||||
>
|
||||
✗ Deaktiviert
|
||||
✗ {{ $t('permissions.inactive') }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!isReadOnly">
|
||||
@@ -76,7 +76,7 @@
|
||||
@click="openPermissionsDialog(member)"
|
||||
class="btn-small"
|
||||
>
|
||||
Anpassen
|
||||
{{ $t('permissions.customize') }}
|
||||
</button>
|
||||
<span v-else class="muted">—</span>
|
||||
</td>
|
||||
@@ -90,21 +90,21 @@
|
||||
<div v-if="selectedMember" class="dialog-overlay" @click.self="closePermissionsDialog">
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-header">
|
||||
<h2>Berechtigungen für {{ selectedMember.user?.email }}</h2>
|
||||
<h2>{{ $t('permissions.permissionsFor') }} {{ selectedMember.user?.email }}</h2>
|
||||
<button @click="closePermissionsDialog" class="close-btn">×</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<p class="info-text">
|
||||
Basis-Rolle: <strong>{{ getRoleLabel(selectedMember.role) }}</strong><br>
|
||||
Hier können Sie individuelle Anpassungen vornehmen.
|
||||
{{ $t('permissions.baseRole') }}: <strong>{{ getRoleLabel(selectedMember.role) }}</strong><br>
|
||||
{{ $t('permissions.customizeInfo') }}
|
||||
</p>
|
||||
|
||||
<div class="permissions-grid">
|
||||
<div v-for="(resource, key) in permissionStructure" :key="key" class="permission-group">
|
||||
<div class="permission-group-header">
|
||||
<h4>{{ resource.label }}</h4>
|
||||
<button class="btn-reset" @click="resetResource(key)" :disabled="isReadOnly">Zurücksetzen</button>
|
||||
<button class="btn-reset" @click="resetResource(key)" :disabled="isReadOnly">{{ $t('permissions.reset') }}</button>
|
||||
</div>
|
||||
<div class="permission-actions">
|
||||
<div v-for="action in resource.actions" :key="action" class="permission-row">
|
||||
@@ -122,9 +122,9 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<button @click="resetAll" class="btn-secondary" :disabled="isReadOnly">Alle zurücksetzen</button>
|
||||
<button @click="closePermissionsDialog" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="saveCustomPermissions" class="btn-primary">Speichern</button>
|
||||
<button @click="resetAll" class="btn-secondary" :disabled="isReadOnly">{{ $t('permissions.resetAll') }}</button>
|
||||
<button @click="closePermissionsDialog" class="btn-secondary">{{ $t('permissions.cancel') }}</button>
|
||||
<button @click="saveCustomPermissions" class="btn-primary">{{ $t('permissions.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,6 +155,7 @@
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { usePermissions } from '../composables/usePermissions.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
@@ -168,6 +169,7 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { isOwner, isAdmin, can } = usePermissions();
|
||||
|
||||
const infoDialog = ref({
|
||||
@@ -184,7 +186,7 @@ export default {
|
||||
details: '',
|
||||
type: 'info',
|
||||
confirmText: 'OK',
|
||||
cancelText: 'Abbrechen',
|
||||
cancelText: t('permissions.cancel'),
|
||||
showCancel: true,
|
||||
resolveCallback: null
|
||||
});
|
||||
@@ -208,7 +210,7 @@ export default {
|
||||
details,
|
||||
type,
|
||||
confirmText: options.confirmText || 'OK',
|
||||
cancelText: options.cancelText || 'Abbrechen',
|
||||
cancelText: options.cancelText || t('permissions.cancel'),
|
||||
showCancel: options.showCancel !== false,
|
||||
resolveCallback: resolve
|
||||
};
|
||||
@@ -263,7 +265,7 @@ export default {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
} else {
|
||||
error.value = err.response?.data?.error || 'Fehler beim Laden der Daten';
|
||||
error.value = err.response?.data?.error || t('permissions.errorLoadingData');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -281,7 +283,7 @@ export default {
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
console.error('Error updating role:', err);
|
||||
await showInfo('Fehler', err.response?.data?.error || 'Fehler beim Aktualisieren der Rolle', '', 'error');
|
||||
await showInfo(t('messages.error'), err.response?.data?.error || t('permissions.errorUpdatingRole'), '', 'error');
|
||||
// Reload to revert changes
|
||||
await loadData();
|
||||
}
|
||||
@@ -395,7 +397,7 @@ export default {
|
||||
await loadData(true);
|
||||
} catch (err) {
|
||||
console.error('Error saving permissions:', err);
|
||||
await showInfo('Fehler', err.response?.data?.error || 'Fehler beim Speichern der Berechtigungen', '', 'error');
|
||||
await showInfo(t('messages.error'), err.response?.data?.error || t('permissions.errorSavingPermissions'), '', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
160
frontend/src/views/PersonalSettings.vue
Normal file
160
frontend/src/views/PersonalSettings.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>{{ $t('settings.personalSettings') }}</h1>
|
||||
|
||||
<div class="settings-container">
|
||||
<div class="settings-section card">
|
||||
<h2 class="section-title">{{ $t('settings.language') }}</h2>
|
||||
<p class="section-description">{{ $t('settings.languageDescription') }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="language-select">{{ $t('settings.selectLanguage') }}</label>
|
||||
<select
|
||||
id="language-select"
|
||||
v-model="selectedLanguage"
|
||||
@change="changeLanguage"
|
||||
class="language-select"
|
||||
>
|
||||
<option
|
||||
v-for="(name, code) in availableLanguages"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'PersonalSettings',
|
||||
data() {
|
||||
return {
|
||||
selectedLanguage: this.$i18n.locale,
|
||||
languageCodes: ['de', 'en-GB', 'en-US', 'en-AU', 'de-CH', 'fr', 'es', 'it', 'pl', 'ja', 'zh', 'tl', 'th', 'fil']
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['language']),
|
||||
availableLanguages() {
|
||||
const languages = {};
|
||||
this.languageCodes.forEach(code => {
|
||||
languages[code] = this.$t(`languages.${code}`);
|
||||
});
|
||||
return languages;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Set initial language from store
|
||||
if (this.language) {
|
||||
this.selectedLanguage = this.language;
|
||||
this.$i18n.locale = this.language;
|
||||
} else {
|
||||
this.selectedLanguage = this.$i18n.locale;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setLanguage']),
|
||||
changeLanguage() {
|
||||
this.$i18n.locale = this.selectedLanguage;
|
||||
this.setLanguage(this.selectedLanguage);
|
||||
// Sprache wurde geändert - Seite wird automatisch neu geladen
|
||||
console.log(this.$t('settings.languageChanged'));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-color, #333);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary-color, #007bff);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
color: var(--text-color, #333);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.language-select:hover {
|
||||
border-color: var(--primary-color, #007bff);
|
||||
}
|
||||
|
||||
.language-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #007bff);
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<template>
|
||||
<div class="predef-activities">
|
||||
<h2>Vordefinierte Aktivitäten</h2>
|
||||
<h2>{{ $t('predefinedActivities.title') }}</h2>
|
||||
<div class="grid">
|
||||
<div class="list">
|
||||
<div class="toolbar">
|
||||
<button @click="startCreate" class="btn-primary">Neu</button>
|
||||
<button @click="reload" class="btn-secondary">Neu laden</button>
|
||||
<button @click="startCreate" class="btn-primary">{{ $t('predefinedActivities.new') }}</button>
|
||||
<button @click="reload" class="btn-secondary">{{ $t('predefinedActivities.reload') }}</button>
|
||||
</div>
|
||||
<div class="search-section">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
@input="onSearchInput"
|
||||
placeholder="Kürzel suchen (z.B. 'as vh us' oder 'vh as us')..."
|
||||
:placeholder="$t('predefinedActivities.searchPlaceholder')"
|
||||
class="search-input"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="clearSearch" class="btn-clear-search">✕</button>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="deduplicate" class="btn-secondary">Doppelungen zusammenführen</button>
|
||||
<button @click="deduplicate" class="btn-secondary">{{ $t('predefinedActivities.deduplicate') }}</button>
|
||||
</div>
|
||||
<div class="merge-tools">
|
||||
<select v-model="mergeSourceId">
|
||||
<option disabled value="">Quelle wählen…</option>
|
||||
<option disabled value="">{{ $t('predefinedActivities.selectSource') }}</option>
|
||||
<option v-for="a in sortedActivities" :key="'s'+a.id" :value="a.id">{{ formatItem(a) }}</option>
|
||||
</select>
|
||||
<span>→</span>
|
||||
<select v-model="mergeTargetId">
|
||||
<option disabled value="">Ziel wählen…</option>
|
||||
<option disabled value="">{{ $t('predefinedActivities.selectTarget') }}</option>
|
||||
<option v-for="a in sortedActivities" :key="'t'+a.id" :value="a.id">{{ formatItem(a) }}</option>
|
||||
</select>
|
||||
<button class="btn-secondary" :disabled="!canMerge" @click="mergeSelected">Zusammenführen</button>
|
||||
<button class="btn-secondary" :disabled="!canMerge" @click="mergeSelected">{{ $t('predefinedActivities.merge') }}</button>
|
||||
</div>
|
||||
<ul class="items">
|
||||
<li v-for="a in sortedActivities" :key="a.id" :class="{ active: selectedActivity && selectedActivity.id === a.id }" @click="select(a)">
|
||||
@@ -38,7 +38,7 @@
|
||||
<strong>{{ a.code ? '[' + a.code + '] ' : '' }}{{ a.name }}</strong>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span v-if="a.duration">{{ a.duration }} min</span>
|
||||
<span v-if="a.duration">{{ a.duration }} {{ $t('predefinedActivities.min') }}</span>
|
||||
<span v-if="a.durationText"> ({{ a.durationText }})</span>
|
||||
</div>
|
||||
</li>
|
||||
@@ -46,62 +46,62 @@
|
||||
</div>
|
||||
|
||||
<div class="detail" v-if="editModel">
|
||||
<h3>{{ editModel.id ? 'Aktivität bearbeiten' : 'Neue Aktivität' }}</h3>
|
||||
<h3>{{ editModel.id ? $t('predefinedActivities.editActivity') : $t('predefinedActivities.newActivity') }}</h3>
|
||||
<div class="drawing-button-section">
|
||||
<button type="button" class="btn-secondary" @click="showDrawingDialog = true">
|
||||
{{ editModel.drawingData ? 'Übungszeichnung bearbeiten' : 'Übungszeichnung erstellen' }}
|
||||
{{ editModel.drawingData ? $t('predefinedActivities.editDrawing') : $t('predefinedActivities.createDrawing') }}
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="save">
|
||||
<label>Name
|
||||
<label>{{ $t('predefinedActivities.name') }}
|
||||
<input type="text" v-model="editModel.name" required />
|
||||
</label>
|
||||
<label>Kürzel
|
||||
<label>{{ $t('predefinedActivities.code') }}
|
||||
<input type="text" v-model="editModel.code" />
|
||||
</label>
|
||||
<label>Dauer (Minuten)
|
||||
<label>{{ $t('predefinedActivities.duration') }}
|
||||
<input type="number" v-model.number="editModel.duration" min="0" />
|
||||
</label>
|
||||
<label>Dauer (Text)
|
||||
<input type="text" v-model="editModel.durationText" placeholder="z.B. 2x7" />
|
||||
<label>{{ $t('predefinedActivities.durationText') }}
|
||||
<input type="text" v-model="editModel.durationText" :placeholder="$t('predefinedActivities.durationTextPlaceholder')" />
|
||||
</label>
|
||||
<label>Beschreibung
|
||||
<label>{{ $t('predefinedActivities.description') }}
|
||||
<textarea v-model="editModel.description" rows="4" />
|
||||
</label>
|
||||
<div class="image-section">
|
||||
<h4>Bild hinzufügen</h4>
|
||||
<p class="image-help">Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:</p>
|
||||
<h4>{{ $t('predefinedActivities.addImage') }}</h4>
|
||||
<p class="image-help">{{ $t('predefinedActivities.imageHelp') }}</p>
|
||||
|
||||
<label>Bild-Link (optional)
|
||||
<input type="text" v-model="editModel.imageLink" placeholder="z.B. https://example.com/bild.jpg oder /api/predefined-activities/:id/image/:imageId" />
|
||||
<label>{{ $t('predefinedActivities.imageLink') }}
|
||||
<input type="text" v-model="editModel.imageLink" :placeholder="$t('predefinedActivities.imageLinkPlaceholder')" />
|
||||
</label>
|
||||
|
||||
<div class="upload-section">
|
||||
<label>Oder Bild hochladen:
|
||||
<label>{{ $t('predefinedActivities.orUploadImage') }}
|
||||
<input type="file" accept="image/*" @change="onFileChange" />
|
||||
</label>
|
||||
<button class="btn-secondary" :disabled="!selectedFile" @click="uploadImage">
|
||||
{{ editModel.id ? 'Hochladen' : 'Nach Speichern hochladen' }}
|
||||
{{ editModel.id ? $t('predefinedActivities.upload') : $t('predefinedActivities.uploadAfterSave') }}
|
||||
</button>
|
||||
<p v-if="!editModel.id" class="upload-note">
|
||||
Hinweis: Das Bild wird erst nach dem Speichern der Aktivität hochgeladen.
|
||||
{{ $t('predefinedActivities.uploadNote') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image-list" v-if="images && images.length">
|
||||
<h5>Hochgeladene Bilder:</h5>
|
||||
<h5>{{ $t('predefinedActivities.uploadedImages') }}</h5>
|
||||
<div class="image-grid">
|
||||
<div v-for="img in images" :key="img.id" class="image-item">
|
||||
<img :src="imageUrl(img)" alt="Predefined Activity Image" />
|
||||
<button class="btn-small btn-danger" @click="deleteImage(img.id)">Löschen</button>
|
||||
<button class="btn-small btn-danger" @click="deleteImage(img.id)">{{ $t('predefinedActivities.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<button type="button" class="btn-secondary" @click="cancel">Abbrechen</button>
|
||||
<button type="submit" class="btn-primary">{{ $t('common.save') }}</button>
|
||||
<button type="button" class="btn-secondary" @click="cancel">{{ $t('predefinedActivities.cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>Register</h2>
|
||||
<h2>{{ $t('auth.register') }}</h2>
|
||||
<form @submit.prevent="register">
|
||||
<input v-model="email" type="email" placeholder="Email" required />
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Register</button>
|
||||
<input v-model="email" type="email" :placeholder="$t('auth.email')" required />
|
||||
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
|
||||
<button type="submit">{{ $t('auth.register') }}</button>
|
||||
</form>
|
||||
<div class="login-link">
|
||||
<p>Bereits ein Konto? <router-link to="/login">Zum Login</router-link></p>
|
||||
<p>{{ $t('auth.hasAccount') }} <router-link to="/login">{{ $t('auth.toLogin') }}</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,10 +93,10 @@ export default {
|
||||
async register() {
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_BACKEND || 'http://localhost:3005'}/api/auth/register`, { email: this.email, password: this.password });
|
||||
await this.showInfo('Erfolg', 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mails, um den Account zu aktivieren.', '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('auth.registerSuccess'), '', 'success');
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, 'Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('auth.registerFailed'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Spielpläne</h2>
|
||||
<h2>{{ $t('schedule.title') }}</h2>
|
||||
|
||||
<SeasonSelector v-model="selectedSeasonId" @season-change="onSeasonChange" :show-current-season="true" />
|
||||
|
||||
<button @click="openImportModal">Spielplanimport</button>
|
||||
<button @click="openImportModal">{{ $t('schedule.importSchedule') }}</button>
|
||||
<button
|
||||
v-if="playerSelectionDialog.match"
|
||||
@click="openGalleryDialog"
|
||||
class="btn-secondary"
|
||||
:disabled="galleryLoading"
|
||||
>
|
||||
{{ galleryLoading ? 'Galerie wird geladen…' : 'Mitglieder-Galerie' }}
|
||||
{{ galleryLoading ? $t('schedule.galleryLoading') : $t('schedule.gallery') }}
|
||||
</button>
|
||||
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
|
||||
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
|
||||
@@ -20,14 +20,14 @@
|
||||
</div>
|
||||
<div class="output">
|
||||
<ul>
|
||||
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
|
||||
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
|
||||
<li class="special-link" @click="loadAllMatches">{{ $t('schedule.overallSchedule') }}</li>
|
||||
<li class="special-link" @click="loadAdultMatches">{{ $t('schedule.adultSchedule') }}</li>
|
||||
<li class="divider"></li>
|
||||
<li v-for="team in teams" :key="team.id" @click="loadMatchesForTeam(team)"
|
||||
:class="{ active: selectedTeam && selectedTeam.id === team.id }">
|
||||
{{ team.name }}<span class="team-league" v-if="team.league && team.league.name"> ({{ team.league.name }})</span>
|
||||
</li>
|
||||
<li v-if="teams.length === 0" class="no-leagues">Keine Teams für diese Saison gefunden</li>
|
||||
<li v-if="teams.length === 0" class="no-leagues">{{ $t('schedule.noTeamsFound') }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex-item" ref="scheduleContainer">
|
||||
@@ -37,13 +37,13 @@
|
||||
:class="['tab-button', { active: activeTab === 'schedule' }]"
|
||||
@click="activeTab = 'schedule'"
|
||||
>
|
||||
📅 Spielplan
|
||||
📅 {{ $t('schedule.scheduleTab') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'table' }]"
|
||||
@click="activeTab = 'table'"
|
||||
>
|
||||
📊 Tabelle
|
||||
📊 {{ $t('schedule.tableTab') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,23 +51,23 @@
|
||||
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content">
|
||||
<!-- Spielplan Tab -->
|
||||
<div v-show="activeTab === 'schedule'" class="tab-panel">
|
||||
<button @click="generatePDF">Download PDF</button>
|
||||
<button @click="generatePDF">{{ $t('schedule.downloadPDF') }}</button>
|
||||
<div v-if="matches.length > 0">
|
||||
<h3>Spiele für {{ selectedLeague }}</h3>
|
||||
<h3>{{ $t('schedule.gamesFor') }} {{ selectedLeague }}</h3>
|
||||
<table id="schedule-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Uhrzeit</th>
|
||||
<th>Heimmannschaft</th>
|
||||
<th>Gastmannschaft</th>
|
||||
<th>Ergebnis</th>
|
||||
<th>{{ $t('schedule.date') }}</th>
|
||||
<th>{{ $t('schedule.time') }}</th>
|
||||
<th>{{ $t('schedule.homeTeam') }}</th>
|
||||
<th>{{ $t('schedule.guestTeam') }}</th>
|
||||
<th>{{ $t('schedule.result') }}</th>
|
||||
<th
|
||||
v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">
|
||||
Altersklasse</th>
|
||||
<th>Code</th>
|
||||
<th>Heim-PIN</th>
|
||||
<th>Gast-PIN</th>
|
||||
v-if="selectedLeague === $t('schedule.overallSchedule') || selectedLeague === $t('schedule.adultSchedule')">
|
||||
{{ $t('schedule.ageClass') }}</th>
|
||||
<th>{{ $t('schedule.code') }}</th>
|
||||
<th>{{ $t('schedule.homePin') }}</th>
|
||||
<th>{{ $t('schedule.guestPin') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -78,7 +78,7 @@
|
||||
:class="getRowClass(match.date)"
|
||||
style="cursor: pointer;">
|
||||
<td>{{ formatDate(match.date) }}</td>
|
||||
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}</td>
|
||||
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' ' + $t('common.time') : 'N/A' }}</td>
|
||||
<td :class="{ 'highlighted-club': isClubHighlighted(match.homeTeam?.name) }">
|
||||
{{ match.homeTeam?.name || 'N/A' }}
|
||||
</td>
|
||||
@@ -92,30 +92,30 @@
|
||||
<span v-else class="result-pending">—</span>
|
||||
</td>
|
||||
<td
|
||||
v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">
|
||||
v-if="selectedLeague === $t('schedule.overallSchedule') || selectedLeague === $t('schedule.adultSchedule')">
|
||||
{{ match.leagueDetails?.name || 'N/A' }}</td>
|
||||
<td class="code-cell">
|
||||
<span v-if="match.code && selectedLeague && selectedLeague !== ''">
|
||||
<button @click.stop="openMatchReport(match)" class="nuscore-link"
|
||||
title="Spielberichtsbogen öffnen">📊</button>
|
||||
<span class="code-value clickable" @click.stop="copyToClipboard(match.code, 'Code', $event)"
|
||||
:title="'Code kopieren: ' + match.code">{{ match.code }}</span>
|
||||
:title="$t('schedule.openMatchReport')">📊</button>
|
||||
<span class="code-value clickable" @click.stop="copyToClipboard(match.code, $t('schedule.code'), $event)"
|
||||
:title="$t('schedule.copyCode') + ': ' + match.code">{{ match.code }}</span>
|
||||
</span>
|
||||
<span v-else-if="match.code" class="code-value clickable"
|
||||
@click.stop="copyToClipboard(match.code, 'Code', $event)"
|
||||
:title="'Code kopieren: ' + match.code">{{ match.code }}</span>
|
||||
@click.stop="copyToClipboard(match.code, $t('schedule.code'), $event)"
|
||||
:title="$t('schedule.copyCode') + ': ' + match.code">{{ match.code }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td class="pin-cell">
|
||||
<span v-if="match.homePin" class="pin-value clickable"
|
||||
@click.stop="copyToClipboard(match.homePin, 'Heim-PIN', $event)"
|
||||
:title="'Heim-PIN kopieren: ' + match.homePin">{{ match.homePin }}</span>
|
||||
@click.stop="copyToClipboard(match.homePin, $t('schedule.homePin'), $event)"
|
||||
:title="$t('schedule.copyHomePin') + ': ' + match.homePin">{{ match.homePin }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td class="pin-cell">
|
||||
<span v-if="match.guestPin" class="pin-value clickable"
|
||||
@click.stop="copyToClipboard(match.guestPin, 'Gast-PIN', $event)"
|
||||
:title="'Gast-PIN kopieren: ' + match.guestPin">{{ match.guestPin }}</span>
|
||||
@click.stop="copyToClipboard(match.guestPin, $t('schedule.guestPin'), $event)"
|
||||
:title="$t('schedule.copyGuestPin') + ': ' + match.guestPin">{{ match.guestPin }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -123,7 +123,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Keine Spiele vorhanden</p>
|
||||
<p>{{ $t('schedule.noGames') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,18 +131,18 @@
|
||||
<div v-show="activeTab === 'table'" class="tab-panel">
|
||||
<div class="table-section">
|
||||
<div class="table-header">
|
||||
<h3>Ligatabelle</h3>
|
||||
<h3>{{ $t('schedule.leagueTable') }}</h3>
|
||||
</div>
|
||||
<div v-if="leagueTable.length > 0">
|
||||
<table id="league-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Platz</th>
|
||||
<th>Team</th>
|
||||
<th>Matches</th>
|
||||
<th>Sätze</th>
|
||||
<th>Pkt.</th>
|
||||
<th>Bälle</th>
|
||||
<th>{{ $t('schedule.position') }}</th>
|
||||
<th>{{ $t('schedule.team') }}</th>
|
||||
<th>{{ $t('schedule.matches') }}</th>
|
||||
<th>{{ $t('schedule.sets') }}</th>
|
||||
<th>{{ $t('schedule.points') }}</th>
|
||||
<th>{{ $t('schedule.balls') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -159,7 +159,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Keine Tabellendaten verfügbar</p>
|
||||
<p>{{ $t('schedule.noTableData') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,21 +184,21 @@
|
||||
<!-- Mitglieder-Galerie Dialog -->
|
||||
<BaseDialog
|
||||
v-model="showGalleryDialog"
|
||||
title="Mitglieder-Galerie - Klicken Sie auf ein Bild, um als 'Bereit' zu markieren"
|
||||
:title="$t('schedule.galleryTitle')"
|
||||
size="large"
|
||||
:close-on-overlay="true"
|
||||
@close="closeGalleryDialog"
|
||||
>
|
||||
<div class="gallery-dialog-content">
|
||||
<div class="gallery-controls">
|
||||
<label for="gallery-size">Bildgröße:</label>
|
||||
<label for="gallery-size">{{ $t('schedule.imageSize') }}:</label>
|
||||
<select id="gallery-size" v-model="gallerySize" @change="loadGalleryMembers" :disabled="galleryLoading">
|
||||
<option :value="100">100x100 px</option>
|
||||
<option :value="150">150x150 px</option>
|
||||
<option :value="200">200x200 px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="galleryLoading" class="gallery-loading">Galerie wird geladen…</div>
|
||||
<div v-if="galleryLoading" class="gallery-loading">{{ $t('schedule.galleryLoading') }}</div>
|
||||
<div v-else-if="galleryMembers.length > 0" class="gallery-members-grid">
|
||||
<div
|
||||
v-for="member in galleryMembers"
|
||||
@@ -216,7 +216,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gallery-error">
|
||||
{{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
|
||||
{{ galleryError || $t('schedule.noMembersWithImages') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
@@ -224,28 +224,28 @@
|
||||
<!-- Player Selection Dialog -->
|
||||
<BaseDialog
|
||||
v-model="playerSelectionDialog.isOpen"
|
||||
:title="`Spielerauswahl - ${playerSelectionDialog.match?.homeTeam?.name || ''} vs ${playerSelectionDialog.match?.guestTeam?.name || ''}`"
|
||||
:title="`${$t('schedule.playerSelection')} - ${playerSelectionDialog.match?.homeTeam?.name || ''} ${$t('schedule.vs')} ${playerSelectionDialog.match?.guestTeam?.name || ''}`"
|
||||
@close="closePlayerSelectionDialog"
|
||||
:max-width="800"
|
||||
>
|
||||
<div v-if="playerSelectionDialog.loading" class="loading-state">
|
||||
Lade Mitglieder...
|
||||
{{ $t('schedule.loadingMembers') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="player-selection-content">
|
||||
<div class="match-info">
|
||||
<p><strong>Datum:</strong> {{ formatDate(playerSelectionDialog.match?.date) }}</p>
|
||||
<p><strong>Uhrzeit:</strong> {{ playerSelectionDialog.match?.time ? playerSelectionDialog.match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}</p>
|
||||
<p><strong>{{ $t('schedule.date') }}:</strong> {{ formatDate(playerSelectionDialog.match?.date) }}</p>
|
||||
<p><strong>{{ $t('schedule.time') }}:</strong> {{ playerSelectionDialog.match?.time ? playerSelectionDialog.match.time.toString().slice(0, 5) + ' ' + $t('common.time') : 'N/A' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="player-list">
|
||||
<table class="player-selection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Spieler</th>
|
||||
<th>Bereit</th>
|
||||
<th>Vorgesehen</th>
|
||||
<th>Gespielt</th>
|
||||
<th>{{ $t('schedule.player') }}</th>
|
||||
<th>{{ $t('schedule.ready') }}</th>
|
||||
<th>{{ $t('schedule.planned') }}</th>
|
||||
<th>{{ $t('schedule.played') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -279,13 +279,13 @@
|
||||
</table>
|
||||
|
||||
<div v-if="playerSelectionDialog.members.length === 0" class="no-members">
|
||||
Keine aktiven Mitglieder gefunden
|
||||
{{ $t('schedule.noActiveMembers') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button @click="savePlayerSelection" class="btn-save">Speichern</button>
|
||||
<button @click="closePlayerSelectionDialog" class="btn-cancel">Abbrechen</button>
|
||||
<button @click="savePlayerSelection" class="btn-save">{{ $t('schedule.save') }}</button>
|
||||
<button @click="closePlayerSelectionDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
@@ -455,7 +455,7 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
console.error('Error details:', error.response?.data);
|
||||
await this.showInfo('Fehler', 'Laden der Mitgliederliste fehlgeschlagen.', getSafeErrorMessage(error), 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingMembers'), getSafeErrorMessage(error), 'error');
|
||||
} finally {
|
||||
this.playerSelectionDialog.loading = false;
|
||||
}
|
||||
@@ -516,19 +516,19 @@ export default {
|
||||
});
|
||||
|
||||
if (closeDialog) {
|
||||
await this.showInfo('Erfolg', 'Spielerauswahl gespeichert', '', 'success');
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('schedule.playerSelectionSaved'), '', 'success');
|
||||
this.closePlayerSelectionDialog();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving player selection:', error);
|
||||
await this.showInfo('Fehler', 'Fehler beim Speichern der Spielerauswahl', '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('schedule.errorSavingPlayerSelection'), '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Gallery Methods
|
||||
async openGalleryDialog() {
|
||||
if (!this.playerSelectionDialog.match) {
|
||||
await this.showInfo('Hinweis', 'Bitte wählen Sie zuerst ein Spiel aus', '', 'info');
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('schedule.pleaseSelectGame'), '', 'info');
|
||||
return;
|
||||
}
|
||||
this.showGalleryDialog = true;
|
||||
@@ -546,7 +546,7 @@ export default {
|
||||
this.galleryMembers = response.data.members || [];
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Galerie:', error);
|
||||
this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht geladen werden.';
|
||||
this.galleryError = error?.response?.data?.error || this.$t('schedule.errorLoadingGallery');
|
||||
this.galleryMembers = [];
|
||||
} finally {
|
||||
this.galleryLoading = false;
|
||||
@@ -656,11 +656,11 @@ export default {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
this.showInfo('Erfolg', 'Spielplan erfolgreich importiert!', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('schedule.scheduleImportSuccess'), '', 'success');
|
||||
this.closeImportModal();
|
||||
this.loadTeams();
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Importieren der CSV-Datei', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorImportingCSV'), '', 'error');
|
||||
}
|
||||
},
|
||||
// Sortierfunktion für Ligen
|
||||
@@ -749,7 +749,7 @@ export default {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('ScheduleView: Error loading teams:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der Teams', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingTeams'), '', 'error');
|
||||
}
|
||||
},
|
||||
onSeasonChange(season) {
|
||||
@@ -762,7 +762,7 @@ export default {
|
||||
},
|
||||
async loadMatchesForTeam(team) {
|
||||
if (!team || !team.league) {
|
||||
this.showInfo('Hinweis', 'Für dieses Team ist keine Liga hinterlegt. Bitte zuerst eine Liga zuordnen.', '', 'warning');
|
||||
this.showInfo(this.$t('messages.warning'), this.$t('schedule.noLeagueForTeam'), '', 'warning');
|
||||
return;
|
||||
}
|
||||
this.selectedTeam = team;
|
||||
@@ -775,24 +775,24 @@ export default {
|
||||
// Lade auch die Tabellendaten für diese Liga
|
||||
await this.loadLeagueTable(team.league.id);
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der Matches', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingMatches'), '', 'error');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
async loadAllMatches() {
|
||||
this.selectedLeague = 'Gesamtspielplan';
|
||||
this.selectedLeague = this.$t('schedule.overallSchedule');
|
||||
this.selectedTeam = null;
|
||||
try {
|
||||
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
|
||||
this.matches = response.data;
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden des Gesamtspielplans', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingOverallSchedule'), '', 'error');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
async loadAdultMatches() {
|
||||
this.selectedLeague = 'Spielplan Erwachsene';
|
||||
this.selectedLeague = this.$t('schedule.adultSchedule');
|
||||
this.selectedTeam = null;
|
||||
try {
|
||||
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
|
||||
@@ -806,7 +806,7 @@ export default {
|
||||
return !isYouth;
|
||||
});
|
||||
} catch (error) {
|
||||
this.showInfo('Fehler', 'Fehler beim Laden des Erwachsenenspielplans', '', 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingAdultSchedule'), '', 'error');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
@@ -834,12 +834,12 @@ export default {
|
||||
const element = this.$refs.scheduleContainer;
|
||||
const highlightName = this.getCurrentClubName();
|
||||
if (element) {
|
||||
const pdfGen = new PDFGenerator();
|
||||
pdfGen.addTitle(`Spiele für ${highlightName} in ${this.selectedLeague}`);
|
||||
const pdfGen = new PDFGenerator(20, 10, this.$t);
|
||||
pdfGen.addTitle(`${this.$t('schedule.gamesFor')} ${highlightName} ${this.$t('common.in')} ${this.selectedLeague}`);
|
||||
|
||||
// Bestimme die auszuschließenden Spalten
|
||||
// Spaltenstruktur: Datum, Uhrzeit, Heimmannschaft, Gastmannschaft, Ergebnis, [Altersklasse], Code, Heim-PIN, Gast-PIN
|
||||
const hasAgeClass = this.selectedLeague === 'Gesamtspielplan' || this.selectedLeague === 'Spielplan Erwachsene';
|
||||
const hasAgeClass = this.selectedLeague === this.$t('schedule.overallSchedule') || this.selectedLeague === this.$t('schedule.adultSchedule');
|
||||
let excludeColumns;
|
||||
if (hasAgeClass) {
|
||||
// Mit Altersklasse: Ergebnis=4, Code=6, Heim-PIN=7, Gast-PIN=8
|
||||
@@ -858,9 +858,9 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
pdfGen.save('Spielpläne.pdf');
|
||||
pdfGen.save(this.$t('schedule.schedulePDF'));
|
||||
} else {
|
||||
await this.showInfo('Hinweis', 'Keine Matches gefunden, um PDF zu generieren.', '', 'info');
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('schedule.noMatchesForPDF'), '', 'info');
|
||||
}
|
||||
},
|
||||
getUniqueLocations() {
|
||||
@@ -975,8 +975,8 @@ export default {
|
||||
},
|
||||
|
||||
async fetchTableFromMyTischtennis() {
|
||||
if (!this.selectedTeam || !this.selectedTeam.league || this.selectedLeague === 'Gesamtspielplan' || this.selectedLeague === 'Spielplan Erwachsene') {
|
||||
this.showInfo('Info', 'Bitte wählen Sie eine spezifische Liga aus, um die Tabelle zu laden.', '', 'info');
|
||||
if (!this.selectedTeam || !this.selectedTeam.league || this.selectedLeague === this.$t('schedule.overallSchedule') || this.selectedLeague === this.$t('schedule.adultSchedule')) {
|
||||
this.showInfo(this.$t('messages.info'), this.$t('schedule.selectSpecificLeague'), '', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -985,10 +985,10 @@ export default {
|
||||
const leagueId = this.selectedTeam.league.id;
|
||||
const response = await apiClient.post(`/matches/leagues/${this.currentClub}/table/${leagueId}/fetch`);
|
||||
this.leagueTable = response.data.data;
|
||||
this.showInfo('Erfolg', 'Tabellendaten erfolgreich von MyTischtennis geladen!', '', 'success');
|
||||
this.showInfo(this.$t('messages.success'), this.$t('schedule.tableDataLoaded'), '', 'success');
|
||||
} catch (error) {
|
||||
console.error('ScheduleView: Error fetching table from MyTischtennis:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Laden der Tabellendaten von MyTischtennis', getSafeErrorMessage(error), 'error');
|
||||
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingTable'), getSafeErrorMessage(error), 'error');
|
||||
} finally {
|
||||
this.fetchingTable = false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Team-Verwaltung</h2>
|
||||
<h2>{{ $t('teamManagement.title') }}</h2>
|
||||
|
||||
<SeasonSelector
|
||||
v-model="selectedSeasonId"
|
||||
@@ -11,24 +11,24 @@
|
||||
<!-- Automatische Jobs Info -->
|
||||
<div v-if="schedulerJobs.rating_updates || schedulerJobs.match_results" class="scheduler-jobs-info">
|
||||
<div class="job-info" v-if="schedulerJobs.rating_updates?.lastRun">
|
||||
<span class="job-label">🔄 Rating-Updates:</span>
|
||||
<span class="job-label">🔄 {{ $t('teamManagement.ratingUpdates') }}:</span>
|
||||
<span class="job-details">
|
||||
Zuletzt: {{ formatJobDate(schedulerJobs.rating_updates.lastRun) }}
|
||||
{{ $t('teamManagement.lastRun') }}: {{ formatJobDate(schedulerJobs.rating_updates.lastRun) }}
|
||||
<span v-if="schedulerJobs.rating_updates.updatedCount !== null" class="job-count">
|
||||
({{ schedulerJobs.rating_updates.updatedCount }} aktualisiert)
|
||||
({{ schedulerJobs.rating_updates.updatedCount }} {{ $t('teamManagement.updated') }})
|
||||
</span>
|
||||
<span v-if="!schedulerJobs.rating_updates.success" class="job-error">⚠️ Fehler</span>
|
||||
<span v-if="!schedulerJobs.rating_updates.success" class="job-error">⚠️ {{ $t('teamManagement.error') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="job-info" v-if="schedulerJobs.match_results?.lastRun">
|
||||
<div class="job-header">
|
||||
<span class="job-label">📊 Spielergebnisse:</span>
|
||||
<span class="job-label">📊 {{ $t('teamManagement.matchResults') }}:</span>
|
||||
<span class="job-details">
|
||||
Zuletzt: {{ formatJobDate(schedulerJobs.match_results.lastRun) }}
|
||||
{{ $t('teamManagement.lastRun') }}: {{ formatJobDate(schedulerJobs.match_results.lastRun) }}
|
||||
<span v-if="schedulerJobs.match_results.fetchedCount !== null" class="job-count">
|
||||
({{ schedulerJobs.match_results.fetchedCount }} abgerufen)
|
||||
({{ schedulerJobs.match_results.fetchedCount }} {{ $t('teamManagement.fetched') }})
|
||||
</span>
|
||||
<span v-if="!schedulerJobs.match_results.success" class="job-error">⚠️ Fehler</span>
|
||||
<span v-if="!schedulerJobs.match_results.success" class="job-error">⚠️ {{ $t('teamManagement.error') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="schedulerJobs.match_results.teamDetails && schedulerJobs.match_results.teamDetails.length > 0" class="team-details">
|
||||
@@ -45,9 +45,9 @@
|
||||
<div class="toggle-new-team">
|
||||
<span @click="toggleNewTeam">
|
||||
<span class="add">{{ teamFormIsOpen ? '-' : '+' }}</span>
|
||||
{{ teamToEdit === null ? "Neues Team" : "Team bearbeiten" }}
|
||||
{{ teamToEdit === null ? $t('teamManagement.newTeam') : $t('teamManagement.editTeam') }}
|
||||
</span>
|
||||
<button v-if="teamToEdit !== null" @click="resetToNewTeam">Neues Team anlegen</button>
|
||||
<button v-if="teamToEdit !== null" @click="resetToNewTeam">{{ $t('teamManagement.createNewTeam') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="teamFormIsOpen" class="new-team-form">
|
||||
@@ -55,14 +55,14 @@
|
||||
<!-- Linke Spalte: Grundeinstellungen -->
|
||||
<div class="basic-settings">
|
||||
<label>
|
||||
<span>Team-Name:</span>
|
||||
<input type="text" v-model="newTeamName" placeholder="z.B. Herren 1, Damen 2">
|
||||
<span>{{ $t('teamManagement.teamName') }}:</span>
|
||||
<input type="text" v-model="newTeamName" :placeholder="$t('teamManagement.teamNamePlaceholder')">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Spielklasse:</span>
|
||||
<span>{{ $t('teamManagement.league') }}:</span>
|
||||
<select v-model="newLeagueId">
|
||||
<option value="">Keine Spielklasse</option>
|
||||
<option value="">{{ $t('teamManagement.noLeague') }}</option>
|
||||
<option v-for="league in filteredLeagues" :key="league.id" :value="league.id">
|
||||
{{ league.name }}
|
||||
</option>
|
||||
@@ -71,39 +71,39 @@
|
||||
|
||||
<div class="form-actions">
|
||||
<button @click="addNewTeam" :disabled="!newTeamName.trim()">
|
||||
{{ teamToEdit ? 'Ändern' : 'Anlegen & Bearbeiten' }}
|
||||
{{ teamToEdit ? $t('teamManagement.change') : $t('teamManagement.createAndEdit') }}
|
||||
</button>
|
||||
<button @click="resetNewTeam" v-if="teamToEdit === null" class="cancel-action">
|
||||
Felder leeren
|
||||
{{ $t('teamManagement.clearFields') }}
|
||||
</button>
|
||||
<button @click="resetToNewTeam" v-if="teamToEdit !== null" class="cancel-action">
|
||||
Neues Team anlegen
|
||||
{{ $t('teamManagement.createNewTeam') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Spieler-Statistik -->
|
||||
<div v-if="teamToEdit && teamToEdit.leagueId" class="player-stats">
|
||||
<div class="stats-header">
|
||||
<span class="section-title">📊 Spieleinsätze</span>
|
||||
<span class="section-title">📊 {{ $t('teamManagement.playerStats') }}</span>
|
||||
<button @click="refreshPlayerStats" :disabled="loadingStats" class="btn-sm">
|
||||
{{ loadingStats ? '⏳' : '🔄' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingStats" class="loading-stats">Lade Statistiken...</div>
|
||||
<div v-if="loadingStats" class="loading-stats">{{ $t('teamManagement.loadingStats') }}</div>
|
||||
|
||||
<div v-else-if="playerStats.length === 0" class="no-stats">
|
||||
Keine Spieleinsätze erfasst.
|
||||
{{ $t('teamManagement.noPlayerStats') }}
|
||||
</div>
|
||||
|
||||
<table v-else class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Spieler</th>
|
||||
<th title="(Q)TTR-Wert">(Q)TTR</th>
|
||||
<th title="Gesamte Saison (ab 1. Juli)">Saison</th>
|
||||
<th :title="isSecondHalf ? 'Rückrunde (ab 1. Januar)' : 'Vorrunde (Juli - Dezember)'">
|
||||
{{ isSecondHalf ? 'Rückrunde' : 'Vorrunde' }}
|
||||
<th>{{ $t('teamManagement.player') }}</th>
|
||||
<th :title="$t('teamManagement.qttr')">(Q)TTR</th>
|
||||
<th :title="$t('teamManagement.seasonFull')">{{ $t('teamManagement.season') }}</th>
|
||||
<th :title="isSecondHalf ? $t('teamManagement.secondHalfFull') : $t('teamManagement.firstHalfFull')">
|
||||
{{ isSecondHalf ? $t('teamManagement.secondHalf') : $t('teamManagement.firstHalf') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -128,40 +128,40 @@
|
||||
<div v-if="teamToEdit" class="advanced-settings">
|
||||
<!-- Upload-Buttons -->
|
||||
<div class="upload-actions compact">
|
||||
<span class="section-title">📋 Dokumente</span>
|
||||
<span class="section-title">📋 {{ $t('teamManagement.documents') }}</span>
|
||||
<div class="upload-buttons-compact">
|
||||
<button @click="uploadCodeList" class="btn-upload-sm">
|
||||
📋 Code-Liste
|
||||
📋 {{ $t('teamManagement.codeList') }}
|
||||
</button>
|
||||
<button @click="uploadPinList" class="btn-upload-sm">
|
||||
🔐 Pin-Liste
|
||||
🔐 {{ $t('teamManagement.pinList') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automatische Jobs Info für dieses Team -->
|
||||
<div v-if="getTeamJobInfo(teamToEdit)" class="team-job-info compact">
|
||||
<span class="section-title">🔄 Automatische Jobs</span>
|
||||
<span class="section-title">🔄 {{ $t('teamManagement.automaticJobs') }}</span>
|
||||
<div v-if="getTeamJobInfo(teamToEdit).lastRun" class="team-job-details">
|
||||
<div class="team-job-item">
|
||||
<span class="team-job-label">Zuletzt aktualisiert:</span>
|
||||
<span class="team-job-label">{{ $t('teamManagement.lastUpdated') }}:</span>
|
||||
<span class="team-job-value">{{ formatJobDate(getTeamJobInfo(teamToEdit).lastRun) }}</span>
|
||||
</div>
|
||||
<div v-if="getTeamJobInfo(teamToEdit).success !== null" class="team-job-item">
|
||||
<span class="team-job-label">Status:</span>
|
||||
<span v-if="getTeamJobInfo(teamToEdit).success" class="team-job-status success">✓ Erfolgreich</span>
|
||||
<span v-else class="team-job-status error">✗ Fehler</span>
|
||||
<span class="team-job-label">{{ $t('teamManagement.status') }}:</span>
|
||||
<span v-if="getTeamJobInfo(teamToEdit).success" class="team-job-status success">✓ {{ $t('teamManagement.successful') }}</span>
|
||||
<span v-else class="team-job-status error">✗ {{ $t('teamManagement.error') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="team-job-details">
|
||||
<span class="team-job-no-data">Noch keine automatische Aktualisierung</span>
|
||||
<span class="team-job-no-data">{{ $t('teamManagement.noAutomaticUpdate') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MyTischtennis URL Konfiguration -->
|
||||
<div class="mytischtennis-config compact">
|
||||
<div class="mytischtennis-header-compact">
|
||||
<span class="section-title">🏓 MyTischtennis</span>
|
||||
<span class="section-title">🏓 {{ $t('teamManagement.myTischtennis') }}</span>
|
||||
<div class="status-inline">
|
||||
<span v-if="getMyTischtennisStatus(teamToEdit).complete" class="badge-sm complete">✓</span>
|
||||
<span v-else-if="getMyTischtennisStatus(teamToEdit).partial" class="badge-sm partial">⚠</span>
|
||||
@@ -182,7 +182,7 @@
|
||||
type="text"
|
||||
v-model="myTischtennisUrl"
|
||||
@keyup.enter="parseMyTischtennisUrl"
|
||||
placeholder="MyTischtennis URL..."
|
||||
:placeholder="$t('teamManagement.myTischtennisUrlPlaceholder')"
|
||||
class="compact-url-input"
|
||||
:disabled="parsingUrl"
|
||||
>
|
||||
@@ -198,10 +198,10 @@
|
||||
</div>
|
||||
|
||||
<div class="teams-list">
|
||||
<h3>Teams ({{ teams.length }}) - Saison {{ currentSeason?.season || 'unbekannt' }}</h3>
|
||||
<h3>{{ $t('teamManagement.teams') }} ({{ teams.length }}) - {{ $t('teamManagement.season') }} {{ currentSeason?.season || $t('teamManagement.seasonUnknown') }}</h3>
|
||||
|
||||
<div v-if="teams.length === 0" class="no-teams">
|
||||
<p>Noch keine Teams vorhanden. Erstellen Sie Ihr erstes Team!</p>
|
||||
<p>{{ $t('teamManagement.noTeamsYet') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="teams-grid">
|
||||
@@ -214,10 +214,10 @@
|
||||
<div class="team-header">
|
||||
<h4>{{ team.name }}</h4>
|
||||
<div class="team-actions">
|
||||
<button @click.stop="editTeam(team)" class="btn-edit" title="Bearbeiten">
|
||||
<button @click.stop="editTeam(team)" class="btn-edit" :title="$t('teamManagement.edit')">
|
||||
✏️
|
||||
</button>
|
||||
<button @click.stop="deleteTeam(team)" class="btn-delete" title="Löschen">
|
||||
<button @click.stop="deleteTeam(team)" class="btn-delete" :title="$t('teamManagement.delete')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
@@ -225,17 +225,17 @@
|
||||
|
||||
<div class="team-info">
|
||||
<div class="info-row">
|
||||
<span class="label">Spielklasse:</span>
|
||||
<span class="label">{{ $t('teamManagement.league') }}:</span>
|
||||
<span class="value">
|
||||
{{ team.league ? team.league.name : 'Keine Zuordnung' }}
|
||||
{{ team.league ? team.league.name : $t('teamManagement.noAssignment') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Saison:</span>
|
||||
<span class="value">{{ team.season?.season || 'Unbekannt' }}</span>
|
||||
<span class="label">{{ $t('teamManagement.season') }}:</span>
|
||||
<span class="value">{{ team.season?.season || $t('teamManagement.unknown') }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Erstellt:</span>
|
||||
<span class="label">{{ $t('teamManagement.created') }}:</span>
|
||||
<span class="value">{{ formatDate(team.createdAt) }}</span>
|
||||
</div>
|
||||
|
||||
@@ -243,13 +243,13 @@
|
||||
<div class="info-row mytischtennis-status">
|
||||
<span class="label">🏓 MyTischtennis:</span>
|
||||
<span v-if="getMyTischtennisStatus(team).complete" class="status-badge complete" :title="getMyTischtennisStatus(team).tooltip">
|
||||
✓ Vollständig konfiguriert
|
||||
✓ {{ $t('teamManagement.fullyConfigured') }}
|
||||
</span>
|
||||
<span v-else-if="getMyTischtennisStatus(team).partial" class="status-badge partial" :title="getMyTischtennisStatus(team).tooltip">
|
||||
⚠ Teilweise konfiguriert
|
||||
⚠ {{ $t('teamManagement.partiallyConfigured') }}
|
||||
</span>
|
||||
<span v-else class="status-badge missing" :title="getMyTischtennisStatus(team).tooltip">
|
||||
✗ Nicht konfiguriert
|
||||
✗ {{ $t('teamManagement.notConfigured') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -263,20 +263,20 @@
|
||||
{{ getTeamJobInfo(team).success ? '✓' : '✗' }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="team-job-no-data">Nie</span>
|
||||
<span v-else class="team-job-no-data">{{ $t('teamManagement.never') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF-Dokumente Icons -->
|
||||
<div class="team-documents">
|
||||
<div class="documents-label">Dokumente:</div>
|
||||
<div class="documents-label">{{ $t('teamManagement.documents') }}:</div>
|
||||
<div class="document-icons">
|
||||
<button
|
||||
v-if="getTeamDocuments(team.id, 'code_list').length > 0"
|
||||
@click.stop="showPDFDialog(team.id, 'code_list')"
|
||||
class="document-icon code-list-icon"
|
||||
title="Code-Liste anzeigen"
|
||||
:title="$t('teamManagement.showCodeList')"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
@@ -284,7 +284,7 @@
|
||||
v-if="getTeamDocuments(team.id, 'pin_list').length > 0"
|
||||
@click.stop="showPDFDialog(team.id, 'pin_list')"
|
||||
class="document-icon pin-list-icon"
|
||||
title="Pin-Liste anzeigen"
|
||||
:title="$t('teamManagement.showPinList')"
|
||||
>
|
||||
🔐
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="tournaments-view">
|
||||
<div class="tournament-config">
|
||||
<h3>Datum</h3>
|
||||
<h3>{{ $t('tournaments.date') }}</h3>
|
||||
<select v-model="selectedDate">
|
||||
<option value="new">Neues Turnier</option>
|
||||
<option value="new">{{ $t('tournaments.newTournament') }}</option>
|
||||
<option v-for="date in dates" :key="date.id" :value="date.id">
|
||||
<template v-if="date.name">
|
||||
{{ date.name }} ({{ date.date ? new Date(date.date).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}) : 'Unbekanntes Datum' }})
|
||||
}) : $t('tournaments.unknownDate') }})
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ date.date ? new Date(date.date).toLocaleDateString('de-DE', {
|
||||
@@ -23,43 +23,43 @@
|
||||
</select>
|
||||
<div v-if="selectedDate === 'new'" class="new-tournament">
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" v-model="newTournamentName" placeholder="Turniername" />
|
||||
{{ $t('tournaments.name') }}:
|
||||
<input type="text" v-model="newTournamentName" :placeholder="$t('tournaments.tournamentName')" />
|
||||
</label>
|
||||
<label>
|
||||
Datum:
|
||||
{{ $t('tournaments.date') }}:
|
||||
<input type="date" v-model="newDate" />
|
||||
</label>
|
||||
<label>
|
||||
Gewinnsätze:
|
||||
{{ $t('tournaments.winningSets') }}:
|
||||
<input type="number" v-model.number="newWinningSets" min="1" />
|
||||
</label>
|
||||
<button @click="createTournament">Erstellen</button>
|
||||
<button @click="createTournament">{{ $t('tournaments.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedDate !== 'new'" class="tournament-setup">
|
||||
<div class="tournament-info">
|
||||
<label>
|
||||
Name:
|
||||
{{ $t('tournaments.name') }}:
|
||||
<input type="text" v-model="currentTournamentName" @input="updateTournament" />
|
||||
</label>
|
||||
<label>
|
||||
Datum:
|
||||
{{ $t('tournaments.date') }}:
|
||||
<input type="date" v-model="currentTournamentDate" @change="updateTournament" />
|
||||
</label>
|
||||
<label>
|
||||
Gewinnsätze:
|
||||
{{ $t('tournaments.winningSets') }}:
|
||||
<input type="number" v-model.number="currentWinningSets" min="1" @input="updateTournament" />
|
||||
</label>
|
||||
<button @click="generatePDF" class="btn-primary" style="margin-top: 1rem;">PDF exportieren</button>
|
||||
<button @click="generatePDF" class="btn-primary" style="margin-top: 1rem;">{{ $t('tournaments.exportPDF') }}</button>
|
||||
</div>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="isGroupTournament" @change="onModusChange" />
|
||||
<span>Spielen in Gruppen</span>
|
||||
<span>{{ $t('tournaments.playInGroups') }}</span>
|
||||
</label>
|
||||
<section class="tournament-classes">
|
||||
<div class="classes-header" @click="toggleClasses">
|
||||
<h4>Klassen</h4>
|
||||
<h4>{{ $t('tournaments.classes') }}</h4>
|
||||
<span class="collapse-icon" :class="{ 'expanded': showClasses }">▼</span>
|
||||
</div>
|
||||
<div v-show="showClasses" class="classes-content">
|
||||
@@ -67,25 +67,25 @@
|
||||
<div v-for="classItem in tournamentClasses" :key="classItem.id" class="class-item">
|
||||
<template v-if="editingClassId === classItem.id">
|
||||
<input type="text" v-model="editingClassName" @keyup.enter="saveClassEdit(classItem)" @blur="saveClassEdit(classItem)" @keyup.esc="cancelClassEdit" class="class-name-input" ref="classEditInput" />
|
||||
<button @click="saveClassEdit(classItem)" class="btn-save-small" title="Speichern">✓</button>
|
||||
<button @click="cancelClassEdit" class="btn-cancel-small" title="Abbrechen">✕</button>
|
||||
<button @click="saveClassEdit(classItem)" class="btn-save-small" :title="$t('tournaments.save')">✓</button>
|
||||
<button @click="cancelClassEdit" class="btn-cancel-small" :title="$t('tournaments.cancel')">✕</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="class-name-label">{{ classItem.name }}</span>
|
||||
<button @click.stop="editClass(classItem)" class="btn-edit-small" title="Bearbeiten">✏️</button>
|
||||
<button @click.stop="deleteClass(classItem)" class="trash-btn-small" title="Löschen">🗑️</button>
|
||||
<button @click.stop="editClass(classItem)" class="btn-edit-small" :title="$t('tournaments.edit')">✏️</button>
|
||||
<button @click.stop="deleteClass(classItem)" class="trash-btn-small" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-class">
|
||||
<input type="text" v-model="newClassName" placeholder="Klassenname" class="class-name-input" @keyup.enter="addClass" />
|
||||
<button @click="addClass" class="btn-add">Klasse hinzufügen</button>
|
||||
<input type="text" v-model="newClassName" :placeholder="$t('tournaments.className')" class="class-name-input" @keyup.enter="addClass" />
|
||||
<button @click="addClass" class="btn-add">{{ $t('tournaments.addClass') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="participants">
|
||||
<div class="participants-header" @click="toggleParticipants">
|
||||
<h4>Teilnehmer</h4>
|
||||
<h4>{{ $t('tournaments.participants') }}</h4>
|
||||
<span class="collapse-icon" :class="{ 'expanded': showParticipants }">▼</span>
|
||||
</div>
|
||||
<div v-show="showParticipants" class="participants-content">
|
||||
@@ -93,12 +93,12 @@
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-seeded-cell">Gesetzt</th>
|
||||
<th class="participant-name">Name</th>
|
||||
<th v-if="allowsExternal" class="participant-club-cell">Verein</th>
|
||||
<th class="participant-class-cell">Klasse</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">Gruppe</th>
|
||||
<th class="participant-action-cell">Aktion</th>
|
||||
<th class="participant-seeded-cell">{{ $t('tournaments.seeded') }}</th>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
|
||||
<th class="participant-class-cell">{{ $t('tournaments.class') }}</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
|
||||
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
@@ -113,17 +113,17 @@
|
||||
</td>
|
||||
<td class="participant-name">
|
||||
<template v-if="participant.member">
|
||||
{{ participant.member.firstName || 'Unbekannt' }}
|
||||
{{ participant.member.firstName || $t('tournaments.unknown') }}
|
||||
{{ participant.member.lastName || '' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ participant.firstName || 'Unbekannt' }}
|
||||
{{ participant.firstName || $t('tournaments.unknown') }}
|
||||
{{ participant.lastName || '' }}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="allowsExternal" class="participant-club-cell">
|
||||
<template v-if="participant.member">
|
||||
<em>(Vereinsmitglied)</em>
|
||||
<em>{{ $t('tournaments.clubMember') }}</em>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ participant.club || '–' }}
|
||||
@@ -146,7 +146,7 @@
|
||||
</select>
|
||||
</td>
|
||||
<td class="participant-action-cell">
|
||||
<button @click="removeParticipant(participant)" class="trash-btn-small" title="Löschen">🗑️</button>
|
||||
<button @click="removeParticipant(participant)" class="trash-btn-small" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -155,34 +155,34 @@
|
||||
</div>
|
||||
<div class="add-participant">
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>Vereinsmitglied hinzufügen</h5>
|
||||
<h5>{{ $t('tournaments.addClubMember') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">-- Teilnehmer auswählen --</option>
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">Hinzufügen</button>
|
||||
<button @click="addParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
<button v-if="hasTrainingToday && !allowsExternal" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 Aus Trainingstag laden
|
||||
📅 {{ $t('tournaments.loadFromTraining') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>Externen Teilnehmer hinzufügen</h5>
|
||||
<h5>{{ $t('tournaments.addExternalParticipant') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<input type="text" v-model="newExternalParticipant.firstName" placeholder="Vorname" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" placeholder="Nachname" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" placeholder="Verein (optional)" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" placeholder="Geburtsdatum (optional)" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">Hinzufügen</button>
|
||||
<input type="text" v-model="newExternalParticipant.firstName" :placeholder="$t('tournaments.firstName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" :placeholder="$t('tournaments.lastName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" :placeholder="$t('tournaments.club') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" :placeholder="$t('tournaments.birthdate') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowsExternal" class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">-- Teilnehmer auswählen --</option>
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
@@ -198,37 +198,37 @@
|
||||
</section>
|
||||
<section v-if="isGroupTournament" class="group-controls">
|
||||
<label>
|
||||
Aufsteiger pro Gruppe:
|
||||
{{ $t('tournaments.advancersPerGroup') }}:
|
||||
<input type="number" v-model.number="advancingPerGroup" min="1" @change="onModusChange" />
|
||||
</label>
|
||||
<label style="margin-left:1em">
|
||||
Maximale Gruppengröße:
|
||||
{{ $t('tournaments.maxGroupSize') }}:
|
||||
<input type="number" v-model.number="maxGroupSize" min="1" />
|
||||
</label>
|
||||
|
||||
<div v-if="tournamentClasses.length > 0" class="groups-per-class">
|
||||
<h4>Gruppen pro Klasse</h4>
|
||||
<h4>{{ $t('tournaments.groupsPerClass') }}</h4>
|
||||
<div v-for="classItem in tournamentClasses" :key="classItem.id" class="class-group-config">
|
||||
<label>
|
||||
{{ classItem.name }}:
|
||||
<input type="number" v-model.number="groupsPerClass[classItem.id]" min="0" @change="onGroupCountChange" style="width: 60px;" />
|
||||
Gruppen
|
||||
{{ $t('tournaments.group') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="groups-per-class">
|
||||
<label>
|
||||
Anzahl Gruppen:
|
||||
{{ $t('tournaments.numberOfGroups') }}:
|
||||
<input type="number" v-model.number="numberOfGroups" min="1" @change="onGroupCountChange" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button @click="createGroups">Gruppen erstellen</button>
|
||||
<button @click="randomizeGroups">Zufällig verteilen</button>
|
||||
<button @click="resetGroups">Gruppen zurücksetzen</button>
|
||||
<button @click="createGroups">{{ $t('tournaments.createGroups') }}</button>
|
||||
<button @click="randomizeGroups">{{ $t('tournaments.randomizeGroups') }}</button>
|
||||
<button @click="resetGroups">{{ $t('tournaments.resetGroups') }}</button>
|
||||
</section>
|
||||
<section v-if="groups.length" class="groups-overview">
|
||||
<h3>Gruppenübersicht</h3>
|
||||
<h3>{{ $t('tournaments.groupsOverview') }}</h3>
|
||||
<template v-for="(classGroups, classId) in groupsByClass" :key="classId">
|
||||
<div v-if="classId !== 'null' && classId !== 'undefined'" class="class-section">
|
||||
<h4 class="class-header">
|
||||
@@ -236,20 +236,20 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div v-for="group in classGroups" :key="group.groupId" class="group-table">
|
||||
<h4>Gruppe {{ group.groupNumber }}</h4>
|
||||
<h4>{{ $t('tournaments.groupNumber') }} {{ group.groupNumber }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Index</th>
|
||||
<th>Platz</th>
|
||||
<th>Spieler</th>
|
||||
<th>Punkte</th>
|
||||
<th>Satz</th>
|
||||
<th>Diff</th>
|
||||
<th>{{ $t('tournaments.index') }}</th>
|
||||
<th>{{ $t('tournaments.position') }}</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
<th>{{ $t('tournaments.points') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.diff') }}</th>
|
||||
<th v-for="(opponent, idx) in groupRankings[group.groupId]" :key="`opp-${opponent.id}`">
|
||||
G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}
|
||||
</th>
|
||||
<th>Live-Platz</th>
|
||||
<th>{{ $t('tournaments.livePosition') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -281,22 +281,22 @@
|
||||
</template>
|
||||
<div class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="resetMatches" class="trash-btn">
|
||||
🗑️ Gruppenspiele
|
||||
🗑️ {{ $t('tournaments.resetGroupMatches') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section v-if="groupMatches.length" class="group-matches">
|
||||
<h4>Gruppenspiele</h4>
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Runde</th>
|
||||
<th>Gruppe</th>
|
||||
<th>Begegnung</th>
|
||||
<th>Ergebnis</th>
|
||||
<th>Sätze</th>
|
||||
<th>Aktion</th>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th>{{ $t('tournaments.group') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -304,10 +304,10 @@
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>
|
||||
<template v-if="getGroupClassName(m.groupId)">
|
||||
{{ getGroupClassName(m.groupId) }} - Gruppe {{ m.groupNumber }}
|
||||
{{ getGroupClassName(m.groupId) }} - {{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
</template>
|
||||
<template v-else>
|
||||
Gruppe {{ m.groupNumber }}
|
||||
{{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
@@ -527,31 +527,31 @@
|
||||
&& !groupMatches.length
|
||||
&& !knockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<button @click="startMatches">
|
||||
Spiele erstellen
|
||||
{{ $t('tournaments.createMatches') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout && getTotalNumberOfGroups > 1" class="ko-start">
|
||||
<button @click="startKnockout">
|
||||
K.o.-Runde starten
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showKnockout && canResetKnockout && getTotalNumberOfGroups > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="resetKnockout" class="trash-btn">
|
||||
🗑️ K.o.-Runde
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="showKnockout && getTotalNumberOfGroups > 1" class="ko-round">
|
||||
<h4>K.-o.-Runde</h4>
|
||||
<h4>{{ $t('tournaments.koRound') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Klasse</th>
|
||||
<th>Runde</th>
|
||||
<th>Begegnung</th>
|
||||
<th>Ergebnis</th>
|
||||
<th>Sätze</th>
|
||||
<th>Aktion</th>
|
||||
<th>{{ $t('tournaments.class') }}</th>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1701,8 +1701,8 @@ export default {
|
||||
await this.loadTournamentData();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Turniers:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Aktualisieren des Turniers.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('tournaments.errorUpdatingTournament'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
// Lade Daten neu, um die ursprünglichen Werte wiederherzustellen
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
@@ -1710,7 +1710,7 @@ export default {
|
||||
|
||||
async createTournament() {
|
||||
if (!this.newDate) {
|
||||
await this.showInfo('Fehler', 'Bitte geben Sie ein Datum ein!', '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseEnterDate'), '', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -1737,14 +1737,14 @@ export default {
|
||||
this.newWinningSets = 3;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Turniers:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Erstellen des Turniers.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(error, this.$t('tournaments.errorCreatingTournament'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async addParticipant() {
|
||||
if (!this.selectedMember) {
|
||||
await this.showInfo('Fehler', 'Bitte wählen Sie einen Teilnehmer aus!', '', 'error');
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseSelectParticipant'), '', 'error');
|
||||
return;
|
||||
}
|
||||
const oldMap = this.participants.reduce((map, p) => {
|
||||
@@ -1785,9 +1785,9 @@ export default {
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Gruppen:', error);
|
||||
const message = error.response?.data?.error || error.message || 'Fehler beim Erstellen der Gruppen.';
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
console.error(this.$t('tournaments.errorCreatingGroups'), error);
|
||||
const message = error.response?.data?.error || error.message || this.$t('tournaments.errorCreatingGroups');
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1852,7 +1852,7 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
const pdf = new PDFGenerator();
|
||||
const pdf = new PDFGenerator(20, 10, this.$t);
|
||||
|
||||
// Bereite Daten vor - erstelle Mapping von groupId zu classId
|
||||
const groupIdToClassId = {};
|
||||
@@ -2141,8 +2141,8 @@ export default {
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
} catch (err) {
|
||||
const message = safeErrorMessage(err, 'Fehler beim Zurücksetzen der K.o.-Runde.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
const message = safeErrorMessage(err, this.$t('tournaments.errorResettingKORound'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
:class="['tab-button', { active: activeTab === 'internal' }]"
|
||||
@click="switchTab('internal')"
|
||||
>
|
||||
🏆 Interne Turniere
|
||||
🏆 {{ $t('tournaments.internalTournaments') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'external' }]"
|
||||
@click="switchTab('external')"
|
||||
>
|
||||
🌐 Offene Turniere
|
||||
🌐 {{ $t('tournaments.openTournaments') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'official' }]"
|
||||
@click="switchTab('official')"
|
||||
>
|
||||
📄 Turnierteilnahmen
|
||||
📄 {{ $t('tournaments.tournamentParticipations') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="training-stats">
|
||||
<h2>Trainings-Statistik</h2>
|
||||
<h2>{{ $t('trainingStats.title') }}</h2>
|
||||
|
||||
<div class="stats-overview">
|
||||
<div class="stats-summary">
|
||||
<div class="stat-card">
|
||||
<h3>Aktive Mitglieder</h3>
|
||||
<h3>{{ $t('trainingStats.activeMembers') }}</h3>
|
||||
<div class="stat-number">{{ activeMembers.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Durchschnittliche Teilnahme (aktueller Monat)</h3>
|
||||
<h3>{{ $t('trainingStats.averageParticipationCurrentMonth') }}</h3>
|
||||
<div class="stat-number">{{ averageParticipationCurrentMonth.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Durchschnittliche Teilnahme (letzter Monat)</h3>
|
||||
<h3>{{ $t('trainingStats.averageParticipationLastMonth') }}</h3>
|
||||
<div class="stat-number">{{ averageParticipationLastMonth.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Durchschnittliche Teilnahme (Quartal)</h3>
|
||||
<h3>{{ $t('trainingStats.averageParticipationQuarter') }}</h3>
|
||||
<div class="stat-number">{{ averageParticipationQuarter.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Durchschnittliche Teilnahme (Halbjahr)</h3>
|
||||
<h3>{{ $t('trainingStats.averageParticipationHalfYear') }}</h3>
|
||||
<div class="stat-number">{{ averageParticipationHalfYear.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Durchschnittliche Teilnahme (Jahr)</h3>
|
||||
<h3>{{ $t('trainingStats.averageParticipationYear') }}</h3>
|
||||
<div class="stat-number">{{ averageParticipationYear.toFixed(1) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<!-- Trainingstage-Tabelle (standardmäßig aufgeklappt) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleTrainingDays">
|
||||
<h3>Trainingstage (letzte 12 Monate)</h3>
|
||||
<h3>{{ $t('trainingStats.trainingDays') }}</h3>
|
||||
<span class="toggle-icon">{{ showTrainingDays ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showTrainingDays" class="section-content">
|
||||
@@ -42,9 +42,9 @@
|
||||
<table class="training-days-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Wochentag</th>
|
||||
<th>Teilnehmer</th>
|
||||
<th>{{ $t('trainingStats.date') }}</th>
|
||||
<th>{{ $t('trainingStats.weekday') }}</th>
|
||||
<th>{{ $t('trainingStats.participants') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -62,7 +62,7 @@
|
||||
<!-- Mitglieder-Tabelle (standardmäßig eingeklappt) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleMembers">
|
||||
<h3>Mitglieder-Teilnahmen</h3>
|
||||
<h3>{{ $t('trainingStats.memberParticipations') }}</h3>
|
||||
<span class="toggle-icon">{{ showMembers ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showMembers" class="section-content">
|
||||
@@ -72,38 +72,38 @@
|
||||
<tr>
|
||||
<th @click="sortBy('name')" class="sortable-header">
|
||||
<div class="header-content">
|
||||
<span>Name</span>
|
||||
<span>{{ $t('trainingStats.name') }}</span>
|
||||
<span class="sort-icon">{{ getSortIcon('name') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>TTR</th>
|
||||
<th>QTTR</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>{{ $t('trainingStats.ttr') }}</th>
|
||||
<th>{{ $t('trainingStats.qttr') }}</th>
|
||||
<th>{{ $t('trainingStats.birthdate') }}</th>
|
||||
<th @click="sortBy('participation12Months')" class="sortable-header">
|
||||
<div class="header-content">
|
||||
<span>Teilnahmen (12 Monate)</span>
|
||||
<span>{{ $t('trainingStats.participations12Months') }}</span>
|
||||
<span class="sort-icon">{{ getSortIcon('participation12Months') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('participation3Months')" class="sortable-header">
|
||||
<div class="header-content">
|
||||
<span>Teilnahmen (3 Monate)</span>
|
||||
<span>{{ $t('trainingStats.participations3Months') }}</span>
|
||||
<span class="sort-icon">{{ getSortIcon('participation3Months') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('participationTotal')" class="sortable-header">
|
||||
<div class="header-content">
|
||||
<span>Teilnahmen (Gesamt)</span>
|
||||
<span>{{ $t('trainingStats.participationsTotal') }}</span>
|
||||
<span class="sort-icon">{{ getSortIcon('participationTotal') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('lastTrainingTs')" class="sortable-header">
|
||||
<div class="header-content">
|
||||
<span>Letztes Training</span>
|
||||
<span>{{ $t('trainingStats.lastTraining') }}</span>
|
||||
<span class="sort-icon">{{ getSortIcon('lastTrainingTs') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>Aktionen</th>
|
||||
<th>{{ $t('trainingStats.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -118,7 +118,7 @@
|
||||
<td>{{ formatDate(member.lastTraining) || '-' }}</td>
|
||||
<td>
|
||||
<button @click="showMemberDetails(member)" class="btn-primary btn-small">
|
||||
Details anzeigen
|
||||
{{ $t('trainingStats.showDetails') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user